feat: add htmx derivation from react theme #3

Open
mathieu wants to merge 2 commits from mathieu/htmx into main
Owner

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 via data-slot attribute selectors, so HTMX / Go html/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/*.tsx is the single source of truth: the generator AST-walks each component, extracts its cva bindings and static className strings, and emits CSS rules keyed on the same data-slot / data-variant / data-size attributes that the React components already set. If a .tsx file 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

  • Generator (scripts/generate-htmx-css.ts, 459 lines) — AST-based cva + data-slot extractor. Produces dist/greyhaven.htmx.css (~100KB). Six non-obvious rules documented in-file (see "Generator constraints" below).
  • HTMX showcase (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 toggles data-state for checkboxes, switches, tabs, and toggle-group items; no runtime dependency.
  • Validation harness (htmx-demo/) — compare.py does PIL-based pixel diff with anti-aliasing tolerance; compare-all.sh runs 22 named sections and enforces ≥99.0% similarity.
  • Installer (skill/install.sh --htmx-css) — copies dist/greyhaven.htmx.css into a consumer's public/css/.
  • Gaps log (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)

  1. Emit into @layer utilities, not @layer components. Plain utility classes on children (<svg class="h-3.5">) live in utilities; layer precedence beats specificity.
  2. Wrap every attribute selector in :where() so it contributes zero specificity. Without this, [data-slot="button"][data-variant="default"] at (0,2,0) beats user class="bg-primary/90" at (0,1,0) and silently drops overrides.
  3. Wrap :not() in :where() too. :where(X):not(Y) is still (0,1,0).
  4. Emit default-variant rules as two separate rules, never a comma-joined selector list. Tailwind v4 miscompiles @apply has-[>svg]:px-3 inside comma selectors.
  5. Strip leading-* utilities before @apply. Tailwind v4's text-* uses --tw-leading; once baked in via @apply it clobbers user text-sm/text-xl.
  6. Slots that compose another cva (e.g. ToggleGroupItem uses cn(toggleVariants(...), 'override')) inherit the referenced cva's base + variant rules via viaVariants tracking.

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:

pnpm dev                              # serve React showcase + public/htmx.html
# Capture each section's -react.webp and -htmx.webp via Charlotte
bash htmx-demo/compare-all.sh         # 22-section diff, threshold 99%

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/*.tsx changes (tweaked cva, new variant, renamed slot, etc.):

pnpm htmx-css:build        # regenerates dist/greyhaven.htmx.css
pnpm htmx-demo:build       # recompiles public/htmx.css (Tailwind over the new layer)

When a new component is added (new .tsx with its own data-slot):

  1. pnpm htmx-css:build — generator picks it up automatically from the AST.
  2. Add a mirror section to public/htmx.html with the right data-slot / data-state / data-variant / data-size attributes on static DOM.
  3. If the component is interactive (needs open/close or state toggling), extend the vanilla JS bridge at the bottom of public/htmx.html. checkbox / switch / tabs-trigger / toggle-group-item branches already exist as templates.
  4. Add the section name to SECTIONS[] in htmx-demo/compare-all.sh.
  5. Capture both {section}-react.webp and {section}-htmx.webp via Charlotte, run compare-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 a GAPS.md entry 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

greyproxy currently consumes the HTMX layer. After merging this PR, the consumer-side flow is:

./skill/install.sh <consumer-path> --htmx-css     # copy dist/greyhaven.htmx.css
# consumer re-runs its own Tailwind build (e.g. npm run build:css)

Test plan

  • pnpm dev serves React showcase at / and HTMX mirror at /htmx.html.
  • Every section in public/htmx.html renders visually identical to its React counterpart (capture via Charlotte, run htmx-demo/compare-all.sh, all 22 sections PASS).
  • pnpm htmx-css:build is idempotent (no diff on second run).
  • Interactive bridge in public/htmx.html toggles checkbox / switch / tabs / toggle-group correctly in a real browser.
  • Downstream greyproxy settings page renders with primary-orange selected state and symmetric Button/Toggle padding after re-syncing greyhaven.htmx.css.
# 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 via `data-slot` attribute selectors, so HTMX / Go `html/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/*.tsx` is the single source of truth: the generator AST-walks each component, extracts its `cva` bindings and static `className` strings, and emits CSS rules keyed on the same `data-slot` / `data-variant` / `data-size` attributes that the React components already set. If a `.tsx` file 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 - **Generator** (`scripts/generate-htmx-css.ts`, 459 lines) — AST-based cva + data-slot extractor. Produces `dist/greyhaven.htmx.css` (~100KB). Six non-obvious rules documented in-file (see "Generator constraints" below). - **HTMX showcase** (`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 toggles `data-state` for checkboxes, switches, tabs, and toggle-group items; no runtime dependency. - **Validation harness** (`htmx-demo/`) — `compare.py` does PIL-based pixel diff with anti-aliasing tolerance; `compare-all.sh` runs 22 named sections and enforces ≥99.0% similarity. - **Installer** (`skill/install.sh --htmx-css`) — copies `dist/greyhaven.htmx.css` into a consumer's `public/css/`. - **Gaps log** (`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) 1. Emit into `@layer utilities`, not `@layer components`. Plain utility classes on children (`<svg class="h-3.5">`) live in utilities; layer precedence beats specificity. 2. Wrap every attribute selector in `:where()` so it contributes zero specificity. Without this, `[data-slot="button"][data-variant="default"]` at (0,2,0) beats user `class="bg-primary/90"` at (0,1,0) and silently drops overrides. 3. Wrap `:not()` in `:where()` too. `:where(X):not(Y)` is still (0,1,0). 4. Emit default-variant rules as two separate rules, never a comma-joined selector list. Tailwind v4 miscompiles `@apply has-[>svg]:px-3` inside comma selectors. 5. Strip `leading-*` utilities before `@apply`. Tailwind v4's `text-*` uses `--tw-leading`; once baked in via `@apply` it clobbers user `text-sm`/`text-xl`. 6. Slots that compose another cva (e.g. `ToggleGroupItem` uses `cn(toggleVariants(...), 'override')`) inherit the referenced cva's base + variant rules via `viaVariants` tracking. ## 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: ```bash pnpm dev # serve React showcase + public/htmx.html # Capture each section's -react.webp and -htmx.webp via Charlotte bash htmx-demo/compare-all.sh # 22-section diff, threshold 99% ``` ## 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/*.tsx` changes** (tweaked cva, new variant, renamed slot, etc.): ```bash pnpm htmx-css:build # regenerates dist/greyhaven.htmx.css pnpm htmx-demo:build # recompiles public/htmx.css (Tailwind over the new layer) ``` **When a new component is added** (new `.tsx` with its own `data-slot`): 1. `pnpm htmx-css:build` — generator picks it up automatically from the AST. 2. Add a mirror section to `public/htmx.html` with the right `data-slot` / `data-state` / `data-variant` / `data-size` attributes on static DOM. 3. If the component is interactive (needs open/close or state toggling), extend the vanilla JS bridge at the bottom of `public/htmx.html`. `checkbox` / `switch` / `tabs-trigger` / `toggle-group-item` branches already exist as templates. 4. Add the section name to `SECTIONS[]` in `htmx-demo/compare-all.sh`. 5. Capture both `{section}-react.webp` and `{section}-htmx.webp` via Charlotte, run `compare-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 a `GAPS.md` entry 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 `greyproxy` currently consumes the HTMX layer. After merging this PR, the consumer-side flow is: ```bash ./skill/install.sh <consumer-path> --htmx-css # copy dist/greyhaven.htmx.css # consumer re-runs its own Tailwind build (e.g. npm run build:css) ``` ## Test plan - [ ] `pnpm dev` serves React showcase at `/` and HTMX mirror at `/htmx.html`. - [ ] Every section in `public/htmx.html` renders visually identical to its React counterpart (capture via Charlotte, run `htmx-demo/compare-all.sh`, all 22 sections PASS). - [ ] `pnpm htmx-css:build` is idempotent (no diff on second run). - [ ] Interactive bridge in `public/htmx.html` toggles checkbox / switch / tabs / toggle-group correctly in a real browser. - [ ] Downstream `greyproxy` settings page renders with primary-orange selected state and symmetric Button/Toggle padding after re-syncing `greyhaven.htmx.css`.
mathieu added 2 commits 2026-04-24 20:49:58 +00:00
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.
mathieu requested review from juan 2026-04-24 20:49:58 +00:00
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin mathieu/htmx:mathieu/htmx
git checkout mathieu/htmx
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: monadical/greyhaven-design-system#3