agentsclimarketplace

Compose design catalog

Skill yschimke/skills/skills/compose-design-catalog

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.From its SKILL.md

Install
npx -y skills add yschimke/skills --skill compose-design-catalog

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

26.7 KB, as published. Nobody here has run it

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:

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.

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 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 (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:

# 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:

"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:

// 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.

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):

{ "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.

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:

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:

    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:

    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)"
    

    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:

    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):

    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:

    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
    

    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 under skills/compose-design-catalog/. The export library (@design-parity/catalog-export) lives in yschimke/design-parity; the renderer/CLI in yschimke/compose-ai-tools.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most design frontend skills give

Counted across 1,179 of the 2,086 authors here whose files we hold, read 2026-09-06

  • Commit to a bold aesthetic directionin 31 of 1179, across 24 files
  • Prefer component composition over inheritancein 28 of 1179, across 14 files
  • Animate only transform and opacity propertiesin 27 of 1179, across 22 files
  • Memoize expensive computations with useMemoin 26 of 1179, across 13 files
  • Use semantic HTML elementsin 24 of 1179, across 23 files
  • Virtualize long lists for performancein 21 of 1179, across 10 files
  • Use CSS variables for design tokensin 20 of 1179, across 14 files
  • Implement loading, empty, and error statesin 20 of 1179
  • Lazy load heavy components with Suspensein 19 of 1179, across 8 files
  • Respect prefers-reduced-motion media queriesin 18 of 1179, across 10 files
  • Prioritize CSS-only animations for HTMLin 18 of 1179, across 16 files
  • Use compound components for related UI elementsin 18 of 1179, across 7 files

Said here and by no other author read

  • Install compose-preview using the bootstrap installer
  • Author one preview per component and primary mode
  • Group catalog entries by feature
  • Place catalog-only fixtures in the debug source set
  • Commit a catalog.spec.json file next to the module
  • Validate the spec using the provided node scripts

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.

Keep looking

Skills are one crate of 626,045. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.