compose-design-catalog · git:20260830.a0e7724 · 2026-08-30 · sha256 8592de7e0e860da1

compose-design-catalog git:20260830.a0e7724A

Immutable. This exact content is served forever at /api/v1/blob/8592de7e0e860da1.

---
name: compose-design-catalog
description: Generate an importable design-artifact sticker sheet for a whole Compose component system (Compose M3, Wear Compose M3, Glimmer, Glance/Wear widgets). Use when you want a code-derived component catalog — each component in its primary modes, in two variants (ideal render + bordered layout), with extracted design tokens and accessibility greenlines — laid out for import into Figma, Google Stitch, or Claude Design. Pairs with the compose-preview and compose-preview-design-board skills.
---

# Compose Design Catalog

Render a Compose **component system** and export it as an importable **sticker
sheet**: every component in its primary modes, in two variants (the `ideal`
render and a `layout` render that borders every composable), with the system's
design tokens and an accessibility **greenline** layer extracted automatically
from the render — not transcribed from a spec.

This skill is the system-wide, sticker-sheet sibling of
**compose-preview-design-board** (which arranges an arbitrary set of renders
into one HTML brief). It assumes the **compose-preview** skill is installed —
that skill owns the renderer, CLI, and Gradle plugin. Check first with
`compose-preview --version`; if missing, run the bootstrap installer:

```sh
curl -fsSL https://raw.githubusercontent.com/yschimke/skills/main/scripts/install.sh \
  | bash
```

## Code is the source of truth

This pipeline is **code-led**. Every value on the sheet — padding, corner
radius, type, colour, touch-target size, `maxLines` / overflow — comes from the
renderer's own data products, so the catalog is correct by construction.
Published design kits (the Material 3 / Wear OS / Glimmer / Widget Figma kits)
are **seed/reference only**: use them for the component inventory and for naming
parity, never as authority. A kit/render divergence is a bug in the kit.

## When to use this skill

- You want a designer-ready catalog of a component library (yours, or a system
  like Material 3) generated from the code, refreshable on every change.
- You want the two-variant treatment — an `ideal` render *and* a bordered
  `layout` render — plus tokens and a11y annotations, in one bundle.
- You want artifacts that import into Figma / Stitch / Claude Design, on a
  branch a designer can pull from.

For *one-off* sets of renders → **compose-preview-design-board**. For *rendering*
or *reviewing* → **compose-preview** / **compose-preview-review**.

## Prerequisite: a catalog module

