npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yschimke/compose-design-map

v2.27.0

Published

Project a compose-preview discovery manifest into design-parity's design-map.json, plus a sidecar of unresolved variant declarations. Dependency-free.

Readme

@yschimke/compose-design-map

design-map.json is design-parity's correspondence file: it says which design node a code component is meant to look like. This package writes one, from the catalog annotations compose-ai-tools defines.

./gradlew :<module>:composePreviewDiscover
npx --yes @yschimke/compose-design-map \
  --previews <module>/build/compose-previews/previews.json

Dependency-free and Node-only, so npx is the whole install. Both outputs are generated — regenerate rather than edit. --check regenerates in memory and exits non-zero if a committed copy has drifted, which is the CI posture.

Pin the version in CI. Both files are committed and checked, so the projection's version is an input to a checked-in artifact: float it, and a release here turns a downstream repo red for a change nobody there made.

Why the producer lives here

Every field the projection reads is defined in this repository:

| Field on previews.json | Declared by | | --- | --- | | catalog.reference, referenceSet, noReference, referenceContentsOnly, kitAxis | @CatalogComponent | | catalog.props, catalog.state, catalog.kitValue | @CatalogVariant | | overrides.seeds, overrides.props | @OverrideVariant / @PreviewAxis | | overrides.kitAxis, overrides.kitValue, overrides.noReference | @OverrideVariant |

Rename one of those and the projection has to change in the same commit. Keeping the two on opposite sides of a repo boundary is how a manifest reader goes quietly stale — and the reference belongs on the annotation rather than in a JSON map for the same reason: a map keyed on preview names drifts the moment a preview is renamed, and fails silently when it does.

The consuming half already lived here too — design-references.mjs reads a design-map.json to build a published catalog's references/index.json. Until now nothing in the ecosystem wrote one except a hand-maintained script in a downstream catalog repo.

Where the split falls

A catalog picturing Button at three sizes and two shapes has six renders and one reference. Pairing the other five means answering "which kit node is size=l?" — and that is not a question this repo can answer:

  previews.json
      │
      │  compose-design-map            ← THIS PACKAGE. Knows what the annotations mean.
      │
      ├──▶ design-map.json             base references, one per component. Valid on its own.
      │
      └──▶ design-map-variants.json    "these previews are the same component with these knobs
                                        turned" — unresolved, because `size=l` is a fact about
                                        the Compose API and `Size=Large` is a fact about somebody's
                                        design kit
                │
                │  @design-parity/kit-index      ← THE OTHER REPO. Knows what the KIT means.
                ▼
           design-map.json with a tagged ref/previewId pair per variant

size=l → Size=Large is a translation against a kit's published vocabulary. That vocabulary is a Figma concern, it needs a Figma credential to derive, and it differs per kit — none of which this repo has any business holding. So the variant renders come out as declarations and a resolver that owns a kit index turns them into node ids.

When the catalog knows the kit's word for it

Some values no translation table reaches: the Material 3 kit files one date-picker variant as Type=Full-screen (range), and type=range finds nothing against it. kitAxis / kitValue are how a variant names both sides — the Compose word in props/strings, the kit's word beside it — and this projection carries them onto the seed so the resolver can prefer them over its own tables:

@CatalogVariant(of = "DatePicker/Modal", props = ["type=range"],
                kitAxis = "Type", kitValue = "Full-screen (range)")
{ "seeds": [{ "key": "type", "raw": "range",
              "kitAxis": "Type", "kitValue": "Full-screen (range)" }] }

Projecting is not translating: nothing here checks a declaration against a kit, because there is no kit here to check against. The one thing it does judge is whether the declaration can be placed — the annotation carries one pair per variant, so a cell seeding two knobs gives no way to say which one the axis names. Those are reported and dropped rather than guessed at, since guessing pins the wrong axis and resolves, confidently, to the wrong node.

The two halves are separable because the first is useful alone: a repo with no kit index still gets a valid map of base references, which is most of the value at none of the cost.

The sidecar

design-map-variants.json carries schema: "compose-preview-design-map-variants/v1"; a resolver must match that string before reading it. One entry per component that has variant renders:

