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

@design-parity/kit-index

v1.2.0

Published

The kit vocabulary a design map resolves variants against: walk a design kit's pages, project the referenced component sets into a committed kit index, and resolve a catalog variant knob to the exact kit node (variant axis, component property, or configur

Readme

@design-parity/kit-index

The design kit's own vocabulary, committed — so a catalog that renders one component at six variants can say which kit node each of the six should be compared against, without anyone maintaining that list by hand.

The problem

A design-map.json entry binds a code component to one design node:

{ "code": "ui/Button.kt#FilledButton", "source": "figma", "ref": "figma:AbCdEf/57994:2324" }

That is exactly right for a parity diff, and not enough for a component that renders more than one variant. A catalog picturing Button at three sizes and two shapes has six renders and one ref, and nothing says which kit node the other five belong to. Enumerating them by hand is the mapping-config sprawl a design map exists to avoid — and it drifts silently the moment the kit gains a variant, because a missing entry reads as "no counterpart in the kit" rather than "nobody looked".

So this package derives them, from what the kit itself publishes.

The pipeline

  design kit
      │  dump   ── walk every page, deep
      ▼
  figma-inventory.json      disposable, megabytes
      │  build  ── keep only what design-map.json references
      │            + fetch the component properties the walk cannot see
      ▼
  figma-kit-index.json      COMMITTED, tens of kilobytes
      │  resolve ── project a code knob onto it
      ▼
  { nodeId, name }

Only the two generation steps touch the network, and both are deliberate "refresh the vocabulary" operations that land in a commit a reviewer can see. Resolution runs against the committed index, never the live kit, so a parity run reports the same thing for everyone and needs no design-tool credentials.

Where the code side comes from

For a Compose catalog, the map and its variant declarations are derived from annotations rather than authored. That half lives upstream in compose-ai-tools, because every field it reads (@CatalogComponent(reference = …), @CatalogVariant, @OverrideVariant) is defined there — and it stops at the point where a design kit's vocabulary would be needed:

  previews.json
      │  emit-design-map.mjs             compose-ai-tools: knows the ANNOTATIONS
      ├──▶ design-map.json               base refs, one per component
      └──▶ design-map-variants.json      "same component, these knobs turned" —
           (compose-preview-design-map-      unresolved, because `size=l` is a
            variants/v1)                     Compose fact and `Size=Large` is a
      │                                      kit fact
      │  design-parity-kit-index resolve  THIS PACKAGE: knows the KIT
      ▼
  design-map.json with a tagged ref/previewId pair per variant

resolve reads only committed files, so unlike dump/build it needs no credential and is safe on every build:

$ design-parity-kit-index resolve
Wrote design-map.json: 1 variant reference(s) across 1 component(s).

1 variant(s) are a component PROPERTY in the kit, not a variant beside it. A
definition node renders at the defaults, and no exact configured instance was
indexed for these values, so they remain unpaired:
  - Button/Filled / true (icon=true) — Button: `Icon` (INSTANCE_SWAP, default "54616:25409")

1 reference(s) draw optional content by default. Every render made from them
includes it, so a sticker that leaves it out is compared against something it
never claimed:
  - Button/Filled — Button: `Show icon`

Three kinds of miss are reported apart because they have different owners: a variant the kit models neither way is a real gap; a property-shaped one means the kit has the thing and a node reference just cannot ask for it; and defaulted content means the reference resolved but draws more than the code did. Rolling them together is what makes a retired pattern read as neglect.

--check turns it into a drift gate instead of a writer. A collision — two previews resolving to one node — is refused outright (exit 2, nothing written): the same node cannot be both previews' counterpart, and a map that said so would have the diff report one of the two renders as wrong.

Three kinds of variation — and only two are addressable

| | What it is | Addressable? | | --- | --- | --- | | Variant axis (Size=Large) | A sibling node with its own id | ✅ directly | | Component property (Show icon) | A switch on the node | ⚠️ only via an instance | | Slot / instance swap | A region, or which sub-component nests | ❌ never |

