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

180 lines
7.5 KiB
Markdown

# 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:
```tsx
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:**
```css
[data-slot="toggle-group-item"] { /* only overrides */
min-w-0 flex-1 ... rounded-none first:rounded-l-md ...
}
```
**Generated output after:**
```css
[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.tsx``toggleVariants` 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.tsx``toggleVariants.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.
```ts
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.