From 3782120b492731e8371f5733fad45114d0be895f Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Mon, 8 Jun 2026 05:47:08 +0200 Subject: [PATCH] docs(specs): token-styled ui/Select replacing raw selects (#51) --- .../specs/2026-06-08-token-select-design.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-08-token-select-design.md diff --git a/docs/superpowers/specs/2026-06-08-token-select-design.md b/docs/superpowers/specs/2026-06-08-token-select-design.md new file mode 100644 index 0000000..1ec8c7d --- /dev/null +++ b/docs/superpowers/specs/2026-06-08-token-select-design.md @@ -0,0 +1,132 @@ +# Token-Styled Select — Design + +**Date:** 2026-06-08 +**Status:** Approved (brainstorming) — ready for implementation planning. +**Issue:** #51. + +## Context + +Four raw ``, so existing tests +using `userEvent.selectOptions` / `HTMLSelectElement` must be rewritten to click interaction. + +### Decisions (from brainstorming) +1. **All four selects → `ui/Select`** (uniform token styling + focus ring; lowest risk). +2. New `ui/select.tsx` wraps Base UI Select; **validated by running** (novel primitive). +3. Tests rewritten to Base UI Select interaction (open trigger → click item). + +## Components + +### `web/src/components/ui/select.tsx` (new) +Wrap Base UI Select in the `ui/*` style (`data-slot`, `cn`, no semicolons), mirroring +`ui/combobox.tsx`/`ui/menu.tsx`. Exports (names final after validate-by-running): +- `Select` — `SelectPrimitive.Root` (generic value; `value`/`onValueChange`/`defaultValue`/`disabled`/`name`). +- `SelectTrigger` — `SelectPrimitive.Trigger` styled to **match Input**: `h-8 w-full rounded-lg border + border-input bg-transparent px-2.5 py-1 text-sm inline-flex items-center justify-between + transition-colors outline-none focus-visible:border-ring focus-visible:ring-3 + focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed + disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive dark:bg-input/30` + a + trailing chevron (`SelectPrimitive.Icon` with a lucide `ChevronDown`, `aria-hidden`) and a + `SelectValue` (`SelectPrimitive.Value`) showing the chosen item (with `placeholder`). +- `SelectContent` — `SelectPrimitive.Portal` + `SelectPrimitive.Positioner` (`sideOffset`, `z-50`) + + `SelectPrimitive.Popup` styled as a card (`min-w-[var(--anchor-width)]` if supported, else + `min-w-32`; `rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none` + + open/close animation data-attrs). +- `SelectItem` — `SelectPrimitive.Item` row (`flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm + outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground`) + + a `SelectPrimitive.ItemIndicator` (lucide `Check`) and `SelectPrimitive.ItemText`. +- Token classes only (no raw palette). The exact part tree + props (`SelectValue` placeholder, + positioner anchoring, item `value`) **must be confirmed by running the story** (Base UI Select is + novel here), as combobox/menu/toast were. + +**Accessibility:** `SelectTrigger` accepts `id` so the existing `