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.
180 lines
7.5 KiB
Markdown
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.
|