The middle row is the one that quietly poisons parity results. GET /v1/images renders a node at its property defaults, and a reference is a node id with nowhere to hang an override. So a kit whose Button defaults Show icon to true renders an icon+label reference for a variant whose name says only Type=Round, Size=Small — and label-only code diffed against it reports a missing icon as though the code were wrong.

The way out is not to mutate the kit. It is that somebody already placed an instance at the wanted vector, on an examples page, and that instance's node id is a renderable handle for a point in property space no definition can express. resolvePropertyInstance finds it, matching the whole property vector so a near-miss never passes as an exact one.

The third row stays unpaired on purpose. leading=icon says what the content means, never which node supplies it; guessing would produce a confident reference to the wrong thing.

The governing rule

A wrong translation must find nothing, rather than produce a confident bad reference.

Under a design-led direction a bad reference drives the code away from the kit it is copying — and does so while reporting a clean parity result, which is the worst outcome available. Every unresolved seed is surfaced as unresolved.

Surfaced with a reason, though, because "no counterpart in the kit" is true of every miss and actionable for almost none. resolve classifies each one (explainUnresolved):

| Reason | What it means | What to do | | --- | --- | --- | | … declares a kit axis/value the set does not publish | A kitAxis / kitValue names something this set does not have | Fix the declaration — the reason lists what the set publishes | | the reference already draws this | The base variant carries every seeded value — the render duplicates the reference | Nothing. Not a gap. | | each of … exists, but no node carries them together | Both values are real; the kit's matrix skips their intersection | Nothing here — the kit would have to draw the cell | | no counterpart for … | These seeds have no counterpart at all | The actual lead: map the value, or accept the gap |

The third column is the point. On a real catalog this turned error-unselected (state=unselected, status=error) — where state=unselected resolves perfectly well on its own — into no counterpart for status=error, and stopped two size=small renders being read as missing kit nodes when the reference is the small variant.

When the table cannot reach it: naming the kit's own spelling

Some values no translation reaches. The Material 3 kit spells one date-picker variant Type=Full-screen (range); a catalog seeding type=range resolves to nothing, and — before the reason table above — did so silently, dropping the node from the comparison. The workaround was worse than the problem: seed type=full-screen (range) in Kotlin, and a kit spelling, parentheses and all, becomes load-bearing in catalog source that will rot the next time the kit renames a value.

A seed may therefore carry the kit's own names beside its own:

resolver.resolveVariant("figma:AbCdEf/53977:33595", {
  key: "action",
  raw: "longer",              // what the Compose knob is called
  kitAxis: "Configuration",   // what the kit calls the axis
  kitValue: "Text & longer action", // …and the value
});
// → { nodeId: "53977:33576", name: "Configuration=Text & longer action, …" }

Three properties worth knowing:

  • Authoritative, not a hint. A declaration replaces the alias tables for that seed. size=l resolves fine on its own; size=l declaring kitAxis: "Sise" resolves to nothing. Falling back would make a typo indistinguishable from a correct declaration.
  • Still checked against the kit. A declared axis or value must be one the set really publishes, so the governing rule holds: nobody can declare their way to a node the kit does not draw. What they get instead is the declared reason above, naming the spelling that missed and what the set does publish.
  • Matched without punctuation or case, in any script. of lines finds the kit's # of lines; declaring an axis should not be a typing exercise. Only separators and punctuation are ignored — a kit filing its axes as サイズ and 状態 keeps them distinct, where the slug normalisation used elsewhere would erase both to nothing and match whichever was indexed first.

The same exactness applies wherever a declaration lands. A declared name reaches a component property as readily as an axis — which of the two a kit uses is its own business — but it must be the property's real name, so kitAxis: "focus" no longer passes for Show focus indicator the way the knob key focus does. On a family the kit models as folder siblings rather than a set, a declared value names the sibling outright (Middle-inset, not the Inset the near-miss search would find), and a declared axis is refused: there are no axes there to name. And a declaration on an axis another seed already claimed is checked through the same fusion rule as any other — Type=Selected plus a declared Error unselected resolves to nothing rather than silently discarding the first seed.

Either field can be given alone: an axis alone when only the name differs, a value alone when only the spelling does.