`@Preview` discovery is local-module only, so the components must be authored as
`@Preview` functions in a Gradle module that depends on the target library
(`androidx.compose.material3`, `androidx.wear.compose.material3`,
`androidx.xr.glimmer:glimmer`, `androidx.glance`, …). Author **one `@Preview`
per component × primary mode**, padded, with the breakpoints the system's kit
documents (e.g. `compact` / `medium` / `expanded` for M3; small/large round for
Wear). See the `samples/design-catalog-*` modules in
[yschimke/compose-ai-tools](https://github.com/yschimke/compose-ai-tools).

### Cataloguing an existing app (the cheap path)

A dedicated catalog module is right when you are documenting a *component
library*. When you are cataloguing an **app that already has `@Preview`
functions**, don't author a parallel set — point the spec at the previews that
are already there. Adoption is then a plugin line plus `catalog.spec.json`,
with **no change to UI code**, and the sheet stays honest because it renders the
same previews the team already maintains.

The [compose-samples catalogs](https://github.com/yschimke/compose-samples/tree/agent/preview-catalogs)
are built this way: JetNews covers 22 of its 23 existing previews without
touching a single composable.

Two rules make this work well:

- **Group by feature, not by widget.** The value of a sample/app catalog is
  showing what the app is *for* — adaptive postures, the states of a screen, an
  RTL mirror — so `groups`/`section` should follow that, not alphabetical
  component names.
- **Put catalog-only fixtures in `src/debug`.** Anything you *do* need to add
  (motion fixtures, a composed feature shot) belongs in the debug source set:
  the plugin renders the `debug` variant, so they are discovered like any other
  preview, but they never reach a release build.

## Declare & validate the spec (`catalog.spec.json`)

The catalog's inventory, grouping, captions, sections and per-component variants
are declared in a hand-authored **`catalog.spec.json`** committed next to the
module. Each component's `preview` **must equal an exact `@Preview` function
name** — a mistyped or renamed name renders nothing and surfaces only as a late
"missing" entry at the *end* of the (long) render. Its shape is documented by
[`scripts/design-artifacts/catalog.spec.schema.json`](https://github.com/yschimke/compose-ai-tools/blob/main/scripts/design-artifacts/catalog.spec.schema.json)
(reference it via `$schema` for editor validation).

Two build-free helpers in compose-ai-tools' `scripts/design-artifacts/` scan the
module's Kotlin source directly — no Gradle build, no render — so you author and
check the spec before spending a render:

```sh
# Scaffold a starter spec from the module's @Preview functions (one flat
# "Components" group; caption and regroup from there):
node scripts/design-artifacts/init-catalog-spec.mjs \
  --module :app --system my-system --title "My System" --out catalog.spec.json

# Resolve every `preview` (component + variant) against the discovered functions,
# with typo suggestions, structural checks, and coverage gaps. Exits non-zero on
# errors, so it runs as the pre-flight in design-artifacts.yml before the render:
node scripts/design-artifacts/validate-catalog-spec.mjs --spec catalog.spec.json
```

### Breakpoints: one card, or a card per size

A multipreview (`@WearPreviewDevices`, a local `@CatalogWearBreakpoints`) renders
one function at several device sizes, and the join keys on **function name** — so
by default they all fold into one entry carrying one image per size, tagged from
the spec's `breakpoints` table:

```jsonc
"breakpoints": [
  { "size": "smallRound", "device": "id:wearos_small_round", "widthDp": 192 },
  { "size": "largeRound", "device": "id:wearos_large_round", "widthDp": 227 }
]
```

Declare each by `device` (matched first) *and* `widthDp` (the fallback): a width
is a fingerprint two devices can share, and an undeclared device falls back to
the generic Material width class, making two renders indistinguishable on that
axis — the export warns when it sees one. A Wear catalog that declares no
`breakpoints` inherits the standard round table.

### Reproducing a kit: what splits, and what folds

A design-led catalog — one whose job is to reproduce a *published kit* rather than to publish its
own system — needs one taxonomy decision made consistently, and the tempting rule is the wrong one.

The kit models variation as **variant properties on a component set**: one `Button` set with
`Style = Filled | Tonal | Outline | Child`, one selection set with `Type = Checkbox | Radio |
Switch`. "One kit set is one catalog component" is the right default, and it is what keeps a sheet
browsable — 35 shapes are cells of one card, not 35 cards.

But it cannot be the whole rule, because some of those axes are **separate functions in code**:

> An axis **splits** into a component per value when its values are separate composables; it
> **folds** into cells when they are arguments to one.

`Style=` on the Wear kit's `Button` set splits — `Button`, `FilledTonalButton`, `OutlinedButton`,
`ChildButton` — because which one you call is the choice a reader of the catalog is making. The same
`Style=` on its `Button-Compact` set *folds*, because Compose ships one `CompactButton` that takes
emphasis as `colors`: there is no second function to choose, so there is nothing to split. The test
is the call site, not the word — "emphasis always splits" gets the second case wrong.

Two consequences worth planning for: components that split share their set's node (that is fine and
expected — each cell can still name the kit's own value via `kitAxis`/`kitValue`), and ids follow the
**code's** names while membership follows the **kit's** (`Button/Outlined` for the kit's
`Style=Outline`, because `OutlinedButton` is what a reader greps for).

### Determinism is a capture-state problem, not just a clock problem

Everyone remembers to pin the clock. The subtler cases are components whose *resting* state is not
what the kit draws, or is not stable:

- **A component at rest may be the wrong picture.** A swipe-to-reveal at rest is indistinguishable
  from the card underneath it, and every kit cell draws what the gesture uncovers — so seed its
  state (`rememberRevealState(RightRevealing)`) and publish the revealed component. What you pin is
  where the capture *starts*; the gesture still works in a live session.
- **Anything animated must be pinned, not merely started.** An indeterminate progress indicator, a
  placeholder shimmer, a loading spinner: a capture is one frame of it, and the frame differs on
  every publish. Give progress a fixed value, and keep a shimmer to the live lane.
- **Anything derived from "now" must be a literal.** A date picker opened on today, a time picker on
  now, a relative timestamp — each makes the delivery branch's history noise rather than change.

The cost of getting this wrong is invisible in review and expensive later: the render is green, the
diff is real, and nobody can tell a change from a re-render.

### Look at the renders, and then make a machine look at them

A catalog can be green end to end and still publish **blank cards**. The build compiles, discovery
finds the preview, the render succeeds, the bundle publishes — and the sticker is an empty frame.
Nothing in that pipeline asserts a component *drew* anything.

Review does not catch it either, and on a dark-first catalog it is nearly invisible: a sticker that
drew nothing looks exactly like a sticker that drew something dark.

Three real causes, all of which shipped green before being caught by eye:

- **A missing `Modifier.align`.** On Wear's page and scroll indicators, alignment is what makes the
  component lay out at all — without it they collapse to nothing, not to a mispositioned rail.
- **An unsettled animation.** A component whose content animates in from a `LaunchedEffect` captures
  as its first frame, and if that frame is `alpha = 0` the sticker is empty.
- **A component captured in the wrong state** — an at-rest swipe-to-reveal, a collapsed
  scroll-revealed button.

So render locally before publishing. An Android/Robolectric module needs no CLI at all —
`./gradlew :<module>:composePreviewRender` writes the PNGs — and the render is cheap enough
(seconds, for a couple of hundred previews) to be part of the edit loop rather than a CI-only step.

Then make it a test, because eyes do not scale to a growing sheet:

```kotlin
// composePreview { renderBeforeUnitTests.set(true) } puts the real renderer output where a unit
// test can read it — the same PNGs CI publishes, not a fixture.
@Test fun `no sticker publishes an empty frame`() { /* fail any capture with ~no visible pixels */ }
```

Assert that a sticker drew **something**, not that it drew the right thing: a real lower bound is a
per-component judgement that needs re-tuning on every legitimate change, while "not blank" never
does. And verify the guard fails — revert the fix, watch it name the offender, put the fix back. A
render test that has never failed is a render test that might be reading the wrong directory.

### Record what you did NOT reproduce

A design-led catalog is a claim about coverage, so the claim needs to be checkable. Commit a row per
published kit set carrying either the components that reproduce it or a **stated reason** it is
absent, and test it in both directions:

- a set with neither is an unstated gap;
- an **exclusion whose node something now references** is a decision nobody is making any more.

The second direction is the one that pays off over time. A kit routinely publishes things the code
cannot draw — a size the library has no counterpart for, a style whose painter overload only exists
on some components, an asset that is app content rather than a component — and each of those is a
fact worth writing down once. Without the reverse check, the note survives the limitation and starts
lying.

The same test catches the cheaper mistake: a variant cell **seeded but not implemented**. It renders
green, and it publishes the default picture under another cell's name.

### Dark-first systems

Wear draws its components on a black watch face, so a Wear catalog's component
sticker is a **single dark capture** on a transparent background — `modes:
["dark"]` in the spec, `display.surface: "dark"` so the server's front door
stages the hero on dark rather than washing a light-on-transparent sticker out,
and a local `@Preview(showBackground = false)` multipreview rather than a
light/dark pair.

That single mode is load-bearing beyond presentation: a dark-only catalog cannot
project a `design-map.json` today, because the projector pairs a component's
reference with its `_Light` capture. Wire parity accordingly — see
[design-parity-review](../design-parity-review/references/ci.md#a-dark-only-catalog-projects-zero-components).

When you want a **card per breakpoint** — its own id and caption — use `select`
rather than splitting the `@Preview` in the module (splitting costs the
multipreview's other axes, e.g. `@WearPreviewFontScales`):

```jsonc
{ "componentId": "Home/SmallRound", "preview": "HomeListViewPreview",
  "select": { "size": "smallRound" }, "caption": "Home — small round." },
{ "componentId": "Home/LargeRound", "preview": "HomeListViewPreview",
  "select": { "size": "largeRound" }, "caption": "Home — large round." }
```

Two entries may share one `preview` as long as each selects a different value. An
**annotation-led** inventory says the same thing in code with
`@CatalogComponent(id = "Layout/List", perBreakpoint = true)`, which yields
`Layout/List/smallRound`, `Layout/List/largeRound`, … — one per breakpoint the
function actually rendered at, in `breakpoints` order. It's a flag, not a list:
the multipreview below it already decides the devices, so the names come from the
renders. One breakpoint keeps the plain id; none resolved keeps the component
whole and warns. Adopting `perBreakpoint` on a published catalog **moves those
sticker URLs**, which is why it's opt-in — the preview server already
disambiguates merely *colliding* card labels on its own. A spec entry always
overrides the annotation. Full rules:
[`docs/design/DESIGN_CATALOGS.md`](https://github.com/yschimke/compose-ai-tools/blob/main/docs/design/DESIGN_CATALOGS.md).

Discovery recognises `@Preview` and any `annotation class` meta-annotated with it
(`@CatalogModes`, `@CatalogTemplate`, …); pass `--preview-annotation <Name>` for a
multipreview annotation imported from another module. The authoritative check
stays the render + completeness gate — this is the fast local/CI pre-flight.

> **Wear catalogs always need this flag.** Wear previews are conventionally
> annotated `@WearPreviewDevices` / `@WearPreviewFontScales` / `@WearPreviewLargeRound`,
> which live in `androidx.wear.compose.ui.tooling.preview` — an external
> artifact the source scan cannot see. Without the flags the validator reports
> **`discovered 0 @Preview function(s)`** and fails every entry, which reads
> like a broken spec rather than a missing flag:
>
> ```sh
> node validate-catalog-spec.mjs --spec catalog.wear.spec.json \
>   --preview-annotation WearPreviewDevices \
>   --preview-annotation WearPreviewFontScales
> ```
>
> Record the required flags in the spec's `$comment` so the next run doesn't
> rediscover this. The same applies to any app-defined multipreview annotation
> declared in a different module from the previews that use it.

## Workflow

1. **Render the system with its data products.** Ask the renderer for the
   captures plus the wireframe, theme, semantics, and a11y products:

   ```sh
   compose-preview show --module samples:design-catalog-m3 \
     --with-extension a11y,theme,semantics,semantics-wireframe --json \
     > /tmp/m3-show.json
   ```

   - `capture` PNGs → the `ideal` variant.
   - `compose/semantics-wireframe` (PNG/SVG) → the `layout` variant (bordered).
   - `compose/theme` → the token set (`colorScheme` + `typography` + `shapes`).
   - `compose/semantics` v6 → per-node bounds, padding, `textOverflow`
     (`maxLines` / `lineCount` / `truncated`).
   - `a11y/atf` + `a11y/touchTargets` → the greenline findings.

2. **Build and write the catalog.** The maintained path is the
   `generate-design-catalog.mjs` driver: it renders to a portable bundle with
   `compose-preview bundle pack --with-semantics`, joins it to `catalog.spec.json`
   (matching each component's `preview` to the rendered function name), and writes
   the importable bundle. This is exactly what `design-artifacts.yml` runs:

   ```sh
   compose-preview bundle pack --module samples:design-catalog-m3 --with-semantics \
     -o build/m3-bundle.png
   node scripts/design-artifacts/generate-design-catalog.mjs \
     --spec catalog.spec.json --renders build/m3-bundle.png --out out/ \
     --renderer "$(compose-preview --version | head -1)"
   ```

   **Scoping a pack — use the file flags for any generated list.** `bundle pack`
   takes `--id` (pack only these previews) and `--exclude-preview-id` (skip
   these), and both are **comma-separated**. A preview id may itself contain a
   comma: `@Preview(widthDp = 227, heightDp = 200)` mints
   `…AppCardRemote_width=227dp,height=200dp,dpi=320`. Passing such an id through
   the comma-separated flags splits it into three, which fails in two different
   ways depending on which flag you used:

   - `--exclude-preview-id` — a plain pattern matches by **substring**, so the
     orphaned fragment `dpi=320` matches every preview in the module and the
     render dies with *"excluded every one of the N preview(s) — nothing would
     render"*.
   - `--id` — the render survives (same substring matching), but
     `composePreviewBundle` matches ids **exactly** and fails with *"preview id
     not found: …AppCardRemote_width=227dp"*, naming the first fragment.

   So for anything generated — a shard list, a deferred palette, a design-map
   projection — pass a **file**, one id per line, which has no delimiter to
   collide with:

   ```sh
   compose-preview bundle pack --module :remote-catalog --with-semantics \
     --id-file slice-previews.txt \
     --exclude-preview-id-file slice-excluded.txt \
     -o build/candidates.bundle.png
   ```

   Both refuse an unreadable **or empty** file rather than falling back to an
   empty selection — an empty list means *everything* to both flags, so the
   quiet failure would be packing or rendering the whole catalog while reporting
   success. If you have nothing to select, omit the flag.

   Requires compose-preview **1.41.0+** (`--id-file`); `--exclude-preview-id-file`
   landed in 1.40.0. Hand-written lists of comma-free ids are fine on the inline
   flags.

   Under the hood the driver feeds the render's data products through
   `@design-parity/candidate`'s mappers (`nativeFindings`,
   `semanticsToSemanticTree`, `composeThemeToTokens`) into
   `@design-parity/catalog-export`. To build a catalog **without** a spec file
   (e.g. a custom pipeline), call that library directly:

   ```ts
   import { buildCatalog, writeCatalog } from "@design-parity/catalog-export";

   const catalog = buildCatalog(
     { system: "compose-m3", title: "Compose Material 3",
       library: ["androidx.compose.material3:material3"],
       renderer: "compose-preview 0.17.2" },
     sources, // one ComponentSource per component (ideal+layout images, tokens,
              // semantics, findings) from the mappers above
   );

   await writeCatalog(catalog, ".design-artifacts/compose-m3", {
     sourceRoot: "build/compose-previews",
   });
   ```

   Pass `--source-repo <owner>/<repo>` whenever you generate from a **consumer**
   repo. It defaults to `yschimke/compose-ai-tools`, so a bundle built anywhere
   else silently bakes README/asset links pointing at the wrong repository —
   they resolve, they're just wrong, which is worse than a 404.

   > **`@PreviewParameter` previews cannot carry a catalog component.** A
   > preview whose composable takes a `@PreviewParameter` argument renders its
   > PNG fine, but the renderer emits **no data products for it at all** — no
   > `compose/semantics`, layout, fonts or `figma-svg`. The completeness gate
   > then refuses to publish the whole catalog with
   > `no semantics for: <componentId>` … `incomplete render — refusing to
   > publish`. The message names the component, not the cause, so it reads as a
   > bad spec.
   >
   > Don't reach for `--allow-incomplete` (it publishes a sheet with holes) and
   > don't drop the component. Add a **zero-argument wrapper preview** in
   > `src/debug` that calls the same composable with a literal fixture, and
   > point the spec at the wrapper — the full data-product set comes back.
   >
   > Check for this *before* spending a render: any `@PreviewParameter` in the
   > previews a spec references will hit it. It bites Wear catalogs hardest —
   > 9 of Jetcaster's 12 Wear previews take a `@PreviewParameter`.

3. **Import.** The bundle is tool-neutral first, Figma second:

   ```
   catalog.json            # index: components, both variants, greenlines, optional screen graph
   tokens.dtcg.json        # W3C DTCG token set — Figma Variables / Tokens Studio / Style Dictionary / Claude Design
   figma-variables.json    # Figma variable-collection projection (light/dark as modes)
   images/<component>/<variant>__<state>[__theme][__size].png
   wireframes/<component>.svg  # baked structural vector — placed as a true vector node on Figma import
   ```

   - **Claude Design / Stitch** — import the PNGs + `catalog.json`; the DTCG file
     seeds tokens. Pair with **compose-preview-design-board** to wrap the same
     renders as a browsable HTML brief.
   - **Figma** — import `tokens.dtcg.json` via a DTCG/Tokens-Studio plugin, or
     create variables from `figma-variables.json`; place the variant PNGs as the
     sticker-sheet frames. The maintained path for this is the
     **figma-catalog-import** skill (the import-hop sibling of this one): it
     drives the `@design-parity/figma-plugin`, decides the import case
     (code-led vs design-led × new vs existing file), and reconciles in place
     instead of delete-and-rebuild. Declaring a screen graph in the catalog
     spec (`screens: [{ id, title?, related }]`) turns a code-led import into
     structured per-screen diff pages rather than one flat sheet.

4. **Deliver on a per-system branch.** Publish the generated `out/` to a
   `design-artifacts/<system>` branch (`design-artifacts/compose-m3`,
   `.../wear-m3`, `.../glimmer`, `.../glance-wear`) — the surface a designer pulls
   from, and what the public preview server (`preview.coo.ee`) fetches and serves
   at `/<system>/`.

   Each publish **appends a commit on the branch tip** rather than force-pushing
   a fresh orphan, so the delivery branch carries a per-regeneration history and
   a sticker that moved can be traced back to the source commit that moved it
   (each commit subject names the render date and the short `main` SHA):

   ```bash
   git log --oneline origin/design-artifacts/compose-m3 -- images/button-filled/
   ```

   Regeneration is automatic on the triggers that matter: `design-artifacts.yml`
   runs on a **push to `main`** that touches a catalog or the export driver
   (scoped to just the affected systems, so a one-catalog merge regenerates one
   branch), at the tail of a **published release**, and on a **weekly cron**.
   **Renderer / plugin / CLI changes are deliberately excluded from the push
   trigger** — they do change the output, but they're touched by most merges and
   each system's render is expensive. That drift is picked up by the cron and the
   release chain; dispatch manually if it needs to land sooner.

   Don't hand-roll the pipeline in a consumer repo — call the reusable workflow,
   which is the same one compose-ai-tools' own catalogs use:

   ```yaml
   publish:
     if: ${{ github.repository == 'you/your-repo' }}
     permissions:
       contents: write
     uses: yschimke/compose-ai-tools/.github/workflows/design-artifacts-reusable.yml@main
     with:
       system: your-system
       spec: catalog.spec.json
       module: ':app'
       # Without these three the branch carries PNGs only — see below.
       publish-live-bundle: true
       split-per-preview: true
       split-mode: full
   ```

   > **Publishing a SECOND catalog from the same repo? Add `design-map-command`
   > to both jobs.** `system`, `spec` and `module` vary per call, but
   > `design-map.json` is read from the repo root either way — so the sheet that
   > does not own the committed map is scored against the other one's. Every
   > mapping dangles and the board publishes `coverage.percent: 0`, which looks
   > exactly like a catalog nobody has annotated yet. The input (compose-ai-tools
   > **v1.54.0** and later) lets each job project its own map first:
   >
   > ```yaml
   >       design-map-command: >
   >         ./gradlew :second-module:composePreviewDiscover --stacktrace &&
   >         scripts/design-map.sh second-module
   > ```
   >
   > Set it on the parity workflow too, and make sure that projection passes
   > `--prefix <module-dir>` — it defaults to `catalog`, so a second module's
   > code handles otherwise name files that do not exist. Failure modes:
   > [design-parity-review →
   > CI](../design-parity-review/references/ci.md#two-catalogs-in-one-repo).

   > **A published catalog is static unless you opt into a live bundle.**
   > `publish-live-bundle` and `split-per-preview` both default to **`false`**.
   > Leave them off and the branch gets rendered PNGs and nothing executable, so
   > the server has no daemon to launch: it badges the catalog *"serves baked PNG
   > snapshots only — its delivery branch publishes no live bundle"* and the
   > device, theme and knob controls in the viewer do nothing. Nothing fails and
   > no warning is printed — the publish succeeds and quietly ships a static
   > sheet. Turning them on carries the executable bundle under `bundle/`,
   > records `liveBundle` in `catalog.json`, and splits it into one
   > re-renderable bundle per preview.
   >
   > **Liveness and trust are independent gates**, and the status page shows them
   > in adjacent columns, which invites conflating them. Trust is about whether
   > the server will execute *your* branch (`trust/producers.json`); the live
   > bundle is about whether the branch contains anything to execute. A catalog
   > can read `✓ trusted` and still serve baked PNGs — that's a missing bundle,
   > not a trust problem, and no amount of trust config fixes it.
   >
   > `split-mode: full` (each per-preview bundle keeps its own re-render
   > classpath) requires `publish-live-bundle`; `view-only` is the baked tier for
   > a render the serve host can't drive. Android/Robolectric and CMP desktop
   > catalogs are both live-capable — what matters is that the host bakes the
   > matching daemon, not the platform.
   >
   > Costs to weigh: a full split writes a per-preview bundle for every preview,
   > so the delivery branch and the render both grow with catalog size.

   > **Publish before you register.** A serve host that fetches
   > `design-artifacts/<system>` reconciles its catalog list by *fetching each
   > branch*, so registering a system whose branch does not exist yet fails the
   > reconcile — on preview.coo.ee that is `HTTP 502 — catalog <system> not
   > published: could not fetch …` and one rejected seed entry fails the whole
   > run, even though every other catalog was accepted. Land the first
   > design-artifacts publish, confirm the branch exists, then open the
   > registration change. A registration that raced the publish is fixed by
   > re-running the config job once the branch is there — nothing to revert.

   > **`embed-deps` when a dep isn't on Central or Google Maven.** The serve box
   > rebuilds the live classpath from the Maven coordinates in the bundle, and it
   > resolves from exactly two repos — `repo1.maven.org/maven2` and
   > `dl.google.com/dl/android/maven2`. A dep from anywhere else (JitPack, a
   > company repo, `androidx.dev` snapshots) can't be fetched, the daemon fails to
   > build its classpath, and the catalog **silently** falls back to baked PNGs
   > with `livebundle-unavailable`. `embed-deps: true` carries the reachable jars
   > inside the bundle under `libs/` instead; it costs bundle size, so leave it
   > off when every dep is on the two public repos.
   >
   > Check before you publish rather than reading it off the status page
   > afterwards: list the coordinates the module actually resolves and probe both
   > repos for each `<artifact>-<version>.pom`. Watch for repos that are declared
   > but inert — a `pluginManagement` repo serves build-time compiler plugins and
   > never contributes a runtime classpath entry, and an env-gated repo (the
   > `COMPOSE_SNAPSHOT_ID` branch in the compose-samples `settings.gradle.kts`)
   > contributes nothing when CI doesn't set the variable. Both look alarming in a
   > grep and neither affects the live bundle.

## Source

Maintained at [github.com/yschimke/skills](https://github.com/yschimke/skills)
under `skills/compose-design-catalog/`. The export library
(`@design-parity/catalog-export`) lives in
[yschimke/design-parity](https://github.com/yschimke/design-parity); the
renderer/CLI in
[yschimke/compose-ai-tools](https://github.com/yschimke/compose-ai-tools).