From ee9fcdf4ddcbd1863dc7e404987b2bc7b4c30ea8 Mon Sep 17 00:00:00 2001 From: Matthew L McPeak Date: Mon, 13 Jul 2026 17:07:11 -0400 Subject: [PATCH] docs: add design spec for removing reka-ui dependency Plans a hand-rolled replacement for reka-ui across Button, Checkbox, Switch, RadioGroup, Dialog, and Select to drop the dependency (and its peerDependency burden on consumers) while preserving the current public API and visual design exactly. Co-Authored-By: Claude Sonnet 5 --- .../specs/2026-07-13-remove-reka-ui-design.md | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-13-remove-reka-ui-design.md diff --git a/docs/superpowers/specs/2026-07-13-remove-reka-ui-design.md b/docs/superpowers/specs/2026-07-13-remove-reka-ui-design.md new file mode 100644 index 0000000..d26b887 --- /dev/null +++ b/docs/superpowers/specs/2026-07-13-remove-reka-ui-design.md @@ -0,0 +1,165 @@ +# Remove reka-ui dependency + +## Context + +The library recently finished migrating off PrimeVue onto a shadcn-vue-style +component set (`ff02f7e chore: remove PrimeVue and the ThemeEngine, migration +complete`). That migration adopted `reka-ui` (the Vue port of Radix +primitives) as the headless behavior layer under `Button`, `Checkbox`, +`Switch`, `RadioGroup`, `Dialog`, and `Select`. `reka-ui` is currently a +`peerDependency` of `@nychthemeron/library`, meaning every consumer of the +library has to install it too. + +Goal: drop `reka-ui` entirely — fewer dependencies for the library and its +consumers, no external headless-UI surface to track for breaking changes — +by hand-rolling the behavior each component needs directly in this repo. + +This is an internal implementation swap. No component's public props, emits, +slots, or CSS classes change; Storybook stories in `packages/playground` +should work unmodified against the new implementations. + +## Scope + +Files currently importing from `reka-ui` (24 total, `packages/library/src/components/ui/`): + +- `button/Button.vue` +- `checkbox/Checkbox.vue` +- `switch/Switch.vue` +- `radio-group/RadioGroup.vue`, `radio-group/RadioGroupItem.vue` +- `dialog/*.vue` (10 files: Dialog, DialogTrigger, DialogContent, + DialogOverlay, DialogClose, DialogTitle, DialogDescription, DialogFooter, + DialogScrollContent, plus the already-presentational DialogHeader which + doesn't import reka-ui but lives in the same family) +- `select/*.vue` (11 files: Select, SelectTrigger, SelectValue, + SelectContent, SelectItem, SelectItemText, SelectLabel, SelectGroup, + SelectSeparator, SelectScrollUpButton, SelectScrollDownButton) + +## Key decisions + +**Dialog is built as a fully custom `div` + `Teleport`, not on the native +`` element.** Native `` would give focus-trap/ESC/top-layer +behavior for free, but its `close()` removes the element from the render +tree immediately, which fights the fade/zoom exit animation the current +design already has (`data-closed:animate-out data-closed:fade-out-0 +data-closed:zoom-out-95`). A ``-driven custom implementation +lets the leave animation play out naturally before unmount. + +**Select is a fully custom listbox, not a native `` cannot reproduce — its +dropdown is unstyleable OS chrome in every browser. Given the existing +Storybook stories only exercise flat item lists (no search/multi-select), +a hand-rolled listbox with manual keyboard nav and simple fixed-position +placement is sufficient and preserves the current visual design exactly. + +**No positioning library.** Select's popover placement is computed manually +via `getBoundingClientRect()` (flip above the trigger when there's +insufficient room below, recalculate on scroll/resize while open) rather +than pulling in `@floating-ui/dom` or using CSS anchor positioning (the +latter is Chrome/Edge-only today, not viable for full browser support). + +## Shared internal primitives + +New composables/utilities under `packages/library/src/lib/` (or similar), +used across the affected components instead of one large headless library: + +- **`Slot` (asChild helper)** — clones the single child VNode and merges + the parent's props/listeners onto it. Needed for `Button`'s `asChild`, + `DialogTrigger as-child`, `DialogClose as-child`, `SelectIcon as-child`. + This is the one piece of VNode-cloning plumbing kept in the codebase. +- **Portal** — Vue's built-in `` directly; no wrapper + component needed. +- **`useFocusTrap`** — on activation, moves focus into a container, cycles + Tab/Shift+Tab within its focusable descendants, restores focus to the + previously-focused element on deactivation. Used by Dialog and Select. +- **`useDismissableLayer`** — closes on Escape keydown and on + click/pointerdown outside a given container. Used by Dialog (overlay + click + Escape) and Select (outside click + Escape). +- **`useRovingFocus`** — arrow-key navigation across a set of items with a + single tab stop (only the active item is in the tab order). Used by + `RadioGroup`. +- **`usePopoverPosition`** — computes `fixed` top/left for a floating + element relative to a trigger's `getBoundingClientRect()`, flips + above/below based on available viewport space, recalculates on + scroll/resize while open. Used by Select's listbox. +- **Scroll lock** — reuse `@vueuse/core`'s `useScrollLock` (already a + dependency) rather than writing a new composable. Applied to `` + while Dialog or Select is open. + +## Per-component plan + +- **Button** — replace `Primitive` with `` (default + `'button'`), using the new `Slot` helper when `asChild` is true. + +- **Checkbox / Switch** — reka's `CheckboxRoot`/`SwitchRoot` currently + render as a `