Add design spec for PrimeVue → shadcn-vue migration

Captures the architecture decisions from brainstorming: reka-ui/Tailwind v4/cva
replace PrimeVue's runtime, ThemeEngine is replaced by shadcn-vue's own cn()
utility, and compound components adopt shadcn-vue's idiomatic API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Matthew McPeak 2026-07-10 14:55:02 -04:00
parent 8751c150cd
commit c2c279984a

View file

@ -0,0 +1,100 @@
# PrimeVue → shadcn-vue Migration Design
**Date:** 2026-07-10
**Project:** nychthemeron
**Status:** Approved
## Overview
Replace PrimeVue as the underlying component primitive layer with shadcn-vue (Reka UI primitives + Tailwind CSS v4 + `class-variance-authority`), while preserving nychthemeron's identity: the Hades/Apollo custom-property theme system, the `@nychthemeron/library` installable-package distribution model, and the `createNychthemeron()` global-registration plugin.
## Goals
- Remove `primevue` as a dependency; components are built on Reka UI primitives instead.
- Adopt Tailwind CSS v4 and `cva`-based variants as the styling mechanism, replacing hand-written per-component CSS.
- Preserve the Hades/Apollo design tokens (CSS custom properties) as the source of truth for color/typography, mapped onto Tailwind's expected theme slots.
- Replace the generic `ThemeEngine`/`Themeable`/pt-injection wrapping layer with the much smaller `cn()` utility (`clsx` + `tailwind-merge`) that shadcn-vue itself generates, plus shared `cva` variant configs where genuinely duplicated.
- Adopt shadcn-vue's idiomatic compound-component API for multi-part components (Dialog, Select, Card), even though this breaks the current single-component prop/slot API.
- Keep the library an installable workspace package (no shadcn-vue CLI/registry distribution model for consumers) — the CLI is used once, at authoring time, to scaffold component source into `packages/library`.
## Non-goals
- No change to the monorepo structure (`packages/library`, `packages/playground`), the bun workspace setup, or the "no npm publishing" distribution model.
- No redesign of the Hades/Apollo color palette or typography — token *values* stay the same; only how they're wired into the styling layer changes.
- No visual-regression parity requirement with the old PrimeVue-based rendering — some spacing/detail differences are expected and acceptable since components are being rebuilt from shadcn-vue's scaffolds.
## Architecture
### Dependencies
- **Removed:** `primevue` (peer dependency of `packages/library`; dependency of `packages/playground`).
- **Added (library peer deps):** `vue`, `reka-ui`.
- **Added (library deps/devDeps):** `tailwindcss`, `@tailwindcss/vite`, `class-variance-authority`, `clsx`, `tailwind-merge`, plus whatever icon package shadcn-vue's scaffolds default to (e.g. `lucide-vue-next`) if generated components reference it.
- shadcn-vue itself is never a runtime dependency — it is a one-time code generator (`npx shadcn-vue@latest init` / `add <component>`) invoked during implementation to scaffold component source directly into `packages/library/src/components`. Generated source is then adapted (token wiring, extra variants, our custom extensions) and owned in-repo like any other component.
### Styling & theming
- Tailwind v4, CSS-first configuration (no `tailwind.config.ts`); Tailwind is imported via `@import "tailwindcss"` in the library's CSS entry point, using the Vite plugin (`@tailwindcss/vite`) in both `packages/library/vite.config.ts` and `packages/playground/vite.config.ts` / Storybook Vite config.
- A `@theme` block maps existing Hades/Apollo custom properties onto the canonical color slots shadcn-vue's generated `cva` variants expect:
- `--color-primary: var(--primary)`, `--color-primary-foreground: var(--primary-fg)`
- `--color-secondary: var(--neutral)`, `--color-secondary-foreground: var(--neutral-fg)`
- `--color-destructive: var(--danger)`, `--color-destructive-foreground: var(--danger-fg)`
- `--color-background: var(--surface-0)`, `--color-foreground: var(--text-body)`
- `--color-card: var(--surface-1)`, `--color-card-foreground: var(--text-body)`
- `--color-popover: var(--surface-2)`, `--color-popover-foreground: var(--text-body)`
- `--color-border: var(--border)`, `--color-input: var(--border)`, `--color-ring: var(--primary)`
- Additional semantic slots for nychthemeron's 5-way severity system beyond shadcn's stock set: `--color-info`, `--color-info-foreground`, `--color-success`, `--color-success-foreground`, `--color-warning`, `--color-warning-foreground` (all mapped from existing `--info`/`--success`/`--warn` tokens and their `-fg` counterparts).
- Theme switching mechanism is unchanged: `[data-theme="hades"]` / `[data-theme="apollo"]` selectors on `<html>` continue to define the underlying custom properties; Tailwind utilities simply read through `var()` at paint time, so the Storybook theme toolbar keeps working without modification.
- Global concerns (font-family tokens, base box-sizing reset, `:focus-visible` ring, `:disabled` opacity) remain in one small global CSS file, imported once. All per-component layout/spacing/color styling that used to live in `nychthemeron.css`/`theme.css` moves into Tailwind utility classes and `cva` variants inside each component's own `.vue` file — the shadcn-vue idiom.
- `packages/library` exports (`./style`, `./theme`) keep the same public paths; the files they point at change from hand-written CSS to the new Tailwind-entry CSS.
### Engine replacement
- `packages/library/src/engine/` (`ThemeEngine`, `Themeable`, `LibComponent`, `Key`, `Props`, pt-injection logic) is deleted in full.
- Its replacement is the CLI-generated `src/lib/utils.ts` exporting `cn()` (`clsx` + `tailwind-merge`), used inside every component for merging caller-provided `class` props with internal variant classes.
- Where a color/severity mapping is genuinely shared across components (e.g. the 5-way severity set used by both `Badge` and `Alert`), factor it into one shared `cva` base (e.g. `src/lib/variants.ts`) rather than duplicating the variant list — but this is a targeted, small shared config, not a generic runtime wrapper.
- No generic "wrap any component and inject classes via props" mechanism is preserved; shadcn-vue's model (own the markup, write the classes directly) makes that unnecessary.
### Component mapping
| Current (PrimeVue-wrapped) | shadcn-vue CLI component(s) | Notes |
|---|---|---|
| `Button` | `button` | Single component; `cva` variant prop replaces `severity` (`primary/secondary/info/success/warning/danger`); loading→disabled behavior and the loading-spinner rendering are coded directly in the SFC. |
| `LoadingIcon` | *(none — custom SVG)* | Unchanged, re-exported as-is. |
| `InputText``Input` | `input` | Direct port. |
| `Textarea` | `textarea` | Direct port. |
| `Checkbox` + `CheckboxGroup` | `checkbox` | shadcn-vue has no stock `CheckboxGroup`; hand-build one using the same provide/inject pattern the CLI's own `radio-group` uses, so it matches the surrounding idiom. |
| `RadioButton` + `RadioButtonGroup` | `radio-group``RadioGroup`, `RadioGroupItem` | Direct CLI equivalent; naming changes to match. |
| `ToggleSwitch``Switch` | `switch` | Direct port, renamed. |
| `Select` | `select``Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem` | Compound API replaces the `options`/`optionLabel`/`optionValue` props; consumers write `<SelectItem>` children. |
| `Card` | `card``Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | `#subtitle` slot → `CardDescription`; header-image use case becomes a plain child placed before `CardHeader`. |
| `Dialog` | `dialog``Dialog`, `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogTitle`, `DialogDescription`, `DialogFooter`, `DialogClose` | Current `headerActions` (multiple header buttons, not just close) becomes extra children placed in `DialogHeader` alongside `DialogClose`. |
| `Message``Alert` | `alert``Alert`, `AlertTitle`, `AlertDescription` | Stock `Alert` has no close button; add a `closable` prop / optional close-button piece as our extension on top of the scaffolded component. |
| `Tag``Badge` | `badge` | Direct port; variants extended to cover the 5 severities (stock only has `default/secondary/destructive/outline`). |
All components keep the existing `Nych*` global-registration convention via `createNychthemeron()` — the installer gains additional entries for compound sub-components (e.g. `NychDialogTrigger`, `NychSelectItem`).
## Testing strategy
- `packages/library/tests/lib.spec.ts` (entirely PrimeVue-pt-injection-specific, using a `callSetup` helper that simulates the old engine's render function) is deleted and replaced with per-component test files that mount the real compound components via `@vue/test-utils` and assert rendered output/classes/`v-model` behavior/disabled state directly — no engine-internals testing since there's no engine.
- `packages/library/tests/engine.spec.ts` is deleted along with the engine it tests.
## Storybook / playground
- All 13 `.stories.ts` files are rewritten to the new compound API; `Select.stories.ts`, `Dialog.stories.ts`, and `Card.stories.ts` see the largest rewrites since their public shape changes most.
- `.storybook/preview.ts` drops the `app.use(PrimeVue, { unstyled: true })` setup call; the `@nychthemeron/library/theme` import path is kept but now resolves to Tailwind-built CSS instead of hand-written component CSS.
## Build order
1. **Foundation** — wire up Tailwind v4 (`@tailwindcss/vite` in both packages), write the `@theme` token-mapping block, run `shadcn-vue init` to scaffold `src/lib/utils.ts` and CLI config, delete `packages/library/src/engine` and the `primevue` dependency.
2. **Simple components** — Button, Input, Textarea, Switch, Badge, re-export LoadingIcon unchanged.
3. **Grouped/compound-lite** — Checkbox + hand-built CheckboxGroup, RadioGroup + RadioGroupItem, Card, Alert.
4. **Complex compound** — Select, Dialog.
5. **Cleanup** — rewrite all Storybook stories and library tests to match the new API, remove any remaining PrimeVue references from configs/CI.
## Success criteria
- `primevue` no longer appears in any `package.json`, config, or import across the repo.
- All 13 original components have a shadcn-vue-based equivalent, installable the same way as today (`import { X } from '@nychthemeron/library'`, registered via `createNychthemeron()`).
- Hades and Apollo themes both render correctly across every component with no regression in the token *values* used.
- `bun run test`, `bun run type-check`, and `bun run lint` all pass.
- Storybook (`bun run dev` in `packages/playground`) starts and every story renders without console errors, using the new compound APIs.