Files
suaveplan/openspec/changes/enterprise-react-ui-theme-dracula/specs/react-ui-themes/spec.md
2026-05-27 23:29:53 +01:00

62 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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".