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