One case a declaration deliberately cannot reach: two seeds fusing onto one value in a script that writes no spaces. The fusion rule asks whether a published value says exactly what the two seeds say together, which it answers by comparing word sets — and エラー選択済み is a single token however many words a reader sees in it. Containment is the obvious substitute and is not safe: 選択 (selected) is a substring of 未選択 (unselected), so a check built on it accepts the node drawing the opposite state. Telling lexical containment from semantic fusion needs a segmenter this package has no business carrying, so such a vector stays unresolved and is reported as unresolved. Space-separated scripts fuse normally, whatever the alphabet.

That rule is why the matching is fussier than it first looks. A boolean axis accepts True from any knob, so a naive matcher had footer=true resolving to Show back=True and supporting=on resolving to Leading icon=True — both real nodes, both the wrong component. Candidate axes must therefore share a whole word with the knob or its value, matched word-for-word rather than by substring (Leading icon contains the letters of on).

Usage

import { KitIndexResolver, loadKitIndex } from "@design-parity/kit-index";

const resolver = new KitIndexResolver(await loadKitIndex("figma-kit-index.json"));

// A rendered cross-product cell — every seed must map to a distinct axis, and
// the exact resulting vector must name a real sibling.
resolver.resolveVariant("figma:AbCdEf/57994:2324", [
  { key: "size", raw: "l" },
  { key: "shape", raw: "square" },
]);
// → { nodeId: "57994:2310", name: "Type=Square, Size=Large, State=Enabled" }

// A property-shaped variant, paired with an instance configured that way.
resolver.resolveVariant("figma:AbCdEf/57994:2324", { key: "icon", raw: "false" });
// → { nodeId: "…", name: "State=Enabled (configured instance)" }  — or undefined

// What a reference draws whether or not the code asked for it.
resolver.defaultedContent("figma:AbCdEf/57994:2324");
// → [{ name: "Show icon", setName: "Button" }]

The resolver takes the index object, not a path: it is a committed artifact the caller has already read — possibly from a bundle, a cache, or a test fixture — and a resolver that read a fixed filename from the working directory could not be used twice in one process or tested without a real kit.

CLI

$ export FIGMA_TOKEN=figd_…

$ design-parity-kit-index dump  --file AbCdEf --depth 8
31 page(s) in AbCdEf
  [1/31] Buttons (11:1833): 42 component(s), 0 hidden-variant example(s), 7 configured instance(s), deepest 6
  …

$ design-parity-kit-index build --file AbCdEf
Wrote figma-kit-index.json: 55 set(s), 1284 variant(s), 6 hidden variant render
alias(es), 43 standalone component(s), 3 specimen node(s), 38 set(s) carrying
component properties, 61 configured instance render handle(s).

$ design-parity-kit-index validate
figma-kit-index.json is a valid kit index.

$ design-parity-kit-index resolve --check   # CI: fail if the committed map drifted
design-map.json is up to date.

Two steps rather than one because they fail differently: dump is the expensive walk of a whole file and its output is disposable; build is the cheap projection worth re-running while a design map is still changing. build degrades without a token — writing an index with no property vocabulary, and saying so — and validate never touches the network.

Vocabulary

The knob→axis and value→spelling tables are data, not logic, and the defaults are tuned against Material-3-shaped kits. A kit that files its variants differently supplies overrides, which merge per key so renaming one axis does not mean restating the other thirty:

new KitIndexResolver(index, {
  vocabulary: { axes: { density: ["Density", "Compactness"] } },
});

resolve reads the same shape from a kit-vocabulary.json beside the index — picked up automatically when present, or named with --vocabulary:

{
  "axes":   { "density": ["Density", "Compactness"] },
  "values": { "cosy": ["Comfortable"] }
}

That file exists so a catalog can learn one more of its kit's spellings without waiting for a release of this package. Only the tables live there — a rule about how values are matched at all (the multi-word slug match, the fused-axis search) is logic, and belongs here where it can be tested.

Files

figma-kit-index.json is generated — regenerate it, never hand-edit it. It is validated against schema/kit-index.schema.json on load, which matters more for a generated file than a hand-authored one: nobody proof-reads it, and a half-written index fails as a resolver that silently finds nothing.