Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
# OpenSpec Change: enterprise-react-ui-theme-dracula
- **Change ID**: enterprise-react-ui-theme-dracula
- **Package**: `@suaveplan/react-ui-theme-dracula` (new)
- **Category**: react
- **Target path**: `packages/react/react-ui-theme-dracula`
- **Status**: PROPOSED
- **Date**: 2026-05-05
- **Reference design**: `openspec/cross-cutting-designs/2026-05-05-react-ui-react-i18n-react-forms.md` §4 (theme plugin contract), §11 (`TranslationKey` augmentation)
- **Prereqs (one SHIPPED, one in-flight)**:
- `enterprise-react-i18n` shipped 2026-05-05 — archived at `openspec/archive/2026-05-05-enterprise-react-i18n/`; capability spec at `openspec/specs/react-i18n/spec.md`. This change consumes `TranslationKey` to brand the theme's `labelKey` and contributes `react-ui-theme-dracula.label` to `ContributedTranslationKeys` via TypeScript module augmentation in 5 locales (en-GB, zh-CN, ar-AE, cy-GB, ru-RU).
- `enterprise-react-ui-v2` (sibling, in-flight) — provides the `ThemePlugin<TName>` contract, the `ThemeTokenSet` / `MotionTokenSet` / `ThemableVariants` shapes the theme implements, and the `useThemedVariants` hook that the example variant overrides exercise.
---
## Why
`@suaveplan/react-ui` ships three built-in themes — `lightTheme`, `darkTheme`, `highContrastTheme` — that exercise the `ThemePlugin` contract from inside the package. That is necessary, but not sufficient: a contract that has only ever been satisfied by code in the same package as the contract itself is not proven to work for the consumers it was designed for. External theme authors must be able to ship a separate, publishable npm package that:
- declares `@suaveplan/react-ui` as a peer dependency only,
- registers itself with `<ThemeProvider>` without modifying any source inside `@suaveplan/react-ui`,
- contributes its own translation keys via `TranslationKey` module augmentation without modifying `@suaveplan/react-i18n`,
- exercises both override-existing-variants AND brand-new-variant paths of `variantOverrides` (so the type-safety trade-off documented in master design §4.2 is validated by a real consumer, not by a synthetic test fixture inside `react-ui`).
This package exists for that proof. It is the first external `ThemePlugin<TName>` in the ecosystem, and it gates the rollout of every future theme package (brutalist, solarized, monokai, consumer brand themes). If this package cannot be authored cleanly against the public surface, the contract is wrong and must be fixed before any other theme ships.
The Dracula palette was chosen because it is widely recognised, has a published reference (`https://draculatheme.com/contribute`), and its high-saturation purples and pinks meaningfully exercise the WCAG AA contrast requirement on text-foreground pairs (where a less-saturated palette would not surface a contrast regression).
---
## Current-state audit (genesis)
Package does not exist on genesis. No prior implementation in `../suaveplan/`. This is a genesis-original package authored against the master design — there is no upstream parity to preserve.
`@suaveplan/react-ui` itself is not yet built on genesis (it lands in Wave 2 of the master design build order); this change sits in Wave 6 (example pack + theme) and lands after the chassis, the three built-in themes, and at least the foundation component batch (Button, Card) are green at 100% coverage.
---
## Available workspace deps on genesis
| Package | Range | Role |
|---|---|---|
| `@suaveplan/react-ui` | `workspace:^` (peer) | Provides the `ThemePlugin<TName>` contract, `ThemeTokenSet`, `MotionTokenSet`, `ThemableVariants` augmentation slot, and the `<ThemeProvider>` consumer |
| `@suaveplan/react-i18n` | `workspace:^` (peer) | Provides the `TranslationKey` brand and the `ContributedTranslationKeys` augmentation slot for `react-ui-theme-dracula.label` |
| `@suaveplan/types` | `workspace:^` | Shared option / contract types |
| `tslib` | `catalog:build` | Helper runtime for downlevel emit |
External (catalog): none beyond `tslib`. Themes are CSS-variable + variant-override only — no Radix, no React DOM primitives, no animation libraries. The Tailwind preset is a plain object literal and does not pull `tailwindcss` as a runtime dep (consumers already have it).
Peer deps stay peer (not listed in `dependencies`) so the consumer's installed `@suaveplan/react-ui` and `@suaveplan/react-i18n` versions are authoritative — exactly the integration scenario the package is built to prove.
---
## Public API surface
```ts
import {
draculaTheme,
draculaCssVariables,
draculaTailwindPreset,
} from '@suaveplan/react-ui-theme-dracula';
// Side-effect import contributes 'react-ui-theme-dracula.label' into ContributedTranslationKeys.
import '@suaveplan/react-ui-theme-dracula';
```
| Export | Shape | Purpose |
|---|---|---|
| `draculaTheme` | `ThemePlugin<'dracula'>` | The plugin object passed to `<ThemeProvider themes={[..., draculaTheme]}>` |
| `draculaCssVariables` | `ThemeTokenSet` | Raw token export so consumers can read individual tokens (e.g. recharts palette) without going through `useThemeTokens` |
| `draculaTailwindPreset` | `TailwindPreset` | Optional Tailwind preset registering the dracula colour scale + brand-new size token |
| `./styles/dracula.css` | CSS file | `@layer base { [data-theme='dracula'] { --background: ...; ... } }` for non-Tailwind / SSR-fallback consumption |
| `./translations/<locale>.json` × 5 | JSON | Per-locale theme label, registered via the package's `onRegister` hook |
| `./translations/augmentation.d.ts` | Declaration file | Augments `ContributedTranslationKeys` with `'react-ui-theme-dracula.label'` |
The package root (`src/index.ts`) uses explicit named re-exports (no `export *`, per CLAUDE.md §10).
---
## Design decisions
### 1. Implements the public `ThemePlugin<'dracula'>` from `@suaveplan/react-ui/theme`
No private surface, no internal API. The package consumes the same exports any third-party theme would. If `react-ui` needs to expose something to make this work, the gap is reported back to `enterprise-react-ui-v2` and fixed at the contract level — never papered over with a private reach-into.
### 2. CSS variables tuned for WCAG AA
The canonical Dracula palette (`https://draculatheme.com/contribute`) is the starting point. Each foreground/background pair is contrast-checked against WCAG AA (4.5:1 for normal text, 3:1 for large text and UI components) and adjusted where the canonical pair fails. The adjustments are documented inline in `dracula.ts` so consumers can see which pairs were tuned and why. The `highContrastTheme` shipped with `react-ui` raises the bar to AAA; this theme stays at AA so it remains visually faithful to Dracula while still being accessible.
### 3. `variantOverrides` exercises both axes of the contract
- **Override an existing variant.** `Button.variants.variant.default` and `Card.variants.variant.default` are overridden with Dracula purple shadows + the canonical Dracula border palette. This validates that the `useThemedVariants` merge correctly replaces the base cva entry without leaking base classes through `tailwind-merge`.
- **Add a brand-new variant.** `Button.variants.size.xl` is added (`h-14 px-10 text-xl`). This validates the type-safety trade-off documented in master design §4.2: the default-typed `useThemedVariants('Button')` call rejects `size: 'xl'`, and only the narrowed `useThemedVariants<'dracula', 'Button'>('Button')` form accepts it. A spec test in `react-ui` (added by `enterprise-react-ui-v2`) pins this typing behaviour against the dracula package as the consumer.
### 4. `motionTokens.respectReducedMotion: 'soft'`
The built-in themes default to `'strict'` (collapse to `0ms` under `prefers-reduced-motion: reduce`). Dracula opts into `'soft'` (halve durations, disable spring, keep crossfades) to validate that the non-default branch of the motion handler in `<ThemeProvider>` actually runs against a real theme. This is verified by an integration test that mocks the media query and asserts the resolved durations.
### 5. Per-locale translations contribute via module augmentation
The package ships exactly one translation key — `react-ui-theme-dracula.label` (the human-readable theme name shown in `<ThemeSwitcher>`). The key is contributed via `declare module '@suaveplan/react-i18n' { interface ContributedTranslationKeys { 'react-ui-theme-dracula.label': true; } }` in `src/translations/augmentation.d.ts`, generated from `translations/en-GB.json`. The package's `onRegister` lifecycle hook calls `i18n.addResourceBundle('react-ui-theme-dracula', translations[locale])` for the active locale; `onUnregister` removes it. This validates that `ContributedTranslationKeys` is augmentable from a leaf package without `react-i18n` ever importing the leaf — the architecture invariant in master design §11.2.
### 6. Locale set matches the bundled five
`en-GB`, `zh-CN`, `ar-AE`, `cy-GB`, `ru-RU` — same as `react-ui`. A theme that ships only `en-GB` would silently fall back to English in the other four locales, which is the behaviour we want to verify works end-to-end. Welsh's six plural categories do not apply to a single label key, but including `cy-GB` exercises the locale-resolution path under a less-common BCP-47 tag.
### 7. Zero Radix dependencies
Themes are passive: CSS variables, cva variant overrides, motion tokens, font stacks. They never reach into Radix, never render React components, never own state. The package's `package.json` `dependencies` is empty save for `tslib`; `devDependencies` carries vitest, the testing wrappers, and the build chain. Verified by a contract test (`tests/no-radix-deps.test.ts`) that parses `package.json` and asserts no `@radix-ui/*` key appears in any dep block.
---
## What changes
### ADDED
- `packages/react/react-ui-theme-dracula/package.json` — version `0.1.0`, peer-dep `@suaveplan/react-ui` and `@suaveplan/react-i18n`, subpath exports for `./theme/dracula`, `./tailwind-preset`, `./styles/dracula.css`, `./translations/<locale>`.
- `packages/react/react-ui-theme-dracula/tsconfig.json` — extends `@suaveplan/typescript-config/library-browser`.
- `packages/react/react-ui-theme-dracula/vitest.config.ts` — `createTestConfig()` with `pool: 'forks'`, istanbul coverage at 100/100/100/100.
- `packages/react/react-ui-theme-dracula/README.md` — package overview, install snippet, three usage examples (basic registration, narrowed variant call, custom Tailwind preset extension), modules table linking to every co-located `.md`.
- `packages/react/react-ui-theme-dracula/src/index.ts` — explicit named re-exports for `draculaTheme`, `draculaCssVariables`, `draculaTailwindPreset` plus side-effect import of `./translations/index.js` (the augmentation barrel).
- `src/theme/dracula.ts` + `dracula.md` + `dracula.test.ts` — `draculaTheme` and `draculaCssVariables` constructors, palette definitions, contrast-check inline comments.
- `src/theme/variant-overrides/button.ts` + `button.md` + `button.test.ts` — Button variant overrides including the brand-new `size: 'xl'` entry.
- `src/theme/variant-overrides/card.ts` + `card.md` + `card.test.ts` — Card variant overrides (override-only path).
- `src/theme/motion-tokens.ts` + `motion-tokens.md` + `motion-tokens.test.ts` — Dracula motion token set with `respectReducedMotion: 'soft'`.
- `src/tailwind-preset/tailwind-preset.ts` + `tailwind-preset.md` + `tailwind-preset.test.ts` — Tailwind preset module including the dracula colour scale + xl size token.
- `src/styles/dracula.css` — CSS file declaring the `[data-theme='dracula']` block for non-Tailwind consumers.
- `src/translations/<locale>.json` × 5 — per-locale theme label.
- `src/translations/augmentation.d.ts` — augments `ContributedTranslationKeys`.
- `src/translations/index.ts` — runtime registration helpers + the side-effect-only import of `augmentation.d.ts`.
- `src/translations/index.md` + `src/translations/index.test.ts` — register / unregister coverage.
- `tests/contract.test.tsx` — registers `draculaTheme` against `<ThemeProvider>`, switches to it, asserts `data-theme='dracula'` is set on the root, asserts CSS variables resolve to the dracula palette, asserts narrowed `useThemedVariants<'dracula', 'Button'>('Button')({ size: 'xl' })` produces a non-empty class string.
- `tests/no-radix-deps.test.ts` — parses `package.json`, asserts no Radix or React-DOM dep.
- `tests/wcag-aa.test.ts` — runs the documented foreground/background pairs through a contrast helper, asserts every pair clears AA.
- `apps/storybook` story addition (in `react-ui`'s Storybook): `Button.stories.tsx` gains a `WithTheme: dracula` story rendered under `<ReactUiProvider themes={[lightTheme, draculaTheme]} defaultTheme='dracula'>`.
---
## Parity Checklist
N/A — this is a genesis-original package. There is no upstream `@suaveplan/react-ui-theme-dracula` (nor any equivalent) on `../suaveplan/`. The master design (`openspec/cross-cutting-designs/2026-05-05-react-ui-react-i18n-react-forms.md`) is the canonical specification; this proposal cites it as the reference rather than a parity table.
---
## Verification approach
Every requirement in `specs/react-ui-themes/spec.md` carries a `**Verified by:**` line pointing at the test file + name that pins it. Concretely:
- **Theme name and labelKey** — `src/theme/dracula.test.ts`::"draculaTheme.name is 'dracula' and labelKey is the augmented translation key"
- **`ThemePlugin<'dracula'>` shape conformance** — `src/theme/dracula.test.ts`::"draculaTheme satisfies ThemePlugin<'dracula'> at compile time and at runtime"
- **WCAG AA on every documented pair** — `tests/wcag-aa.test.ts`::"every documented foreground/background pair clears WCAG AA contrast"
- **`variantOverrides` overrides existing Button/Card variants** — `src/theme/variant-overrides/button.test.ts`::"override of Button variant.default merges through useThemedVariants without leaking base classes" + the equivalent `card.test.ts` case
- **`variantOverrides` adds a brand-new variant** — `src/theme/variant-overrides/button.test.ts`::"Button.size.xl is added by dracula and resolved by narrowed useThemedVariants call"
- **`motionTokens.respectReducedMotion: 'soft'` runs the soft branch** — `src/theme/motion-tokens.test.ts`::"under prefers-reduced-motion: reduce, durations halve and spring disables but crossfades remain"
- **Translations contribute via module augmentation** — `src/translations/index.test.ts`::"registering dracula contributes react-ui-theme-dracula.label to the active i18n bundle for every supported locale" + the type-level `expectTypeOf` assertion that `'react-ui-theme-dracula.label'` is assignable to `TranslationKey` after the side-effect import
- **Zero Radix / React-DOM deps** — `tests/no-radix-deps.test.ts`::"package.json declares no @radix-ui/* or react-dom dependency in any dep block"
- **End-to-end registration with `<ThemeProvider>`** — `tests/contract.test.tsx`::"registering draculaTheme switches data-theme, resolves CSS variables, and exposes the narrowed variant call"
All test files import runner primitives from `@suaveplan/testing/runner` per CLAUDE.md §11.
---
## Telemetry
None. Themes are passive: they declare CSS variables, cva overrides, and motion tokens. They do not emit spans, counters, or gauges. The active-theme metric (`ui.theme.active{name='dracula'}`) is owned by `<ThemeProvider>` in `@suaveplan/react-ui`, not by this package — a theme being instrumented from outside is part of what makes themes safe to author.
---
## Out of scope
- **Animations beyond motion-token swaps.** The Dracula community ships various keyframe animations (glow pulses, gradient shifts) — these belong in a follow-up `@suaveplan/react-ui-theme-dracula-fx` package layered on top, not in the base theme. The base theme stays pure tokens + variant overrides so its scope mirrors the contract surface exactly.
- **Dracula-specific component variants beyond Button and Card.** The two components are sufficient to exercise both axes of `variantOverrides` (override + brand-new). Adding overrides for the remaining ~55 components inflates surface area without adding contract coverage; consumers who want Dracula-styled DataTable / Calendar / Sidebar can author them in their own app or contribute follow-up overrides via PR.
- **A "dark Dracula" / "light Dracula" pair.** Dracula is a single palette by design. A future theme package can ship the inverted palette under a different name.
- **Sigstore / SLSA provenance for the theme bundle.** The `@suaveplan/slsa` package is available, but theme packages do not yet have a documented provenance flow; deferred to whichever change first wires SLSA across `react-ui-*` siblings.
---
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/react-ui-theme-dracula` to enumerate.
This package is a leaf — nothing in the genesis monorepo currently depends on it. The dependents list will be exactly zero on landing:
- No dependents (verified by `bunx turbo ls --filter=...@suaveplan/react-ui-theme-dracula`).
The first downstream consumer will be `@suaveplan/react-ui`'s Storybook (which adds a `WithTheme: dracula` story for Button) — that change ships in the same wave as this one (Wave 6 of the master design) and is enumerated in the `## What changes` block above. Storybook is not a workspace package, so it does not appear in the dependents enumeration.

View File

@@ -0,0 +1,61 @@
# react-ui-themes spec delta — enterprise-react-ui-theme-dracula
## ADDED Requirements
### Requirement: Dracula theme exports a `ThemePlugin<'dracula'>`
The `@suaveplan/react-ui-theme-dracula` package SHALL export a `draculaTheme` value satisfying `ThemePlugin<'dracula'>` from `@suaveplan/react-ui/theme`. The `name` field SHALL be the literal string `'dracula'`. The `labelKey` field SHALL be the branded `TranslationKey` `'react-ui-theme-dracula.label'` (NOT a plain string literal — typing failures here are compile-time errors, never runtime warnings).
The plugin object SHALL be a single, shared, frozen value — re-importing it across modules SHALL return the same reference so theme equality checks inside `<ThemeProvider>` short-circuit correctly.
**Verified by:** `src/theme/dracula.test.ts`::"draculaTheme.name is 'dracula' and labelKey is the augmented translation key" AND "draculaTheme satisfies ThemePlugin<'dracula'> at compile time and at runtime".
### Requirement: CSS variables clear WCAG AA on every documented pair
`draculaTheme.cssVariables: ThemeTokenSet` SHALL declare every token required by `ThemeTokenSet` (background, foreground, primary + foreground, secondary + foreground, muted + foreground, accent + foreground, destructive + foreground, border, input, ring, radius, chart 1–5, sidebar bundle).
Every foreground/background pair SHALL clear WCAG AA contrast — 4.5:1 for normal text and 3:1 for large text and UI components. Where the canonical Dracula palette pair fails AA, the package SHALL tune the foreground or background and inline-document the adjustment delta in `src/theme/dracula.ts` next to the affected token.
**Verified by:** `tests/wcag-aa.test.ts`::"every documented foreground/background pair clears WCAG AA contrast".
### Requirement: `variantOverrides` exercises both override AND brand-new variant
`draculaTheme.variantOverrides` SHALL contain at minimum:
1. An override of `Button.variants.variant.default` — a key that already exists in the base Button cva config. The merged result SHALL replace the base classes via `useThemedVariants('Button')` without leaking the original Dracula-incompatible base classes through `tailwind-merge`.
2. An override of `Card.variants.variant.default` — same override-only path on a second component to confirm the merge is not Button-specific.
3. A brand-new entry `Button.variants.size.xl` — a key that does NOT exist in the base Button cva config. The narrowed call `useThemedVariants<'dracula', 'Button'>('Button')({ size: 'xl' })` SHALL resolve to the dracula-defined classes; the un-narrowed call `useThemedVariants('Button')({ size: 'xl' })` SHALL be a TypeScript compile-time error.
The narrowed call's runtime fallback when the active theme is NOT dracula SHALL be the documented behaviour from master design §4.2 (the call still works but the resulting class string is empty / undefined for `xl`); this package SHALL NOT attempt to throw on theme mismatch — that is the consumer's responsibility per the §4.2 trade-off.
**Verified by:** `src/theme/variant-overrides/button.test.ts`::"override of Button variant.default merges through useThemedVariants without leaking base classes" AND "Button.size.xl is added by dracula and resolved by narrowed useThemedVariants call" AND `src/theme/variant-overrides/card.test.ts`::"override of Card variant.default merges through useThemedVariants without leaking base classes".
### Requirement: `motionTokens.respectReducedMotion: 'soft'` runs the soft branch
`draculaTheme.motionTokens.respectReducedMotion` SHALL be `'soft'`. Under `prefers-reduced-motion: reduce`, the resolved durations SHALL be exactly half of the declared `duration.fast` / `duration.base` / `duration.slow` values, the spring easing SHALL be replaced with the linear `easing.inOut`, and crossfade transitions SHALL remain enabled. Under no media-query match, all values SHALL pass through unchanged.
**Verified by:** `src/theme/motion-tokens.test.ts`::"under prefers-reduced-motion: reduce, durations halve and spring disables but crossfades remain" AND "without the reduced-motion media-query match, all motion values pass through unchanged".
### Requirement: Translation key contributed via `ContributedTranslationKeys` augmentation
The package SHALL contribute the translation key `'react-ui-theme-dracula.label'` into `ContributedTranslationKeys` from `@suaveplan/react-i18n` via TypeScript module augmentation in `src/translations/augmentation.d.ts`. The augmentation SHALL be picked up by any consumer that imports the package's root entry, because the root entry side-effect-imports `./translations/index.js` which in turn references the augmentation file.
The package SHALL NOT modify, fork, or shadow `@suaveplan/react-i18n` itself — `ContributedTranslationKeys` SHALL stay a public augmentation slot on `react-i18n`'s side. The architecture invariant from master design §11.2 (no backwards dependency from `react-i18n` to leaf packages) SHALL hold; verified by the existing `scripts/lint/check-no-backwards-deps.ts` gate from the `enterprise-react-i18n` change.
The package SHALL ship one translation file per locale in the bundled five (`en-GB`, `zh-CN`, `ar-AE`, `cy-GB`, `ru-RU`), each containing the single key `react-ui-theme-dracula.label`. The `onRegister` lifecycle hook SHALL call `i18n.addResourceBundle('react-ui-theme-dracula', translations[locale])` for the active locale; the `onUnregister` hook SHALL remove the bundle.
**Verified by:** `src/translations/index.test.ts`::"registering dracula contributes react-ui-theme-dracula.label to the active i18n bundle for every supported locale" AND "unregistering dracula removes the bundle without leaking into other themes" AND the type-level `expectTypeOf<TranslationKey>().toMatchTypeOf<'react-ui-theme-dracula.label'>()` assertion in the same file.
### Requirement: Zero Radix and zero React-DOM dependencies
The package SHALL declare no `@radix-ui/*` package and no `react-dom` package in any of `dependencies`, `devDependencies`, or `peerDependencies`. Themes are passive — they declare CSS variables, cva variant overrides, and motion tokens. They never render React components, never reach into Radix primitives, and never own component state.
**Verified by:** `tests/no-radix-deps.test.ts`::"package.json declares no @radix-ui/* or react-dom dependency in any dep block".
### Requirement: Registers cleanly with `<ThemeProvider>` end-to-end
Wrapping a tree in `<ThemeProvider themes={[lightTheme, draculaTheme]} defaultTheme='dracula'>` SHALL set `data-theme='dracula'` on the registered theme attribute target, resolve `getComputedStyle(...).getPropertyValue('--background')` to the Dracula background token, and expose the narrowed `useThemedVariants<'dracula', 'Button'>('Button')` call.
This requirement is the integration acceptance criterion for the entire change — it is what proves the contract works for an external theme package without modifications to `@suaveplan/react-ui` source.
**Verified by:** `tests/contract.test.tsx`::"registering draculaTheme switches data-theme, resolves CSS variables, and exposes the narrowed variant call".

View File

@@ -0,0 +1,86 @@
# tasks — enterprise-react-ui-theme-dracula
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Package scaffolding
- [ ] 1.1 Create `packages/react/react-ui-theme-dracula/` directory.
- [ ] 1.2 Author `package.json` at version `0.1.0` with peer-deps `@suaveplan/react-ui` and `@suaveplan/react-i18n` (both `workspace:^`), `tslib` from `catalog:build` as the only runtime dep, devDeps for the testing wrappers + biome + vitest from catalog refs. `"sideEffects": ["./src/translations/index.js", "./styles/dracula.css"]` so the side-effect import that registers translations and the CSS file are not tree-shaken away.
- [ ] 1.3 Author `tsconfig.json` extending `@suaveplan/typescript-config/library-browser`.
- [ ] 1.4 Author `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config`, istanbul provider, thresholds 100/100/100/100.
- [ ] 1.5 Author the subpath `exports` map: `.`, `./theme/dracula`, `./tailwind-preset`, `./styles/dracula.css`, `./translations/<locale>` for each of the five locales.
- [ ] 1.6 Confirm subpath count matches the §6 / §13 step-8 helper output.
## 2. Palette + ThemePlugin
- [ ] 2.1 Create `src/theme/dracula.ts` with the canonical Dracula palette as `draculaCssVariables: ThemeTokenSet`. Inline-comment every foreground/background pair with its measured contrast ratio.
- [ ] 2.2 Tune any pair below WCAG AA (4.5:1 normal text / 3:1 large + UI) and document the adjustment delta in the same comment.
- [ ] 2.3 Construct `draculaTheme: ThemePlugin<'dracula'>` with `name`, `labelKey: 'react-ui-theme-dracula.label'`, `cssVariables`, `tailwindPreset`, `variantOverrides`, `motionTokens`, `colorScheme: 'dark'`, `fontStack` (Dracula's recommended JetBrains Mono / Inter pairing).
- [ ] 2.4 Author `src/theme/dracula.md` with the six required sections (≥200 words excluding code blocks).
## 3. Variant overrides — both axes
- [ ] 3.1 Create `src/theme/variant-overrides/button.ts` overriding `Button.variants.variant.default` with the Dracula purple shadow and adding `Button.variants.size.xl` (`h-14 px-10 text-xl`).
- [ ] 3.2 Create `src/theme/variant-overrides/card.ts` overriding `Card.variants.variant.default` with Dracula border + foreground tokens (override-only path).
- [ ] 3.3 Author sibling `.md` files for each.
- [ ] 3.4 Author sibling `.test.ts` files; the Button test pins both the override merge AND the brand-new `size: 'xl'` resolution through a narrowed `useThemedVariants<'dracula', 'Button'>('Button')` call.
## 4. Motion tokens
- [ ] 4.1 Create `src/theme/motion-tokens.ts` with `respectReducedMotion: 'soft'`, halved durations under reduced-motion, spring disabled, crossfade easings preserved.
- [ ] 4.2 Author `motion-tokens.md`.
- [ ] 4.3 Author `motion-tokens.test.ts` mocking `matchMedia('(prefers-reduced-motion: reduce)')` and asserting the soft branch resolves the documented values.
## 5. Tailwind preset + CSS file
- [ ] 5.1 Create `src/tailwind-preset/tailwind-preset.ts` registering the Dracula colour scale (50–950) plus the brand-new `xl` size token. Plain object literal — no runtime tailwind import.
- [ ] 5.2 Author `tailwind-preset.md`.
- [ ] 5.3 Author `tailwind-preset.test.ts` asserting the preset shape.
- [ ] 5.4 Create `src/styles/dracula.css` declaring the `[data-theme='dracula']` block under `@layer base` for non-Tailwind / SSR-fallback consumers.
## 6. Translations × 5 locales
- [ ] 6.1 Create `src/translations/en-GB.json` with `{ "react-ui-theme-dracula.label": "Dracula" }`. (Source of truth; English keeps the brand name.)
- [ ] 6.2 Create `src/translations/zh-CN.json` (`"德古拉"` — confirm with native reviewer in PR).
- [ ] 6.3 Create `src/translations/ar-AE.json` (`"دراكولا"` — confirm with native reviewer in PR).
- [ ] 6.4 Create `src/translations/cy-GB.json` (`"Dracula"` — proper noun stays untranslated).
- [ ] 6.5 Create `src/translations/ru-RU.json` (`"Дракула"` — confirm with native reviewer in PR).
- [ ] 6.6 Create `src/translations/augmentation.d.ts` augmenting `ContributedTranslationKeys` with `'react-ui-theme-dracula.label': true`.
- [ ] 6.7 Create `src/translations/index.ts` exporting the runtime `register` / `unregister` helpers wired into `draculaTheme.onRegister` / `onUnregister`. Side-effect-imports `augmentation.d.ts`.
- [ ] 6.8 Author `src/translations/index.md`.
- [ ] 6.9 Author `src/translations/index.test.ts` covering register / unregister for all five locales + the type-level `expectTypeOf` assertion that `'react-ui-theme-dracula.label'` is assignable to `TranslationKey`.
## 7. Index + barrel
- [ ] 7.1 Create `src/index.ts` with explicit named re-exports — `draculaTheme`, `draculaCssVariables`, `draculaTailwindPreset` — plus the side-effect import of `./translations/index.js`. No `export *` (CLAUDE.md §10).
- [ ] 7.2 Verify zero `export *` matches inside `src/<feature>/index.ts` with the §13 step-4a grep.
## 8. Contract tests
- [ ] 8.1 Create `tests/contract.test.tsx`: render `<ThemeProvider themes={[draculaTheme]} defaultTheme='dracula'><div data-testid='probe' /></ThemeProvider>`, assert `data-theme='dracula'` on the root, assert `getComputedStyle` resolves `--background` to the dracula token, assert `useThemedVariants<'dracula', 'Button'>('Button')({ size: 'xl' })` returns a non-empty class string.
- [ ] 8.2 Create `tests/no-radix-deps.test.ts`: parse `package.json`, assert no `@radix-ui/*` or `react-dom` key in any dep block.
- [ ] 8.3 Create `tests/wcag-aa.test.ts`: iterate the documented foreground/background pairs, run them through a contrast-ratio helper, assert every pair clears AA.
## 9. Storybook integration in `react-ui`
- [ ] 9.1 In `packages/react/react-ui/src/components/button/button.stories.tsx`, add a `WithTheme: dracula` story that wraps `<Button>` in `<ReactUiProvider themes={[lightTheme, draculaTheme]} defaultTheme='dracula'>` (importing `draculaTheme` as a devDep). Story import is dev-only so it does not introduce a runtime cycle.
## 10. README + docs sweep
- [ ] 10.1 Author `packages/react/react-ui-theme-dracula/README.md` with package name, description, install (`bun add @suaveplan/react-ui-theme-dracula`), basic usage, narrowed-variant usage, custom Tailwind extension example, modules table linking every co-located `.md`.
- [ ] 10.2 Run `bun run docs:check` and confirm all sibling `.md` files exist and meet the ≥200-word floor.
## 11. Pre-publish gate
- [ ] 11.1 `bunx turbo lint typecheck test --filter=@suaveplan/react-ui-theme-dracula` green.
- [ ] 11.2 `bunx turbo test:coverage --filter=@suaveplan/react-ui-theme-dracula` at 100/100/100/100.
- [ ] 11.3 `bunx turbo build --filter=@suaveplan/react-ui-theme-dracula` green.
- [ ] 11.4 `bun scripts/lint/check-test-runner-imports.ts` green for this package (zero direct `vitest` / `bun:test` imports — CLAUDE.md §11).
- [ ] 11.5 §13 step-4 banned-pattern grep returns zero matches.
- [ ] 11.6 §13 step-9 secret scan returns zero matches.
- [ ] 11.7 Refresh `MANIFEST.md` via `bun scripts/folder-reorg/generate-manifest.ts` and refresh `MISSING.yml` via `bun scripts/generate-missing.ts`. Update `STATUS.md` and `SUMMARY.md` per §13b.
- [ ] 11.8 `bunx markdownlint-cli2 --fix` then a clean second pass with no `--fix`.