docs: README HTMX section — React-derivation, maintenance workflow, validation

- Make React-derivation explicit: the HTMX theme is generated from
  components/ui/*.tsx; never edit dist/greyhaven.htmx.css by hand.
- Fix stale CSS example: update @layer components → @layer utilities and
  show the :where() zero-specificity wrappers the generator now emits.
- Add "Maintenance — what to do when React changes" with explicit commands
  for cva tweaks, new components, generator bugs, and token updates.
- Point consumers to public/htmx.html as the 1:1 reference implementation
  and to GAPS.md for the generator edge-case log.
- Scope section updated: call out the vanilla-JS bridge that ships with
  public/htmx.html for checkbox/switch/tabs/toggle-group, separate from
  the positioning-heavy components that still need Alpine.
- Scripts Reference: add pnpm htmx-demo:build.
This commit is contained in:
2026-04-24 14:52:30 -06:00
parent 90930d8f78
commit 60e2b045eb

View File

@@ -253,20 +253,24 @@ pnpm build-storybook # Static build
The React components assume a React runtime. For HTMX, Django templates, Rails ERB, Go `html/template`, Astro SSR, or any other server-rendered stack, consume the design system via the auto-generated CSS layer. The React components assume a React runtime. For HTMX, Django templates, Rails ERB, Go `html/template`, Astro SSR, or any other server-rendered stack, consume the design system via the auto-generated CSS layer.
**The HTMX theme is 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 React already sets. If a `.tsx` file doesn't use it, the HTMX layer doesn't have it. Never edit `dist/greyhaven.htmx.css` by hand.
### What you get ### What you get
`dist/greyhaven.htmx.css` is generated from `components/ui/*.tsx` (AST walk over `cva()` configs + static `className` strings on `data-slot` elements). It contains ~300 `@layer components` rules, one per data-slot, with attribute selectors for variants and sizes. `dist/greyhaven.htmx.css` is ~100KB with ~300 rules emitted into `@layer utilities`, one per data-slot, with attribute selectors for variants and sizes. Selectors are wrapped in `:where()` for zero specificity so consumer `className` overrides still win (matching React + tailwind-merge behavior).
```css ```css
[data-slot="card"] { @apply bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm; } @layer utilities {
[data-slot="card-header"] { @apply grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6; } :where([data-slot="card"]) { @apply bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm; }
[data-slot="card-title"] { @apply leading-none font-semibold; } :where([data-slot="card-header"]) { @apply grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6; }
:where([data-slot="card-title"]) { @apply leading-none font-semibold; }
[data-slot="button"] { @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ; } :where([data-slot="button"]) { @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ; }
[data-slot="button"]:not([data-variant]), :where([data-slot="button"]):where(:not([data-variant])) { @apply bg-primary text-primary-foreground hover:bg-primary/90; }
[data-slot="button"][data-variant="default"] { @apply bg-primary text-primary-foreground hover:bg-primary/90; } :where([data-slot="button"]):where([data-variant="default"]) { @apply bg-primary text-primary-foreground hover:bg-primary/90; }
[data-slot="button"][data-variant="outline"] { @apply border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground; } :where([data-slot="button"]):where([data-variant="outline"]) { @apply border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground; }
[data-slot="button"][data-size="sm"] { @apply h-8 rounded-md gap-1.5 px-3; } :where([data-slot="button"]):where([data-size="sm"]) { @apply h-8 rounded-md gap-1.5 px-3; }
}
``` ```
### Install ### Install
@@ -304,19 +308,58 @@ Add to your Tailwind v4 input CSS:
<span data-slot="badge" data-variant="success">Active</span> <span data-slot="badge" data-variant="success">Active</span>
``` ```
See `public/htmx.html` for a full reference implementation — every component in the React showcase has a matching static-HTML section.
### Scope ### Scope
- **Static visual components** (Card, Button, Badge, Input, Label, Textarea, Table, Separator, Code, Kbd, Progress, Avatar, Skeleton, Alert, Pagination, Breadcrumb, Navbar, etc.) → fully driven by CSS, no JS needed. - **Static visual components** (Card, Button, Badge, Input, Label, Textarea, Table, Separator, Code, Kbd, Progress, Avatar, Skeleton, Alert, Pagination, Breadcrumb, Navbar, etc.) → fully driven by CSS, no JS needed.
- **Interactive components** (Dialog, Dropdown, Popover, Select, Combobox, Accordion, Tabs, Tooltip, etc.) → CSS emits their static styles, but open/close / positioning / focus management is the consumer's responsibility. Alpine.js pairs naturally with HTMX for these. - **Interactive components with simple state toggles** (Checkbox, Switch, Tabs, ToggleGroup) → `public/htmx.html` ships a ~60-line vanilla-JS delegation bridge that flips `data-state` on click. Copy or adapt it into your consumer.
- **Interactive components with positioning** (Dialog, Dropdown, Popover, Select, Combobox, Tooltip) → CSS emits their per-state visual rules, but open/close, portaling, and focus management are the consumer's responsibility. Alpine.js pairs naturally with HTMX for these.
- **Native HTML alternatives**: `<details>` covers Accordion/Collapsible, `<dialog>` covers Dialog. The CSS rules apply to those too. - **Native HTML alternatives**: `<details>` covers Accordion/Collapsible, `<dialog>` covers Dialog. The CSS rules apply to those too.
### Regenerate ### Maintenance — what to do when React changes
Because the HTMX layer is generated from React, **React is the only place to edit component styles**. Every consumer-facing change follows the same flow.
**When any `components/ui/*.tsx` changes** (tweaked cva, new variant, renamed slot):
```bash ```bash
pnpm htmx-css:build # Regenerate dist/greyhaven.htmx.css from components/ui/*.tsx pnpm htmx-css:build # regenerate dist/greyhaven.htmx.css
pnpm htmx-demo:build # recompile public/htmx.css (Tailwind over the new layer)
``` ```
Re-runs of `./skill/install.sh --htmx-css` in consumer projects refresh their copy. Then capture a screenshot diff to confirm nothing drifted:
```bash
pnpm dev # serve React at /, HTMX mirror at /htmx.html
# capture each section as -react.webp and -htmx.webp (via Charlotte or equivalent)
bash htmx-demo/compare-all.sh # 22-section diff, threshold 99%
```
**When a new component is added** (new `.tsx` with its own `data-slot`):
1. `pnpm htmx-css:build` — generator picks it up from the AST automatically.
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 needs state toggling, extend the vanilla JS bridge at the bottom of `public/htmx.html` (existing `checkbox` / `switch` / `tabs-trigger` / `toggle-group-item` branches are templates).
4. Add the section name to `SECTIONS[]` in `htmx-demo/compare-all.sh`.
5. Capture screenshots, run `compare-all.sh`, confirm ≥99%.
**When the generator emits something wrong** (padding lost, state rule missing, specificity issue):
Check `GAPS.md` first — most generator edge cases are already documented there with root cause + fix. If it's new, fix the generator (never patch the consumer), add a `GAPS.md` entry, and re-run the demo build + diff sweep.
**When tokens change** (`app/tokens/tokens-*.css`):
Consumers copy those files locally (they're not generated into `dist/`). Consumer bumps the commit hash in their import and re-runs their Tailwind build. No action inside this repo.
**Pushing changes to a consumer:**
```bash
# In the consumer repo, after this design system has been updated:
/path/to/greyhaven-design-system/skill/install.sh . --htmx-css
# then re-run the consumer's Tailwind build, e.g.:
npm run build:css
```
--- ---
@@ -341,6 +384,7 @@ Re-runs of `./skill/install.sh --htmx-css` in consumer projects refresh their co
| `pnpm tokens:build` | Regenerate CSS/TS/MD from token JSON files | | `pnpm tokens:build` | Regenerate CSS/TS/MD from token JSON files |
| `pnpm skill:build` | Regenerate skill/SKILL.md and skill/AGENTS.md from tokens + catalog | | `pnpm skill:build` | Regenerate skill/SKILL.md and skill/AGENTS.md from tokens + catalog |
| `pnpm htmx-css:build` | Regenerate dist/greyhaven.htmx.css from components/ui/*.tsx | | `pnpm htmx-css:build` | Regenerate dist/greyhaven.htmx.css from components/ui/*.tsx |
| `pnpm htmx-demo:build` | Recompile public/htmx.css (Tailwind over the generated layer) for the HTMX showcase |
| `pnpm mcp:start` | Start the MCP server (stdio transport) | | `pnpm mcp:start` | Start the MCP server (stdio transport) |
| `pnpm mcp:build` | Type-check MCP server | | `pnpm mcp:build` | Type-check MCP server |
| `pnpm lint` | Run ESLint | | `pnpm lint` | Run ESLint |