Skip to content

Own the components, and take the palette from the design file - #4

Merged
YaelAnaya merged 21 commits into
mainfrom
restructure-0.2.0
Sep 7, 2026
Merged

YaelAnaya merged 21 commits into
mainfrom
restructure-0.2.0

Conversation

@YaelAnaya

Copy link
Copy Markdown
Contributor

Summary

The package writes its own components from now on. The generator config and
the CLI dependency it needed are gone, the four patch helpers dissolved into
the components they were patching from outside, the gates are compiled with
the rest of the package, and the palette comes from the design file.

Twenty-two commits, each self-contained. The two worth reading on their own
are the last: one is the whole functional change, the other is a mechanical
formatting pass.

What a consumer sees

  • progressAria, menuSurface, twoLineTrigger, inlineLink are gone.
    Each described a limitation rather than a decision, and each is now the
    component's own behaviour: Progress forwards its value, a menu sizes to
    its items and leaves on the frame it is dismissed, SelectTrigger takes
    multiline, Button takes size="inline".
  • Three gate helpers retired — legacyVocabularyIn,
    statusTokenUtilitiesIn, retiredDeclarationsIn — plus
    menuSurfaceGapsIn, which guarded a helper that no longer exists. The
    published surface is eight helpers.
  • Separator is newly exported. It existed, was used internally, and
    reached no consumer.
  • The palette moved: foreground and primary to oklch(0.2 0 0),
    secondary-foreground to oklch(0.269 0 0), and brand to
    oklch(0.663 0.205 39.9) — #F5580B, Robomous orange.
  • cn is a dependency rather than a local module, and styles.css
    imports a sibling tailwind.css that ships with it.

Test plan

  • pnpm format:check && pnpm lint && pnpm build && pnpm test — 75 tests
  • npm pack --dry-run carries src/theme/styles.css,
    src/theme/tailwind.css, dist/gates/ and dist/components/separator,
    and no generator config
  • Both new guards shown to fail when they should: a sibling that is
    missing, a sibling that is uncommitted, a component module that is not
    exported
  • Migrated both consumers against a locally packed 0.2.0 before
    publishing anything: robomous-cloud lint/build green with 38 tests,
    VisionSet lint/build green with 2425 tests

The package was built to guard the fidelity of what the shadcn CLI
wrote, which cost 2,191 lines of snapshots to protect 24 lines of real
difference. What was actually needed was the ability to install
components with components.json. These two documents record the audit,
the design that follows from it, and the task-by-task plan.
A bare h-auto would not displace data-[size=default]:h-8: tailwind-merge
replaces within a group only when the modifiers match, so both would
survive and the attribute selector would win. The helper carried the
modifiers for this reason.
The snapshots under shadcn/ duplicated src/primitives/ almost byte for
byte: 19 of 21 components differed by a single import line. What they
guarded was a fidelity this package no longer wants, since the
components are going to be restyled.

Also drops the shadcn CLI from dependencies, where it was never
imported and reached every consumer's production tree.
Task 7 said to make variantClasses and FOUNDATION_BADGE internal, but
the gate tests live in their own file and import them from the module,
so unexporting them is not possible. They move into the test file that
uses them, which lands the published surface on eight either way.
Progress read value only to size the indicator and never forwarded it
to Radix's Root, which is what derives aria-valuenow. progressAria made
every caller repeat the number, and nothing caught the ones that
forgot — opt-in accessibility.
inlineLink was a class string a caller had to remember because the
component could not carry the size itself. It can now.
twoLineTrigger had to qualify on data-[size=default] because it arrived
from outside and had to outrank the base class. As a prop it qualifies
on nothing.
menuSurface fixed two things from the outside at every call site, and a
gate existed to catch the sites that forgot it. Both are defaults now,
so there is nothing left to forget.
The local module reimplemented what shadcn-ui/cn ships compiled. The
name and signature a consumer sees are unchanged; clsx and
tailwind-merge leave dependencies, and src/lib is now empty.
Moving the stylesheet invalidates tailwind.css, which a gate asserts
literally, and the cn migration left the utils alias pointing at a
directory it had just deleted. The CLI writes that alias into every
component it generates, so the plan now measures the value instead of
assuming it.
What is left scans for design rules: no colour inside a class string,
brand as identity only, one home for the tokens, one status palette,
one icon set. The rest guarded a v1 vocabulary that no repository
still speaks.
primitives/ carried the idea of an untouchable layer beneath another
layer, which is the premise this release retires. theme/ puts the
stylesheet next to the token mirror and the status vocabulary.
They were .mjs with hand-written declarations and a test command of
their own. tsc now emits the types and vitest covers them, so
test:gates and its CI step are gone.
DESIGN.md argued from what upstream would not let us change. It now
argues from the rules we hold ourselves to, and names the gate behind
each one. Also drops the inherited VisionSet narrative that no longer
describes anything here.
styles.css opens with `@import "shadcn/tailwind.css"` and ships as source
for the consumer's Tailwind build, so the package is a runtime dependency
of this one. The audit that removed it grepped for `from "shadcn"` and
never saw the CSS import; nothing here compiles CSS, so lint, build and
test all stayed green while the published package was broken.

Adds src/theme/imports.test.ts, which resolves every package the
stylesheet imports through that package's own exports map — under the
`style` condition a CSS bundler asks for, since Node's own resolver
reports tw-animate-css as missing — and fails naming the specifier that
landed nowhere. Confirmed failing before the dependency came back.
Overriding a utility made sense while the correction arrived from
outside the component. Owning the component, removing it is clearer,
and the design doc now says which one shipped.
The components are this repository's to author from now on, so the
generator config and the CLI dependency it needed are gone, and the gate
that held that config to a schema goes with them. src/theme/tailwind.css
now carries the variant layer the stylesheet imports: the `data-*`
variants every state style in the package qualifies on, which match both
the `data-state` attribute the behaviour libraries set and the bare
attribute Tailwind's own shorthand would cover alone.

Colour comes from the design file. `foreground` and `primary` move to
oklch(0.2 0 0), `secondary-foreground` to oklch(0.269 0 0), and `brand`
becomes Robomous orange: oklch(0.663 0.205 39.9), verified to round-trip
to #F5580B, which rounding all three channels to three decimals does not.

Three defects turned up on the way:

- `Separator` sat in src/components/, imported by field.tsx and exported
  by nothing, so no consumer could reach it. The gate that would have
  caught that no longer existed; src/gates/design.test.ts now compares the
  directory against the public surface.
- src/theme/imports.test.ts resolved package specifiers and skipped
  relative ones by design, so the moment the stylesheet imported a sibling,
  the exact breakage that guard exists to catch went unguarded. It now
  also asserts the sibling exists and is committed: `files: ["src"]` packs
  the working tree, so an uncommitted file publishes fine from here and is
  missing from the clone CI builds from.
- The token contract compared quote style, so a formatter run that spelled
  a font family in double quotes failed it without changing anything a
  browser resolves.

Also declares the formatting this repository never did — .editorconfig,
.gitattributes, a Prettier config and the two scripts. Nothing is
reformatted here; that is the next commit.
Prettier runs in CI from this commit, and this is the one pass that makes
the tree match it. Almost all of it is semicolons on the component
modules — the hand-written modules under src/theme/ and src/gates/ already
carried them, so this converges the two halves of the package on one
answer rather than imposing a new taste. The rest is Prettier's own line
breaking.

Nothing here changes behaviour: lint, build and the seventy-five tests
pass unchanged on both sides of it. The regexes in tokens.test.ts that
match the stylesheet's base layer already allowed whitespace between
selectors, so splitting a selector list across lines does not reach them.

Markdown stays out of Prettier's reach, for reasons written into
.prettierignore: it rewrites `*emphasis*` as `_emphasis_`, and it pads
table cells until DESIGN.md carries single lines past 170 characters — in
the two files whose whole convention is a hard wrap at 100.
`cn` arrives in this release as a dependency rather than a local module, so
it is a new transitive dependency for everything downstream. VisionSet holds
third-party releases for three days before it will resolve them, and both
versions `^0.2.5` admits are younger than that — 0.2.5 by hours — so the
range as written cannot be installed there at all.

The floor was never a requirement: `pnpm add` picked it. 0.2.4 is the newest
version already past that wait, exports the `cn` the components import, and
builds and passes the suite unchanged.
@YaelAnaya
YaelAnaya merged commit 837fca4 into main Sep 7, 2026
1 check passed
@YaelAnaya
YaelAnaya deleted the restructure-0.2.0 branch September 7, 2026 04:09
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