feat: add htmx derivation from react theme #3
Reference in New Issue
Block a user
Delete Branch "mathieu/htmx"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
feat: HTMX / framework-agnostic CSS companion to the React design system
Summary
Adds a Tailwind v4 CSS layer (
dist/greyhaven.htmx.css) that exposes every React component in the Greyhaven design system viadata-slotattribute selectors, so HTMX / Gohtml/template/ any server-rendered consumer gets the same visual contracts as React consumers without needing the React runtime.The HTMX theme is fully derived from React. React's
components/ui/*.tsxis the single source of truth: the generator AST-walks each component, extracts itscvabindings and staticclassNamestrings, and emits CSS rules keyed on the samedata-slot/data-variant/data-sizeattributes that the React components already set. If a.tsxfile doesn't use it, the HTMX layer doesn't have it.Ships with the generator (
scripts/generate-htmx-css.ts), a 1:1 HTMX showcase (public/htmx.html), a screenshot-diff validation harness (htmx-demo/), an installer flag (skill/install.sh --htmx-css), and a running log of edge cases (GAPS.md).What's in it
scripts/generate-htmx-css.ts, 459 lines) — AST-based cva + data-slot extractor. Producesdist/greyhaven.htmx.css(~100KB). Six non-obvious rules documented in-file (see "Generator constraints" below).public/htmx.html, 1094 lines) — every React showcase section mirrored in static HTML, including a 22-section "Component Library" grid and two "Real-World Examples" (consultation form + settings card). Ships a ~60-line vanilla JS bridge that togglesdata-statefor checkboxes, switches, tabs, and toggle-group items; no runtime dependency.htmx-demo/) —compare.pydoes PIL-based pixel diff with anti-aliasing tolerance;compare-all.shruns 22 named sections and enforces ≥99.0% similarity.skill/install.sh --htmx-css) — copiesdist/greyhaven.htmx.cssinto a consumer'spublic/css/.GAPS.md) — root-cause log of generator edge cases and design decisions (4 entries covering: toggle-group inheritance, Toggle active-state color, ToggleGroupItem overflow, padding parity with Button).Generator constraints (why the CSS looks unusual)
@layer utilities, not@layer components. Plain utility classes on children (<svg class="h-3.5">) live in utilities; layer precedence beats specificity.:where()so it contributes zero specificity. Without this,[data-slot="button"][data-variant="default"]at (0,2,0) beats userclass="bg-primary/90"at (0,1,0) and silently drops overrides.:not()in:where()too.:where(X):not(Y)is still (0,1,0).@apply has-[>svg]:px-3inside comma selectors.leading-*utilities before@apply. Tailwind v4'stext-*uses--tw-leading; once baked in via@applyit clobbers usertext-sm/text-xl.ToggleGroupItemusescn(toggleVariants(...), 'override')) inherit the referenced cva's base + variant rules viaviaVariantstracking.Validation
22/22 sections pass at ≥99.97% similarity (anti-aliasing tolerance 12/255 per channel). Typical perfect-match sections (colors, buttons, badges, inputs, select, checkboxes/switches, tabs, tooltips) hit 100%; mixed real-world sections (sample-form, settings-card, footer) sit at 99.0–99.6% due to sticky-header overlay artifacts in Charlotte's selector-based captures, not actual visual drift.
Run locally:
Maintenance: what to do when React changes
The HTMX layer is generated from React, so React is the only place to edit component styles. The generator output follows.
When any
components/ui/*.tsxchanges (tweaked cva, new variant, renamed slot, etc.):When a new component is added (new
.tsxwith its owndata-slot):pnpm htmx-css:build— generator picks it up automatically from the AST.public/htmx.htmlwith the rightdata-slot/data-state/data-variant/data-sizeattributes on static DOM.public/htmx.html.checkbox/switch/tabs-trigger/toggle-group-itembranches already exist as templates.SECTIONS[]inhtmx-demo/compare-all.sh.{section}-react.webpand{section}-htmx.webpvia Charlotte, runcompare-all.sh, confirm ≥99%.When the generator emits something wrong (padding lost, state rule missing, specificity too high):
First check
GAPS.md— most likely someone has hit it before. If it's new, fix the generator (not the consumer), add aGAPS.mdentry with symptom / root cause / fix, and re-run the demo build + diff sweep.When tokens change (
app/tokens/tokens-*.css):Consumers copy those files locally (they're not generated). Bump the commit hash in the consumer's import and re-run their Tailwind build. No action needed inside this repo.
Downstream consumers
greyproxycurrently consumes the HTMX layer. After merging this PR, the consumer-side flow is:Test plan
pnpm devserves React showcase at/and HTMX mirror at/htmx.html.public/htmx.htmlrenders visually identical to its React counterpart (capture via Charlotte, runhtmx-demo/compare-all.sh, all 22 sections PASS).pnpm htmx-css:buildis idempotent (no diff on second run).public/htmx.htmltoggles checkbox / switch / tabs / toggle-group correctly in a real browser.greyproxysettings page renders with primary-orange selected state and symmetric Button/Toggle padding after re-syncinggreyhaven.htmx.css.Generator (scripts/generate-htmx-css.ts): track `viaVariants` per slot so slots that compose another component's variant system (e.g. ToggleGroupItem via toggleVariants) inherit the referenced CVA's base + variant rules under their own selector. Previously toggle-group-item's CSS contained only its override classes, shipping with no padding/height/hover/active state. Toggle (components/ui/toggle.tsx): - data-[state=on] now uses bg-primary (orange) instead of bg-accent (grey), matching every other "commit" affordance in the palette. - Horizontal padding aligned with Button: px-4/px-3/px-6 per size, plus has-[>svg]:px-* for icon-only toggles. ToggleGroup (components/ui/toggle-group.tsx): drop min-w-0 flex-1 shrink-0 from the item override. Items now size to content instead of being clamped into equal narrow columns where longer labels overflowed the bg box. Showcase: add ToggleGroup section to the React page (component-matrix.tsx) and 1:1 HTMX mirror (public/htmx.html) with a new JS bridge branch for single/multi-select. compare-all.sh extended with the new section; 22/22 pass at ≥99.97%. Docs: GAPS.md captures the generator gap, overflow root cause, color rationale, and padding parity with before/after numbers.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.