Skip to content

chore: the foundation says where each part of the preset contract actually lives - #770

Merged
YaelAnaya merged 3 commits into
mainfrom
chore/foundation-micro-closeout
Aug 24, 2026
Merged

chore: the foundation says where each part of the preset contract actually lives#770
YaelAnaya merged 3 commits into
mainfrom
chore/foundation-micro-closeout

Conversation

@YaelAnaya

Copy link
Copy Markdown
Collaborator

The shadcn/Nova foundation is in place and working. What was left over is
representational: three files described the preset contract in ways that were no
longer true, and one machine-readable claim had no gate in front of it. This
reconciles the description with the implementation and changes no runtime value.

The radius question, settled

DESIGN.md records the decoded preset as including radius: medium, the
stylesheet materializes it as --radius: 0.625rem, and components.json has no
radius field. The obvious repair — write the field in — is wrong, and verifying
it inside the repository's Linux container is what this change rests on.

The preset decodes to ten values, none of them a base:

$ shadcn preset decode b3bXyyPdWj --json
{
  "code": "b3bXyyPdWj",
  "version": "b",
  "values": {
    "menuColor": "inverted",
    "menuAccent": "subtle",
    "radius": "medium",
    "font": "inter",
    "iconLibrary": "tabler",
    "theme": "neutral",
    "baseColor": "neutral",
    "style": "nova",
    "chartColor": "orange",
    "fontHeading": "geist"
  },
  "derived": [],
  "url": "https://ui.shadcn.com/create?preset=b3bXyyPdWj"
}

The CLI is 4.18.0, the version DESIGN.md already names as its reference, and
the version pnpm-lock.yaml pins. Its config schema is strict, so the field is
rejected rather than absorbed — parsing the current configuration with radius
added answers:

success: false
errors: [
  {
    "code": "unrecognized_keys",
    "keys": [
      "radius"
    ],
    "path": [],
    "message": "Unrecognized key(s) in object: 'radius'"
  }
]

Adding it would not be harmless documentation; it would break every shadcn
invocation that reads the file. The configuration as committed parses cleanly and
is unchanged here. So the contract is one intent held in three layers, which is
what DESIGN.md now says:

Layer Owns Radius
Preset intent What the code decodes to radius: medium
Runtime The value that paints, in styles.css --radius: 0.625rem
CLI configuration The schema-supported fields, in components.json no field — the schema defines none

base: radix is worth one note for the next reader: it is not a decoded value at
all, but an initialization choice folded into style: "radix-nova".

The icon set, described as it is

Four places still called the primitives "Radix + lucide" — the ui-core package
description, the stack table in AGENTS.md, the frontend setup skill, and the
architecture note. The setup skill's version was the costly one, because it
instructed contributors to reach for the library the foundation had moved off.

DESIGN.md's own anti-pattern was stale in a way that misdirected: it placed the
remaining debt at screen level. No screen imports lucide-react any more. All
eleven remaining imports sit under frontend/ui-core/src/annotator/, and that is
what the rule now names.

The gate

Nothing read components.json, so the repair this change argues against had
nothing standing in its way. The new invariant in
tests/scripts/design_tokens.test.mjs asserts the key set rather than a count,
so a failure names what moved, and pins the preset values the schema does carry.
It was verified by breaking it: adding radius fails it, drifting iconLibrary
off tabler fails it, and restoring the file returns it to green.

Found, not fixed

  • @visionset/app declares lucide-react and has no import of it — three
    mentions in src/demo/ToolStrip.tsx comments are the only occurrences. The
    declaration is removable, but the frontend dependency bump (chore(deps): bump the frontend-minor-patch group across 1 directory with 5 updates #768) edits that
    exact line and the lockfile, so removing it here would conflict for no gain.
    The annotator migration can drop both together.
  • src/demo/ToolStrip.tsx explains its hand-drawn glyphs by saying DESIGN.md
    pins lucide-react. That has not been true since the preset landed. Left
    alone as product code outside this change's scope.

Test plan

Everything ran inside Docker/Linux; no repository validation ran natively on the
host. Two things are worth recording for whoever runs these next.

The app compose service is not a test runner: its image bakes manifests and
tsconfigs but neither eslint.config.js nor vitest.config.ts, so the suites
there fail in the thousands and lint dies on a flat-config error — the same shape
as exec api pytest exiting 5 because tests/ is not mounted. And .gitattributes
forces LF only for *.sh, so on a core.autocrlf=true checkout every Markdown
file is CRLF while the blobs are LF; copying that working tree into a container
fails roughly eighty anchor assertions in files nobody touched, because the
trailing \r corrupts every heading slug. The run below normalizes to LF, which
is what a Linux checkout gives.

A full tree with a real install, in node:24-bookworm:

install (pnpm install --frozen-lockfile)  PASS
build (pnpm -r build)                     PASS
@visionset/ui-core lint (incl. typecheck) PASS
@visionset/ui-core test                   PASS
@visionset/app build                      PASS
@visionset/app lint                       PASS
@visionset/annotator test                 PASS
pnpm test:scripts                         PASS
pnpm version:check                        PASS

The design and documentation gates were re-run against the final content after a
prose rewrap: design_tokens, docs_links and docs_sidebar together report 19
passing, 0 failing.

Playwright was not run. No runtime CSS, token, primitive or product-UI file
changes here, so there is no rendered surface for it to inspect; components.json,
styles.css and tokens.ts are byte-for-byte unchanged.

…ents.json carries only the fields shadcn's schema defines

The foundation's source-of-truth table called components.json "the decoded
preset configuration", which reads as a promise that every decoded property is
represented there. It is not: shadcn's config schema is strict, and a property
it has no field for is rejected rather than ignored, so the file cannot restate
the whole preset even as documentation. The decoded `radius: medium` therefore
has exactly one home, `--radius: 0.625rem` in styles.css, and its absence from
components.json is the design working rather than drift.

DESIGN.md now separates the three layers - preset intent, runtime realization,
schema-supported configuration - with the radius as the worked example, and the
Radius section names the stylesheet as the single place the medium step is
spelled.

The Lucide anti-pattern was also stale in a way that misdirected: it placed the
remaining debt at screen level, where no file imports lucide-react any more.
Every remaining import sits under frontend/ui-core/src/annotator/, which is what
the rule now names. The architecture note picks up the same two corrections, and
its count of the VisionSet extensions catches up with the five roles that exist.
…ibed

Three places still described the primitives as "Radix + lucide": the ui-core
package description, the stack table in the agent guidelines, and the frontend
setup skill's instruction to contributors. Tabler is the icon set, and inside
the primitives and the screens it is already the only one, so the instruction
was pointing new code at the wrong library.

Each now says what is true, and the skill adds where the exception lives, so a
contributor who finds a lucide-react import under src/annotator/ reads it as
debt rather than as the house style.
…defines

Nothing in the repository read components.json, so the one repair the radius
question keeps inviting - writing `radius: medium` into it because the preset
decodes to that - had no gate in front of it. The field would not be ignored:
the schema answers `unrecognized_keys`, which breaks every shadcn invocation
that reads the file.

The gate asserts the key set rather than a count, so a failure names what moved,
and it pins the preset values the schema does carry. It lives beside the other
design invariants, and its failure message points at styles.css, where a decoded
property with no field belongs.
@YaelAnaya
YaelAnaya merged commit 490e622 into main Aug 24, 2026
15 checks passed
@YaelAnaya
YaelAnaya deleted the chore/foundation-micro-closeout branch August 24, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant