overhaul: 03 primitives - #17
Open
CS-5 wants to merge 1 commit into
Open
Conversation
The zero-JS primitive layer: Button, Card and its sub-parts, Badge, Input,
Textarea, Label, FieldError, Separator, Accordion, Dialog, Carousel, Icon,
Skeleton — every one on /styleguide in all variants and all three themes.
Interactivity stays as high up the ladder as it can: Accordion is native
<details> with the name attribute for exclusive open, Dialog is native
<dialog> so the browser owns the focus trap and Esc, Carousel is a
scroll-snap track. Together the whole styleguide ships 835 bytes of inlined
script, and a page built from only the static primitives ships none.
Two silent bugs surfaced here, both of which would have spread across every
page:
cn() was dropping font sizes. Tailwind builds text-* utilities from both
--text-* and --color-*, and the merge step only knows Tailwind's stock scale,
so it treated every text-* class as one conflict group and kept the last.
cn("text-primary-foreground", "text-body") collapsed to text-body, which
rendered every primary button's label in body gray on Safety Yellow — 1.3:1,
measured in the browser. @/lib/cn now registers the DESIGN.md §3 type scale as
the font-size group; those buttons measure 11.7:1, and size-plus-color pairs
keep both classes.
Relatedly, text-body is a color and not a size, so Button, Input, and Textarea
were asking for a size and getting none. They use text-copy now.
Icons follow ADR 0002: @tabler/icons inlined at build, no astro-icon or
Iconify. The package's exports map rewrites every subpath including
package.json, so the icons directory is located through a known icon instead.
CVA recipes live in sibling *.variants.ts files, since Astro forbids exporting
values from a component. That also lets one component reuse another's recipe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRfxMh7FLjQtDbb1BEsCbR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Layer 3 of the overhaul stack, on
overhaul/02-design-system.plan/03-primitives.md.The zero-JS primitive layer, plus the agent skill for adding more.
What's here
Button(5 variants × 4 sizes, renders<a>givenhref),CardwithCardHeader/Title/Description/Content/Footer,Badge(7 tones incl. the four sponsor tiers),Input,Textarea,Label,FieldError,Separator,Accordion+AccordionItem,Dialog,Carousel,Icon,Skeleton. Every one is on/styleguidein all variants and states, and the theme section at the bottom now renders the full set underdata-theme="frc"and"fll".Interactivity stays as high up the ladder as it can go: Accordion is native
<details>with thenameattribute for exclusive open, Dialog is native<dialog>so the browser owns the focus trap and Esc, Carousel is a CSS scroll-snap track. The whole styleguide — Dialog and Carousel included — ships 835 bytes of inlined script, and a page built from only the static primitives ships zero<script>tags and zero.jsfiles.Also here:
src/components/ui/primitives/README.md(the conventions) and.claude/skills/shadcn-astro/SKILL.md(how to port a shadcn component, with the Radix→native mapping table and the Accordion worked through end to end, including what gets dropped and why).Both would have spread through every page built after this one.
1.
cn()was dropping font sizes.Tailwind builds
text-*utilities from two token namespaces —--text-*for sizes,--color-*for colors — and the merge step only recognizes Tailwind's stock scale (text-sm,text-lg), nottext-h4ortext-small. So it treated everytext-*class as a single conflict group and kept only the last one:The first line is the damaging one. Every primary button's label rendered in body gray on Safety Yellow — I measured it in the browser at 1.3:1.
@/lib/cnis now a configured merge (cnfast takes a tailwind-merge config) that registers the §3 type scale as the font-size group. Same buttons now measure 11.7:1, and size-plus-color pairs keep both classes while two colors or two sizes still resolve to the last.The tradeoff worth flagging: adding a size token to
global.cssnow means adding one line tocn.ts, or it silently loses to any color next to it. That's called out incn.ts, the primitives README, and the skill.2.
text-bodyis a color, not a size. Phase 02 established this (bodynames both, Tailwind resolves colors first), but Button, Input, and Textarea were all usingtext-bodyintending the size and silently getting none. They usetext-copynow.Deviations from the phase brief
@tabler/icons, inlined at build per ADR 0002 — notastro-icon+@iconify-json/tabler. Worth knowing: that package'sexportsmap is"./*": "./icons/*", which rewrites every subpath includingpackage.json, sosrc/lib/icon.tslocates the icons directory through a known icon file rather than the manifest. An unknown icon name throws at build with a pointer to tabler.io/icons.*.variants.tsfiles. Astro forbids exporting values from a component (astro/no-exports-from-components), sobuttonVariantscan't live inButton.astro. This turns out better than shadcn's arrangement: another component can import the recipe rather than copying classes.Skeletonincluded, noSpinner— the calendar's loading state needs the former in Phase 07; nothing needs the latter.Verified
pnpm check && pnpm buildgreen.dialog.openistrueafter the trigger,falseafter Escape.cn()fix (11.71:1; 14px / 16.85px / 18.70px for sm/md/lg, the fluid clamp working).Generated by Claude Code