Files
greyhaven-design-system/GAPS.md
Mathieu Virbel 90930d8f78 feat(htmx-css): ToggleGroup support + padding/primary parity
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.
2026-04-24 14:43:55 -06:00

7.5 KiB

HTMX CSS Generator — Known Gaps & Fixes

Running log of edge cases discovered while consuming dist/greyhaven.htmx.css in framework-agnostic (HTMX / Go html/template / etc.) projects. Each entry captures: the bug, its root cause, and the fix. Keep this file updated when new gaps surface so consumers have a single place to check.


Fixed

2026-04-24 — toggle-group-item missing Toggle base styles

Symptom (consumer-side): data-slot="toggle-group-item" renders as unstyled inline text — no padding, no height, no border, and data-state="on" produces no visual change.

Root cause: In React, ToggleGroupItem composes toggleVariants() with a small set of segmented-group overrides:

className={cn(
  toggleVariants({ variant, size }),  // base + variant + size classes
  'min-w-0 flex-1 shrink-0 rounded-none ...'
)}

The generator's extractSlot only captured the first string-literal arg of cn(...) — the segmented overrides — and never followed the toggleVariants(...) call to pull in Toggle's base, variant, and size rules. As a result, the emitted [data-slot="toggle-group-item"] had no padding, hover, disabled, or — critically — the data-[state=on]:bg-accent data-[state=on]:text-accent-foreground rule that drives the pressed state.

Fix (scripts/generate-htmx-css.ts): SlotExtract now carries a viaVariants: string[] field. When processing className={cn(xVariants(...), 'literal')}, the extractor records every *Variants call it sees. The emitter then merges each referenced CVA's base + variant rules under the slot's own selector, so a slot that composes another component's variant system inherits its full rule set.

Generated output before:

[data-slot="toggle-group-item"] { /* only overrides */
  min-w-0 flex-1 ... rounded-none first:rounded-l-md ...
}

Generated output after:

[data-slot="toggle-group-item"] { /* full Toggle base + item overrides */
  inline-flex items-center ... data-[state=on]:bg-accent ...
  min-w-0 flex-1 ... rounded-none first:rounded-l-md ...
}
[data-slot="toggle-group-item"][data-variant="outline"] { /* inherited variant */ }
[data-slot="toggle-group-item"][data-size="sm"] { /* inherited size */ }

Consumer impact: existing HTMX demos do not use toggle-group-item, so no screenshot-diff regression risk. Consumers previously working around this by using data-slot="toggle" on each item (with manual rounded-none / border overrides) can switch to the clean toggle-group-item form.


2026-04-24 — Toggle active state uses primary (orange), not accent (grey)

Symptom: Design review flagged the active-state grey (bg-accent) as too muted — users expected the brand orange for selected items in a segmented control, matching every other "selected" affordance in the system (primary button, active nav link, focus ring).

Fix: components/ui/toggle.tsxtoggleVariants base class swapped from data-[state=on]:bg-accent data-[state=on]:text-accent-foreground to data-[state=on]:bg-primary data-[state=on]:text-primary-foreground. This propagates to ToggleGroupItem automatically (composes toggleVariants) and to the generated [data-slot="toggle"] and [data-slot="toggle-group-item"] rules after pnpm htmx-css:build && pnpm htmx-demo:build.

Rationale: Greyhaven's palette reserves accent for hover hints and subtle surface shifts; primary is the single brand-accent color, used wherever a choice is committed. Selected-state for Toggle/ToggleGroup now matches that convention.

2026-04-24 — ToggleGroupItem overflow: text escapes the bg box

Symptom: With segmented labels of uneven length ("System" / "Light" / "Dark"), the longer label's text rendered outside its button's background rectangle. Selected-state highlight appeared narrower than the text it was supposed to cover, and hover state was clipped for longer labels.

Root cause: ToggleGroupItem layered 'min-w-0 flex-1 shrink-0' on top of toggleVariants' size-based min-w-*. Because tailwind-merge keeps the later min-w-0, each item was allowed to shrink below its content. Combined with flex: 1 1 0% and w-fit on the group, items ended up forced to equal narrow columns sized to container_width / N — which for the "System" case was ~37px, well below the text's ~45px intrinsic width. whitespace-nowrap then let the text bleed out of the button's layout box.

Fix: components/ui/toggle-group.tsx — dropped min-w-0 flex-1 shrink-0 from ToggleGroupItem's override string. Items now size to their content (respecting the min-w-* floor from toggleVariants), the group's w-fit sums them up, and padding is symmetric. Unequal-width items are the intentional result (a segmented "System/Light/Dark" now shows "System" wider than "Light"/"Dark"); if a consumer specifically wants equal-width columns, they can re-apply flex-1 basis-0 on each item via className.

Consumer impact: no regressions in the existing 21 sections; the new toggle-group section passes at 99.98%.

2026-04-24 — Toggle horizontal padding aligned with Button

Symptom: Side-by-side, a Toggle (or ToggleGroupItem) looked cramped compared to a regular Button of the same size — the active-state orange bg sat tight against the label, while Button had comfortable breathing room on both sides. Perceived as a visual-rhythm bug across any UI that mixed the two in the same view.

Root cause: Toggle's cva had roughly half the horizontal padding of Button per size:

size Button Toggle (old)
default px-4 (16px) px-2 (8px)
sm px-3 (12px) px-1.5 (6px)
lg px-6 (24px) px-2.5 (10px)

Fix: components/ui/toggle.tsxtoggleVariants.size.* horizontal padding now matches Button exactly. Also added has-[>svg]:px-* for icon-only toggles, mirroring Button's affordance. min-w-* kept as a floor so very short labels (A, B) still render as balanced pills.

size: {
  default: 'h-9 px-4 min-w-9 has-[>svg]:px-3',
  sm:      'h-8 px-3 min-w-8 has-[>svg]:px-2.5',
  lg:      'h-10 px-6 min-w-10 has-[>svg]:px-4',
},

Known (not yet fixed)

(Add entries here as they are discovered. Template:

### YYYY-MM-DD — <slot or behavior>
**Symptom:** ...
**Root cause:** ...
**Workaround:** ...
**Proposed fix:** ...

)

Radix primitive-only components (AlertDialogAction, AlertDialogCancel)

Symptom: These components have no static data-slot in their JSX — they wrap <AlertDialogPrimitive.Action> which sets data-slot at runtime. The AST extractor never sees them, so no CSS is emitted.

Workaround: Consumers should add data-slot="button" on the corresponding HTML element. The visual contract matches Button exactly (cn(buttonVariants(), className) in React).

Proposed fix: none yet — requires either parsing the Radix primitive source, or adding explicit data-slot attributes upstream.

Interactive components without a built-in JS bridge

Symptom: data-slot="select-trigger", data-slot="tooltip-content", data-slot="dialog-content", data-slot="popover-content" emit the CSS for their open/closed states, but nothing drives them.

Workaround: Consumers must implement open/close + positioning themselves (Alpine.js, plain JS, HTMX swap). public/htmx.html ships a ~60-line vanilla bridge for checkbox / switch / tabs; select / tooltip / dialog remain consumer concerns.

Proposed fix: ship an opt-in greyhaven.htmx.js companion with pointer positioning (e.g. via Floating UI) and focus management.