diff --git a/README.md b/README.md index 94445b5..20ea723 100644 --- a/README.md +++ b/README.md @@ -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 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 -`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 -[data-slot="card"] { @apply bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm; } -[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; } -[data-slot="card-title"] { @apply leading-none font-semibold; } +@layer utilities { + :where([data-slot="card"]) { @apply bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm; } + :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 …; } -[data-slot="button"]:not([data-variant]), -[data-slot="button"][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; } -[data-slot="button"][data-size="sm"] { @apply h-8 rounded-md gap-1.5 px-3; } + :where([data-slot="button"]) { @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium …; } + :where([data-slot="button"]):where(:not([data-variant])) { @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; } + :where([data-slot="button"]):where([data-variant="outline"]) { @apply border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground; } + :where([data-slot="button"]):where([data-size="sm"]) { @apply h-8 rounded-md gap-1.5 px-3; } +} ``` ### Install @@ -304,19 +308,58 @@ Add to your Tailwind v4 input CSS: Active ``` +See `public/htmx.html` for a full reference implementation — every component in the React showcase has a matching static-HTML section. + ### 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. -- **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**: `
` covers Accordion/Collapsible, `` 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 -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 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-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:build` | Type-check MCP server | | `pnpm lint` | Run ESLint |