
Collaborative visual editor assembling compiled catalog components into semantic Compose trees, live Wasm rendering, and exporting designs as Figma-compatible SVG or readable Compose source.
A collaborative visual editor for Compose Multiplatform screens. A person or an agent assembles real, compiled catalog components into a semantic Compose tree, sees that tree rendered by Compose/Wasm, and exports the same saved design as Figma-compatible SVG or readable Compose source.
Extracted from yschimke/compose-preview-server,
where these nine modules were already a second project with an enforced boundary. That boundary is
now a repository boundary; the document that defined it came with them and is still the normative
statement of what may depend on what
(docs/design/UI_BUILDER_PROJECT_BOUNDARY.md).
Start with docs/design/UI_BUILDER_PRODUCT_SPEC.md for
what the product is, and docs/UI_BUILDER_GETTING_STARTED.md
for running it.
| Module | Targets | What it is |
|---|---|---|
:ui-builder |
jvm, wasmJs
|
the editor — canvas, palette, inspector, reducer, exporters, offline service |
:ui-builder-desktop |
JVM desktop | native offline host for the editor; useful for testing and embedding |
:ui-builder-runtime |
JVM | the design service: state, catalog validation, revision-pinned export |
:ui-builder-export |
jvm, wasmJs
|
design → screen-model projection |
:ui-builder-renderer |
wasmJs |
the sandboxed renderer-only runtime |
:ui-builder-web |
— | packages the editor's Wasm output as an immutable archive |
:ui-builder-render-bundle |
— | packages the editor's JVM previews as the polyglot render bundle |
:ui-builder-artwork |
jvm, wasmJs
|
offline artwork bindings |
:ui-builder-reference-jetcaster |
wasmJs |
reference-fidelity fixture |
:ui-builder-generated-jetcaster |
— | generated-fidelity fixture |
./gradlew checkTwo Java floors, both named in gradle/libs.versions.toml: the editor's JVM lane compiles at
21, everything a consumer resolves stays at 17, and Gradle needs both toolchains.
compose-preview-server hosts this editor. It consumes exactly four modules —
:ui-builder-runtime, :ui-builder-export, :ui-builder-web and :ui-builder-render-bundle —
and nothing here may depend on it.
A release goes out in two halves, because the four seams are not the same kind of thing.
Maven Central — three jars and a BOM. What a consumer compiles or resolves against:
| Coordinate | |
|---|---|
ee.schimke.composeai:compose-preview-ui-builder-bom |
version constraints for the three below |
…:compose-preview-ui-builder-runtime |
the design service |
…:compose-preview-ui-builder-export |
the design → screen-model projection |
…:compose-preview-ui-builder-render-bundle |
the packaged preview a design renders through |
-render-bundle is on that list even though nobody names it directly: it is an api dependency of
the runtime, so its coordinate appears in the runtime's POM, and a POM naming an artifact nobody
uploaded is what made compose-preview-serve unresolvable for six consecutive releases.
GitHub release assets — a Wasm ZIP and a Linux Desktop app.
compose-preview-ui-builder-web-<version>.zip is the Wasm editor, which a host unpacks; nothing
compiles against it or resolves it transitively. The release also carries
compose-ui-builder-desktop_<version>_amd64.deb, the native offline Compose Desktop application.
The web bundle stays off Central because a 40 MB frontend distribution published there is permanent
and serves no one. compose-preview-server reaches it through a group-fenced ivy repository over
this repository's releases, so it remains an ordinary versioned dependency:
ivy("https://github.com/yschimke/compose-ui-builder/releases/download") {
patternLayout { artifact("[revision]/[module]-[revision].[ext]") }
content { includeModule("ee.schimke.composeai", "compose-preview-ui-builder-web") }
metadataSources { artifact() }
}:ui-builder-web therefore does not apply composeai.maven-publishing, and that absence is
load-bearing: the release set is derived from which modules apply it, so a module that does not
publish cannot be constrained by a BOM that promises it.
A consumer takes the BOM and then names no versions at all:
implementation(platform("ee.schimke.composeai:compose-preview-ui-builder-bom:<version>"))
implementation("ee.schimke.composeai:compose-preview-ui-builder-runtime")
implementation("ee.schimke.composeai:compose-preview-ui-builder-export")That matters here beyond convenience: the runtime and the export share the screen-model generator, and a version skew between them does not fail resolution — it produces an export that differs between the browser and the service.
Nothing has been published yet. The lane is configured and verified, but no release has gone out, so compose-preview-server still resolves these four by including a checkout of this repository as a composite build.
The set is derived, never listed: a module publishes if and only if its build script applies
composeai.maven-publishing, and :bom constrains exactly that set. Adding a module to the
release is applying the plugin; there is no list to update.
Publishing needs Maven Central credentials and a signing key, which nothing in this repository
carries. Supply them as Gradle properties (~/.gradle/gradle.properties) or environment variables:
mavenCentralUsername=<Central portal token username>
mavenCentralPassword=<Central portal token password>
signingInMemoryKey=<ASCII-armoured secret key, newlines as \n>
signingInMemoryKeyPassword=<key passphrase>Then, with the release version in PLUGIN_VERSION:
PLUGIN_VERSION=0.1.0 ./gradlew publishReleaseArtifacts
PLUGIN_VERSION=0.1.0 ./gradlew :ui-builder-web:webArchive
gh release upload v0.1.0 \
ui-builder-web/build/distributions/compose-preview-ui-builder-web-0.1.0.zip.github/workflows/release.yml does all of this from a v* tag, given the four secrets, and runs
the external-consumer gate first — so a POM naming an unresolvable coordinate fails before anything
reaches Central, where it cannot be taken back.
publishReleaseArtifacts publishes every module in the derived set and fails first if the set has
lost one of the four seams or the BOM. Without PLUGIN_VERSION every module takes the next patch
as a -SNAPSHOT, which is unsigned and safe for publishToMavenLocal.
To exercise the whole lane without credentials — which is what CI does on every pull request:
./scripts/check-ui-builder-external-consumer.shIt stages the derived set into a throwaway repository at a snapshot version, then resolves it from a synthetic consumer outside this source tree.
This repository resolves compose-ai-tools, the preview daemon and the contracts line as published coordinates. To build against your own checkout of one instead, name it:
./gradlew check -PlocalBuilds=tools
./gradlew check -PlocalBuilds=tools,daemon
./gradlew check -PlocalBuilds=allEach defaults to a sibling directory beside this one (../compose-ai-tools,
../compose-preview-daemon, ../compose-preview-contracts); -PlocalBuild.tools=<path> points
somewhere else. Naming a sibling whose directory is missing is an error rather than a silent
fallback to Maven. settings.gradle.kts holds the mechanism and the reasoning.
The older scripts/stage-local-dependency.py route — compile an upstream module into a
workspace-local Maven repository and pin it through a manifest — still works and is the right tool
when you want one fixed upstream build rather than a live one.
A collaborative visual editor for Compose Multiplatform screens. A person or an agent assembles real, compiled catalog components into a semantic Compose tree, sees that tree rendered by Compose/Wasm, and exports the same saved design as Figma-compatible SVG or readable Compose source.
Extracted from yschimke/compose-preview-server,
where these nine modules were already a second project with an enforced boundary. That boundary is
now a repository boundary; the document that defined it came with them and is still the normative
statement of what may depend on what
(docs/design/UI_BUILDER_PROJECT_BOUNDARY.md).
Start with docs/design/UI_BUILDER_PRODUCT_SPEC.md for
what the product is, and docs/UI_BUILDER_GETTING_STARTED.md
for running it.
| Module | Targets | What it is |
|---|---|---|
:ui-builder |
jvm, wasmJs
|
the editor — canvas, palette, inspector, reducer, exporters, offline service |
:ui-builder-desktop |
JVM desktop | native offline host for the editor; useful for testing and embedding |
:ui-builder-runtime |
JVM | the design service: state, catalog validation, revision-pinned export |
:ui-builder-export |
jvm, wasmJs
|
design → screen-model projection |
:ui-builder-renderer |
wasmJs |
the sandboxed renderer-only runtime |
:ui-builder-web |
— | packages the editor's Wasm output as an immutable archive |
:ui-builder-render-bundle |
— | packages the editor's JVM previews as the polyglot render bundle |
:ui-builder-artwork |
jvm, wasmJs
|
offline artwork bindings |
:ui-builder-reference-jetcaster |
wasmJs |
reference-fidelity fixture |
:ui-builder-generated-jetcaster |
— | generated-fidelity fixture |
./gradlew checkTwo Java floors, both named in gradle/libs.versions.toml: the editor's JVM lane compiles at
21, everything a consumer resolves stays at 17, and Gradle needs both toolchains.
compose-preview-server hosts this editor. It consumes exactly four modules —
:ui-builder-runtime, :ui-builder-export, :ui-builder-web and :ui-builder-render-bundle —
and nothing here may depend on it.
A release goes out in two halves, because the four seams are not the same kind of thing.
Maven Central — three jars and a BOM. What a consumer compiles or resolves against:
| Coordinate | |
|---|---|
ee.schimke.composeai:compose-preview-ui-builder-bom |
version constraints for the three below |
…:compose-preview-ui-builder-runtime |
the design service |
…:compose-preview-ui-builder-export |
the design → screen-model projection |
…:compose-preview-ui-builder-render-bundle |
the packaged preview a design renders through |
-render-bundle is on that list even though nobody names it directly: it is an api dependency of
the runtime, so its coordinate appears in the runtime's POM, and a POM naming an artifact nobody
uploaded is what made compose-preview-serve unresolvable for six consecutive releases.
GitHub release assets — a Wasm ZIP and a Linux Desktop app.
compose-preview-ui-builder-web-<version>.zip is the Wasm editor, which a host unpacks; nothing
compiles against it or resolves it transitively. The release also carries
compose-ui-builder-desktop_<version>_amd64.deb, the native offline Compose Desktop application.
The web bundle stays off Central because a 40 MB frontend distribution published there is permanent
and serves no one. compose-preview-server reaches it through a group-fenced ivy repository over
this repository's releases, so it remains an ordinary versioned dependency:
ivy("https://github.com/yschimke/compose-ui-builder/releases/download") {
patternLayout { artifact("[revision]/[module]-[revision].[ext]") }
content { includeModule("ee.schimke.composeai", "compose-preview-ui-builder-web") }
metadataSources { artifact() }
}:ui-builder-web therefore does not apply composeai.maven-publishing, and that absence is
load-bearing: the release set is derived from which modules apply it, so a module that does not
publish cannot be constrained by a BOM that promises it.
A consumer takes the BOM and then names no versions at all:
implementation(platform("ee.schimke.composeai:compose-preview-ui-builder-bom:<version>"))
implementation("ee.schimke.composeai:compose-preview-ui-builder-runtime")
implementation("ee.schimke.composeai:compose-preview-ui-builder-export")That matters here beyond convenience: the runtime and the export share the screen-model generator, and a version skew between them does not fail resolution — it produces an export that differs between the browser and the service.
Nothing has been published yet. The lane is configured and verified, but no release has gone out, so compose-preview-server still resolves these four by including a checkout of this repository as a composite build.
The set is derived, never listed: a module publishes if and only if its build script applies
composeai.maven-publishing, and :bom constrains exactly that set. Adding a module to the
release is applying the plugin; there is no list to update.
Publishing needs Maven Central credentials and a signing key, which nothing in this repository
carries. Supply them as Gradle properties (~/.gradle/gradle.properties) or environment variables:
mavenCentralUsername=<Central portal token username>
mavenCentralPassword=<Central portal token password>
signingInMemoryKey=<ASCII-armoured secret key, newlines as \n>
signingInMemoryKeyPassword=<key passphrase>Then, with the release version in PLUGIN_VERSION:
PLUGIN_VERSION=0.1.0 ./gradlew publishReleaseArtifacts
PLUGIN_VERSION=0.1.0 ./gradlew :ui-builder-web:webArchive
gh release upload v0.1.0 \
ui-builder-web/build/distributions/compose-preview-ui-builder-web-0.1.0.zip.github/workflows/release.yml does all of this from a v* tag, given the four secrets, and runs
the external-consumer gate first — so a POM naming an unresolvable coordinate fails before anything
reaches Central, where it cannot be taken back.
publishReleaseArtifacts publishes every module in the derived set and fails first if the set has
lost one of the four seams or the BOM. Without PLUGIN_VERSION every module takes the next patch
as a -SNAPSHOT, which is unsigned and safe for publishToMavenLocal.
To exercise the whole lane without credentials — which is what CI does on every pull request:
./scripts/check-ui-builder-external-consumer.shIt stages the derived set into a throwaway repository at a snapshot version, then resolves it from a synthetic consumer outside this source tree.
This repository resolves compose-ai-tools, the preview daemon and the contracts line as published coordinates. To build against your own checkout of one instead, name it:
./gradlew check -PlocalBuilds=tools
./gradlew check -PlocalBuilds=tools,daemon
./gradlew check -PlocalBuilds=allEach defaults to a sibling directory beside this one (../compose-ai-tools,
../compose-preview-daemon, ../compose-preview-contracts); -PlocalBuild.tools=<path> points
somewhere else. Naming a sibling whose directory is missing is an error rather than a silent
fallback to Maven. settings.gradle.kts holds the mechanism and the reasoning.
The older scripts/stage-local-dependency.py route — compile an upstream module into a
workspace-local Maven repository and pin it through a manifest — still works and is the right tool
when you want one fixed upstream build rather than a live one.