# 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.