{
  "schema": "compose-preview-design-map-variants/v1",
  "components": [
    {
      "code": "catalog/Catalog.kt#FilledButton",   // the design-map entry these belong to
      "componentId": "Button/Filled",
      "reference": "figma:AbCdEf/1:2",              // the node a resolver walks from
      "basePreviewId": "…FilledButton_Light",
      "renders": [
        { "previewId": "…FilledButton_Light_VARIANT_l", "name": "l",
          "seeds": [{ "key": "size", "raw": "l" }, { "key": "shape", "raw": "round" }] },
        { "previewId": "…FilledButton_Light_VARIANT_indeterminate", "name": "indeterminate",
          "seeds": [{ "key": "progress", "raw": "indeterminate" }],
          "noReference": "The kit publishes determinate progress cells only." }
      ]
    }
  ]
}

It is a separate file rather than another key on the map because the design-map schema sets additionalProperties: false — a map carrying an extra key would fail its own validator. No file is written when nothing declares an axis or a stated cell absence. A render carrying noReference does not enter kit-node resolution; the reason is the result, and remains reportable alongside the cells that do resolve.

Two things worth knowing

One capture per component is mapped, not one per rendered mode — a component maps to a single design node. Where a composable publishes a themed pair, the light capture is the one that pairs, because that is the mode design kits draw their frames in: diffing a dark render against a light reference reports the whole palette as a finding.

Where it publishes exactly one mode, that one pairs, whatever it is. A dark-first catalog — a Wear watch face is a black screen, so its component multipreview is a single dark capture — names no Light capture anywhere, and demanding one used to project the whole catalog to an empty map: a file reading as "nothing here corresponds to the kit" rather than "the projector could not see these", which --strict could not fire on either.

Several modes with no light among them is the one case that stays unmapped. Picking one would be guessing which of Dark and Coral the kit drew, so those components are reported (diagnostics.ambiguousMode, and a --strict failure) rather than paired at random.

A breakpoint fan-out is a size axis, not a mode. A multipreview that draws one composable at several screen sizes — the Wear round breakpoints are the live case — publishes several captures of it, told apart by the same id segment a themed pair uses. Read as modes they are unresolvable (Light is nowhere among wearos_small_round / wearos_large_round), so a full-screen component used to drop out of the map entirely the moment it gained a second size.

They are told apart by a fact the id does not carry: each capture names a device, and the devices have different widths. A palette does not change the frame's width, so captures whose modes map one-to-one onto distinct device widths are a size axis and one of them can be picked on the merits:

  • the narrowest is the base by default, because that is the size a kit draws — a kit publishes its screen artwork at one size and leaves adaptation to the implementation, and the narrowest is the one every larger screen is an adaptation of;
  • --base-breakpoint <dp> moves it, for a kit that draws somewhere else. A named base a given composable does not render falls back to the narrowest rather than dropping it — rendering a subset of the catalog's breakpoints is a legitimate thing for one screen to do;
  • the sizes the base did not take fold under it as cells, seeded breakpoint=<dp> and named <dp>dp, so they are published rather than discarded.

A bare breakpoint=<dp> is a value no kit vocabulary contains, so such a cell resolves against nothing — correctly, for the majority of kits, which draw every screen cell at one size and have no size axis at all. Where a kit does publish screen size as a variant property, the component says so:

@CatalogComponent(id = "Picker", breakpointKit = ["225=Larger Screen (BP)=Yes"])

and the 225dp cell is seeded breakpoint=225 with kitAxis/kitValue attached, pairing with the kit node the picture was always there for. It is a per-component declaration rather than a per-run flag because it is a property of one component's kit set, not of the catalog; a size the component never draws, or a malformed entry, keeps the bare seed and is reported unresolved rather than mispaired. A breakpoint capture is the one kind of cell that cannot carry @OverrideVariant (kitAxis = …) itself — it is not an annotation at all — which is why the mapping lives on the component.

Two captures of the same width are still a mode, whatever devices they name: nothing orders them, so they stay ambiguousMode. An @OverrideVariant cell rides the base breakpoint only — the product of both axes would multiply the sheet by every size, and the base carries the matrix.

overrides.props beats overrides.seeds where both exist. They are not the same list. seeds holds only the values that differ from the composable's defaults; props — emitted for a @PreviewAxis cross product — carries the full axis assignment, defaults included. A cell that knows its own axes pairs by construction, which is exactly what OverrideVariantSpec.props was added for. A cell described only by its non-default seeds is missing the axes it happens to sit at, and a kit that spells its default size explicitly in a combination cell then has nothing to match.