Files
2026-05-27 23:29:53 +01:00

29 KiB
Raw Permalink Blame History

Tasks: enterprise-react-ui-v2

Change ID: enterprise-react-ui-v2 Package: @suaveplan/react-ui Target path: packages/react/react-ui Prerequisite changes (must be archived before this one ships): enterprise-react-i18n (amended with the LocaleDetector chain + ContributedTranslationKeys augmentation pattern) Prerequisite packages on Verdaccio: @suaveplan/react-i18n, @suaveplan/i18n (with i18n-storage-backends and i18n-completion folded in), @suaveplan/error, @suaveplan/events, @suaveplan/logger, @suaveplan/locale-data, @suaveplan/types Sibling changes (parallel work; coordinated archive order): enterprise-react-forms-v2, enterprise-react-ui-theme-dracula, enterprise-react-ui-pack-auth, react-ui-registry-deployment

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.

0. Supersession

  • 0.1 Mark openspec/changes/enterprise-react-ui/ as superseded with a one-line note in its proposal.md header pointing here. Move it to openspec/archive/<yyyy-mm-dd>-enterprise-react-ui-superseded/ only after this change archives (per AGENTS.md §13 archival is post-ship; pre-ship the legacy folder stays where it is so nobody picks it up by accident — the superseded note is enough).
  • 0.2 Update STATUS.md and SUMMARY.md to reflect @suaveplan/react-ui v2 in flight; explicitly note the legacy proposal is superseded so the parity-checklist tooling picks the right source.

1. Wave 0 — Spec authoring + spike

1.1 Spec authoring

  • 1.1.1 Read proposal.md and specs/react-ui/spec.md end-to-end
  • 1.1.2 Read openspec/cross-cutting-designs/2026-05-05-react-ui-react-i18n-react-forms.md (full document; this change owns §3, §4, §5, §6, §8, §10, §11, §12)
  • 1.1.3 Read openspec/AGENTS.md §0a (parity), §13 (pre-ship verification), §13a (behavior-change propagation), §14a (Affected dependents)
  • 1.1.4 Read CLAUDE.md §10 (no export * in non-root barrels) and §11 (test-runner imports via @suaveplan/testing/runner) — both are non-negotiable for every snippet in this change
  • 1.1.5 openspec change validate enterprise-react-ui-v2 exits with no errors

1.2 Wave 0 — Component-list reconciliation

  • 1.2.1 Run npx shadcn@latest search @shadcn from a clean checkout; capture the raw output
  • 1.2.2 Read upstream /mnt/s/web/suaveplan/packages/react/react-ui/src/components/ (57 entries) end-to-end; note every component the legacy proposal listed
  • 1.2.3 Diff the two lists; classify every entry as kept / renamed / split / dropped / added; record rationales for every drop
  • 1.2.4 Update the §"Component count — resolved here" table in proposal.md with the final list and lock the count before any Wave 3 code lands
  • 1.2.5 Reconcile the ## Parity Checklist table to reflect the final set; every row resolves to ✅ / 🚫 (with one-line justification) / ⬆️

1.3 Wave 0 — Theme typing spike

  • 1.3.1 Spike PR (openspec/changes/enterprise-react-ui-v2/spike-theme-typing/) implementing two real cva configs (Button + Card) under the §"Type-safety trade-off" pattern: useThemedVariants('Button') returns base variants, useThemedVariants<'dracula', 'Button'>('Button') allows brand-new variants
  • 1.3.2 Validate the typing ergonomics — write expectTypeOf assertions covering the four cells of (default-typed × narrowed) × (base-variant × theme-variant)
  • 1.3.3 Land the spike (or escalate a different mechanism) before Wave 3 starts — the cross-cutting design §16 row 2 makes this a Wave 0 deliverable, not a Wave 3 risk

2. Wave 2 — Scaffold + chassis

2.1 Package scaffold

  • 2.1.1 Create packages/react/react-ui/ with package.json at 0.1.0, tsconfig.lib.json extending @suaveplan/typescript-config/react-library.json, vitest.config.ts using createTestConfig() from @suaveplan/testing/config, vite.config.ts, tailwind.config.ts, playwright.config.ts, scripts (lint, typecheck, test, test:coverage, build, docs:check, e2e, registry:build, locales:check, locales:types, lint:no-hardcoded-strings, lint:translation-keys)
  • 2.1.2 Dependencies (workspace:^ for internal, catalog:<key> for external — never bare versions per AGENTS.md §1):
    • Internal: @suaveplan/react-i18n, @suaveplan/error, @suaveplan/events, @suaveplan/logger, @suaveplan/locale-data, @suaveplan/types, @suaveplan/i18n (transitive but listed explicitly so the MessageParser import path is reviewed)
    • External: every Radix primitive listed in proposal.md §"Available workspace deps"; class-variance-authority, clsx, cmdk, embla-carousel-react, input-otp, lucide-react, react-day-picker, react-hook-form, @hookform/resolvers, react-resizable-panels, recharts, sonner, tailwind-merge, tailwindcss, vaul, tslib, zod, @opentelemetry/api
  • 2.1.3 Peer dependencies: react ^18.0.0 || ^19.0.0, react-dom ^18.0.0 || ^19.0.0
  • 2.1.4 Dev dependencies: @suaveplan/testing workspace:^, @suaveplan/testing-dom workspace:^, @suaveplan/testing-e2e workspace:^, @testing-library/react, @types/react, @types/react-dom, react, react-dom, vitest, vite, vite-plugin-dts, fast-check, @vitest/coverage-istanbul, @axe-core/playwright, @storybook/react, @storybook/addon-a11y
  • 2.1.5 NO i18next, NO react-i18next, NO @suaveplan/react-translate — verified by grep before commit
  • 2.1.6 "sideEffects": ["**/*.css"] (CSS imports DO have side effects — Tailwind layers register globally)
  • 2.1.7 Subpath exports map: one exports entry per feature module (./provider/react-ui-provider/react-ui-provider, ./theme/theme-provider/theme-provider, ./theme/use-themed-variants/use-themed-variants, ./pack/pack-registry-provider/pack-registry-provider, ./pack/use-pack-component/use-pack-component, one per component, one per locale JSON, etc.) plus ./styles/global.css. AGENTS.md §13 step 8 verifies subpath count matches feature module count.

2.2 Errors and types

  • 2.2.1 Define ReactUiError and codes via defineCodes("REACT_UI", [...]): NO_PROVIDER, THEME_NOT_FOUND, THEME_TOKEN_MISSING, PACK_ALREADY_REGISTERED, PACK_NOT_FOUND, PACK_COMPONENT_NOT_FOUND, PACK_VERSION_MISMATCH, LOCALE_BUNDLE_LOAD_FAILED, NO_HARDCODED_STRINGS_VIOLATION
  • 2.2.2 Define types: ThemePlugin, ThemeTokenSet, MotionTokenSet, TailwindPreset, ThemableVariants (empty interface — augmented per-component), ComponentPack, PackComponents (empty interface — augmented per-pack), PackContext, ReactUiProviderProps, ThemeProviderProps, PackRegistryProviderProps, SupportedLocale, LocaleBundle, every hook return type
  • 2.2.3 react-ui-error.test.ts — code registry, error chain, JSON wire format
  • 2.2.4 Sibling .md for the error class

2.3 Locale bundles

  • 2.3.1 Author src/locales/en-GB.json as the source-of-truth bundle. Single namespace react-ui. Flat keys dot-namespaced by component. Cover every default user-perceivable string for every component planned in §"Component count". Use ICU plural syntax ({count, plural, =0 {No rows} one {# row} other {# rows}}) where component output is plural-sensitive.
  • 2.3.2 Translate to zh-CN.json (CJK), ar-AE.json (RTL — values only; dir="rtl" is set by <ThemeProvider> not the JSON), cy-GB.json (six plural categories — every plural key MUST exercise zero / one / two / few / many / other), ru-RU.json (Slavic plurals)
  • 2.3.3 src/locales/schema.ts — Zod LocaleBundleSchema derived from en-GB.json keys; every other locale validated against this schema at module load
  • 2.3.4 src/locales/index.ts — explicit named export of reactUiBundles: Record<SupportedLocale, () => Promise<LocaleBundle>> and SUPPORTED_LOCALES = ['en-GB', 'zh-CN', 'ar-AE', 'cy-GB', 'ru-RU'] as const. NO export *.
  • 2.3.5 scripts/check-locale-completeness.ts — Bun script asserting every key in en-GB.json exists in every other locale file; structured diff output on failure
  • 2.3.6 scripts/generate-translation-types.ts — Bun script generating src/locales/augmentation.d.ts from en-GB.json keys; output module-augments ContributedTranslationKeys per cross-cutting design §11.2
  • 2.3.7 src/locales/locales.spec-conformance.test.ts — runs the completeness check as a unit test; asserts every key exists in every locale and every locale parses against LocaleBundleSchema
  • 2.3.8 src/locales/cy-plural.spec-conformance.test.ts — asserts Intl.PluralRules('cy', { type: 'cardinal' }).select(n) returns the expected category for canonical reference values (0 → zero, 1 → one, 2 → two, 3 → few, 6 → many, 4 → other) under both bun test and vitest run
  • 2.3.9 src/locales/locales.md — covers add-a-locale workflow, key naming convention, ICU plural usage, RTL note, Welsh six-plural note

2.4 ThemeProvider + theme primitives

  • 2.4.1 src/theme/theme.types.ts — ThemePlugin, ThemeTokenSet, MotionTokenSet, TailwindPreset, ThemableVariants (empty interface — augmented per-component); Zod ThemePluginSchema / ThemeTokenSetSchema / MotionTokenSetSchema
  • 2.4.2 src/theme/theme-provider/theme-provider.tsx — accepts themes, defaultTheme, storageKey, enableSystem, attribute = 'data-theme', scriptNonce. Injects an inline FOUC-prevention script into <head> before hydration (CSP-nonce-aware). Subscribes to prefers-reduced-motion and updates --motion-duration-* / --motion-easing-* CSS variables. Emits theme.changed on the bus and increments react_ui.theme.changed_total + react_ui.theme.switch_duration_ms.
  • 2.4.3 src/theme/use-theme/use-theme.ts — useTheme() returns { theme, themes, setTheme, isPending }; throws REACT_UI.NO_PROVIDER outside a <ThemeProvider> in dev, soft-fails to identity in prod
  • 2.4.4 src/theme/use-theme-tokens/use-theme-tokens.ts — returns the live ThemeTokenSet for components needing direct token access (recharts palette, etc.)
  • 2.4.5 src/theme/use-themed-variants/use-themed-variants.ts — implements the §"Type-safety trade-off" pattern. Resolves the active theme at render and merges variantOverrides[componentName] with the base cva config. Memoised per (themeName, componentName) tuple. Histogram react_ui.themed_variants.resolve_duration_ms on cache miss only.
  • 2.4.6 src/theme/themes/light.ts, src/theme/themes/dark.ts, src/theme/themes/high-contrast.ts — three built-in themes, each implementing ThemePlugin; each with sibling .md documenting tokens
  • 2.4.7 src/theme/theme-switcher/theme-switcher.tsx — headless render-prop mirror of <LanguageSwitcher>; reads theme.labelKey (TranslationKey, never literal) and renders via the active i18n instance
  • 2.4.8 Tests: theme-provider.test.tsx (FOUC script injection, system listener, CSP nonce, setTheme happy path + THEME_NOT_FOUND), use-theme.test.tsx, use-themed-variants.test.tsx (base typing, narrowed typing, memo invalidation on theme change, brand-new variant fallback when active theme lacks it), theme-switcher.test.tsx
  • 2.4.9 Sibling .md per implementation file

2.5 PackRegistryProvider + pack primitives

  • 2.5.1 src/pack/pack.types.ts — ComponentPack, PackComponents (empty interface — augmented per-pack), PackContext; Zod ComponentPackSchema
  • 2.5.2 src/pack/pack-registry-provider/pack-registry-provider.tsx — accepts packs: ComponentPack[], optional components for runtime override per pack; throws REACT_UI.PACK_ALREADY_REGISTERED when two packs share name. Calls each pack's onRegister(ctx) in registration order, onUnregister(ctx) in reverse order. Emits pack.registered / pack.unregistered. Per-pack scoped React context. Forwards each pack's translations to the active i18n instance via addResourceBundle(pack.name, pack.translations[locale]).
  • 2.5.3 src/pack/use-pack-component/use-pack-component.ts — generic-typed usePackComponent<TName, TComponent>(packName, componentName); throws PACK_NOT_FOUND / PACK_COMPONENT_NOT_FOUND
  • 2.5.4 src/pack/use-ui-component/use-ui-component.ts — for built-in components, allows pack override via the runtime components prop on <ReactUiProvider>
  • 2.5.5 src/pack/version-check/version-check.ts — semver peerRange against the running react-ui version; mismatch logs pack.version_mismatch warning + increments react_ui.pack.version_mismatch_total; never throws
  • 2.5.6 Tests: pack-registry-provider.test.tsx (register order, unregister reverse-order, name-collision throw, runtime override, lifecycle hooks fire), use-pack-component.test.tsx, version-check.test.ts
  • 2.5.7 Sibling .md per implementation file

2.6 ReactUiProvider (opinionated wrapper)

  • 2.6.1 src/provider/react-ui-provider/react-ui-provider.tsx — composes <TranslationProvider> → <ThemeProvider> → <PackRegistryProvider>. ~15 lines of composition. Awaits the lazy reactUiBundles[locale]() import on mount and passes the result to i18n.addResourceBundle('react-ui', bundle). Honours the reactUiOverrides prop documented in cross-cutting design §8.4.
  • 2.6.2 OTel span react_ui.provider.mount with attributes { theme, pack.count, locale }
  • 2.6.3 Tests: react-ui-provider.test.tsx (composition order, bundled-translation auto-registration, override merge order, mount span attributes)
  • 2.6.4 Sibling .md

2.7 Utilities + hooks

  • 2.7.1 src/utils/cn/cn.ts — clsx + twMerge; cn.test.ts (merging conflicts, no-arg, array input); sibling .md
  • 2.7.2 src/utils/motion/motion.ts — motionToken(reducedMotion: string, fullMotion: string): string reading window.matchMedia('(prefers-reduced-motion: reduce)'); SSR guard; sibling .md; tests
  • 2.7.3 src/hooks/use-mobile/use-mobile.ts — useMediaQuery('(max-width: 768px)') via useState + useEffect; sibling .md; tests
  • 2.7.4 src/hooks/use-toast/use-toast.ts — toast queue backed by sonner; emits toast.show span event + react_ui.toast.shown_total; sibling .md; tests

2.8 Wave 2 gate

  • 2.8.1 bunx turbo lint typecheck test --filter=@suaveplan/react-ui — green chassis with zero components yet
  • 2.8.2 bunx turbo test:coverage --filter=@suaveplan/react-ui — 100/100/100/100 on the chassis surface
  • 2.8.3 bun scripts/check-locale-completeness.ts — green
  • 2.8.4 bun scripts/generate-translation-types.ts — produces src/locales/augmentation.d.ts matching en-GB.json exactly; idempotent
  • 2.8.5 Wave 2 PR opens; reviewers verify the §"Type-safety trade-off" spike landed and the §"Component count" table is finalised before Wave 3 starts

3. Wave 3 — Components in 8 batches

For every component within a batch:

  • 3.X.Y.1 Implement <component-name>.tsx — Radix wrapper (where applicable), forwardRef, asChild support, useThemedVariants(componentName) for variant resolution, useTranslation('react-ui') for any default user-visible copy, every prop carrying user text typed as TranslationKey (NEVER string)
  • 3.X.Y.2 <component-name>.types.ts — every public prop interface, no inline shapes (AGENTS.md §11)
  • 3.X.Y.3 <component-name>.md — ≥200 words; Purpose + Features + Basic + Advanced + API + Implementation Notes
  • 3.X.Y.4 <component-name>.test.tsx — public-API contract, RHF integration where applicable, callback firing, a11y attributes (role, aria-*), variant classes, theme-override flow under <ThemeProvider themes={[lightTheme, draculaTheme]}> ; { timeout: 5000 } on every it() per CLAUDE.md
  • 3.X.Y.5 <component-name>.stories.tsx — Default + Playground + Variants + States + WithI18n (5 locales incl. RTL ar-AE) + WithTheme (4 themes incl. draculaTheme) + A11y + Composition (where composable)
  • 3.X.Y.6 index.ts barrel using explicit named re-exports (CLAUDE.md §10; no export *)
  • 3.X.Y.7 Add the component's translation keys to src/locales/en-GB.json first, then port to the other four locales
  • 3.X.Y.8 Re-run scripts/generate-translation-types.ts; commit the regenerated augmentation.d.ts
  • 3.X.Y.9 Re-run scripts/lint/check-no-hardcoded-strings.ts and scripts/lint/check-translation-keys.ts; both green

3.1 Batch 3a — Foundation (5)

  • 3.1.1 Button (exercises core theme + variant pipeline)
  • 3.1.2 Input
  • 3.1.3 Label
  • 3.1.4 Separator
  • 3.1.5 Skeleton
  • 3.1.6 Batch 3a gate: bunx turbo lint typecheck test test:coverage --filter=@suaveplan/react-ui green

3.2 Batch 3b — Form primitives (10)

  • 3.2.1 FieldGroup, Field, FieldSet, InputGroup, Textarea, Checkbox, Switch, RadioGroup, Toggle, ToggleGroup
  • 3.2.2 Each primitive's API surface MUST be re-exportable from @suaveplan/react-forms without modification (cross-cutting design §9.2)
  • 3.2.3 Batch 3b gate

3.3 Batch 3c — Overlays (8)

  • 3.3.1 Dialog, Sheet, Drawer, AlertDialog, Popover, HoverCard, Tooltip, ContextMenu
  • 3.3.2 Every overlay component's <component-name>.test.tsx MUST cover focus-trap + restore + Escape-closes (cross-cutting design §10.4)
  • 3.3.3 Batch 3c gate

3.4 Batch 3d — Data display (10)

  • 3.4.1 Table, DataTable, Card, Badge, StatusBadge, Avatar, Alert, Empty, Progress, Spinner
  • 3.4.2 DataTable.emptyMessage prop accepts TranslationKey | ReactNode per cross-cutting design §11.7 pattern C
  • 3.4.3 Batch 3d gate

3.5 Batch 3e — Navigation (8)

  • 3.5.1 Sidebar, NavigationMenu, Breadcrumb, Tabs, Pagination, Menubar, DropdownMenu, Command
  • 3.5.2 Pagination swaps chevron icons under useDirection() === 'rtl'
  • 3.5.3 Command emits command.select span event + react_ui.command.selected_total
  • 3.5.4 Sidebar extracted helpers (SidebarNav, SidebarSection) to stay under the AGENTS.md §3 300-line limit
  • 3.5.5 Batch 3e gate

3.6 Batch 3f — Layout (6)

  • 3.6.1 Accordion, Collapsible, Resizable, ScrollArea, AspectRatio, Carousel
  • 3.6.2 Batch 3f gate

3.7 Batch 3g — Feedback (4)

  • 3.7.1 Sonner (the toast surface), ConfirmDeleteDialog, ErrorBoundary, ThemeToggle
  • 3.7.2 Sonner + ConfirmDeleteDialog suppress animation under motionTokens.respectReducedMotion: 'strict'
  • 3.7.3 Batch 3g gate

3.8 Batch 3h — Specialised (12)

  • 3.8.1 Calendar, DatePicker, Select, MultiSelect, Combobox, SearchableSelect, Slider, InputOTP, Chart, MoneyInput, LanguagePicker, Item
  • 3.8.2 Chart adds keyboard legend nav, <title> + <desc> for screen readers, chart.render span
  • 3.8.3 MoneyInput consumes formatCurrency from @suaveplan/i18n formatters; locale-reactive
  • 3.8.4 LanguagePicker reads from @suaveplan/locale-data; props Zod-validated via LanguagePickerSchema
  • 3.8.5 Batch 3h gate

(Deployment to OVH owned by sibling react-ui-registry-deployment; this change owns the generation script + manifest schemas only.)

  • 4.1 scripts/build-registry.ts — reads src/components/<component>/<component>.tsx, strips internal-only OTel hooks (replaced with no-op shims in the ejected source), rewrites @suaveplan/react-i18n imports to a configurable consumer alias (default @/lib/i18n), captures Tailwind/cva dependencies into registryDependencies, validates manifests against the shadcn registry JSON schema, emits to registry/r/ui/<component>.json
  • 4.2 Theme presets: emit registry/r/themes/<theme>.json for light, dark, highContrast
  • 4.3 Pack manifests: emit registry/r/packs/<pack>.json (used by sibling enterprise-react-ui-pack-auth to register its own packs against this generator)
  • 4.4 registry/registry.json top-level index — consumer queries via npx shadcn@latest search @suaveplan
  • 4.5 scripts/build-registry.test.ts — round-trip test: every generated manifest validates against the shadcn schema; ejected source compiles against a sample Vite project
  • 4.6 Snapshot test: regenerated registry/r/*.json matches the checked-in snapshot byte-for-byte (CI gate per cross-cutting design §10.1 row "Registry build")

5. CSS

  • 5.1 src/styles/global.css — Tailwind @layer base + the variables <ThemeProvider> writes (--background, --foreground, --primary, etc.). The CSS file is the substrate; the variable values come from the active ThemePlugin.cssVariables. Document required consumer import: import '@suaveplan/react-ui/styles/global.css'.
  • 5.2 src/styles/global.css.md documenting the substrate vs theme distinction

6. Tests + E2E

  • 6.1 tests/integration.test.tsx — full provider composition: mount <ReactUiProvider i18n={i18n} themes={[light, dark, highContrast, dracula]} packs={[authPack]}>; switch theme; switch locale; register / unregister a pack at runtime; assert no FOUC, no hydration mismatch
  • 6.2 tests/no-hardcoded-strings.spec-conformance.test.ts — runs scripts/lint/check-no-hardcoded-strings.ts against src/, asserts zero diagnostics
  • 6.3 tests/translation-keys.spec-conformance.test.ts — runs scripts/lint/check-translation-keys.ts against src/, asserts zero diagnostics
  • 6.4 e2e/a11y.spec.ts — @axe-core/playwright against every Storybook story; zero-violation gate
  • 6.5 e2e/keyboard-nav.spec.ts — Dialog, Drawer, Sheet, Command, DropdownMenu, ContextMenu, Menubar, SearchableSelect keyboard paths
  • 6.6 e2e/rtl.spec.ts — every component's WithI18n story under ar-AE; mirror layout; chevrons swapped
  • 6.7 e2e/theme-switch.spec.ts — Playwright performance trace; setTheme returns within 16ms (one frame)
  • 6.8 e2e/locale-switch.spec.ts — locale-switch under 100ms (warm) / 500ms (cold); no Suspense flash on warm

7. CI gates (no-hardcoded-strings + translation-keys)

  • 7.1 scripts/lint/check-no-hardcoded-strings.ts — Bun script using TypeScript compiler API; walks packages/react/react-ui/src/ .tsx/.ts; flags JSX text nodes that aren't whitespace-only and aren't wrapped in {t(...)} / {<Trans>...} / hook call returning a translation; flags watchlisted attribute literals (aria-label, aria-description, aria-roledescription, aria-valuetext, aria-keyshortcuts, placeholder, title, alt); flags object-literal properties on the field-config watchlist (label, message, description, tooltip, placeholder, errorMessage, helperText, validationMessage) when value is a string literal; tests / spec / stories files exempt
  • 7.2 scripts/lint/check-translation-keys.ts — collects every t('...') call across src/, cross-checks against the union of all locale JSON files; missing key fails the build with structured diff
  • 7.3 Wire both into bunx turbo lint for @suaveplan/react-ui filter
  • 7.4 Each script ships with --help text, exits non-zero on diagnostics, and emits <file>:<line>:<col> <diagnostic> line per violation per cross-cutting design §11.3
  • 7.5 scripts/lint/check-no-hardcoded-strings.test.ts — fixtures cover JSX text node, allowed {t(...)}, banned aria-label="x", allowed aria-label={t('x')}, banned object-literal label: 'x', allowed label: 'react-ui.foo.label'
  • 7.6 scripts/lint/check-translation-keys.test.ts — fixture with one valid key + one typo; asserts the typo fails

8. Storybook

  • 8.1 Storybook 8 wired with @storybook/addon-a11y
  • 8.2 Per-component stories per Wave 3 task §3.X.Y.5
  • 8.3 tests/stories.smoke.test.tsx — composeStories smoke-renders every story; matches snapshot of accessible name + role tree
  • 8.4 Storybook static build emits to storybook-static/; sibling react-ui-registry-deployment rsyncs it to OVH at https://ui.suaveplan.com/storybook/

9. Wiring and exports

  • 9.1 src/index.ts — explicit named exports for every public symbol; NO export * per CLAUDE.md §10
  • 9.2 src/index.ts imports ./locales/augmentation as a side-effect-only entry so consumers' type system picks up the ContributedTranslationKeys augmentation automatically
  • 9.3 Subpath exports map declared in package.json for every public module
  • 9.4 README.md at the package root with: install, quickstart (<ReactUiProvider> 10-line example), every component listed with one example, every hook with one example, link to each co-located .md, locale-bundle override note, theme contract pointer, pack contract pointer
  • 9.5 tests/dep-graph.test.ts — verifies no i18next*, no react-i18next, no @suaveplan/react-translate deps
  • 9.6 Add to MANIFEST.md via bun scripts/folder-reorg/generate-manifest.ts per CLAUDE.md "Package Lifecycle"

10. Pre-archive verification (the 5-gate triple, AGENTS.md §13)

  • 10.1 bun run check — Biome 0/0/0 across the monorepo
  • 10.2 bunx turbo lint typecheck test --filter=@suaveplan/react-ui — green
  • 10.3 bunx turbo test:coverage --filter=@suaveplan/react-ui — 100/100/100/100 (provider: 'istanbul')
  • 10.4 bunx turbo build --filter=@suaveplan/react-ui — green
  • 10.5 bun scripts/openspec-conformance.ts @suaveplan/react-ui — exit 0
  • 10.6 bun scripts/check-locale-completeness.ts — green
  • 10.7 bun scripts/lint/check-no-hardcoded-strings.ts — zero diagnostics
  • 10.8 bun scripts/lint/check-translation-keys.ts — zero diagnostics
  • 10.9 bun scripts/build-registry.ts — green; manifests match checked-in snapshot
  • 10.10 Verify only one React version installed: find . -path "*/node_modules/react/package.json" | head -5
  • 10.11 Banned-pattern scan (zero output): grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only" packages/react/react-ui/src/ --include="*.ts" --include="*.tsx"
  • 10.12 Barrel-shape scan per AGENTS.md §13 step 4a — zero output for any <feature>/index.ts
  • 10.13 Test-runner import scan per AGENTS.md §13 step 4b — zero from "vitest" / from "bun:test" in src/
  • 10.14 Missing-.md scan per AGENTS.md §13 step 5 — zero output
  • 10.15 Bare-version scan per AGENTS.md §13 step 7 — zero output
  • 10.16 Subpath-export count matches feature-module count per AGENTS.md §13 step 8

11. Pre-archive 7-row checklist (AGENTS.md §13 §"Quality bar")

  • 11.1 Generics threaded; expectTypeOf assertions on useThemedVariants (base vs narrowed), usePackComponent, <ThemePlugin> augmentation, <ComponentPack> augmentation
  • 11.2 Zod schemas at every public boundary (ReactUiProviderPropsSchema, ThemePluginSchema, ComponentPackSchema, LocaleBundleSchema, every component's props schema where the legacy package had one)
  • 11.3 All nine error codes thrown from at least one production path AND covered by a *.test.ts assertion
  • 11.4 Strict-mode tested: dev no-provider throw, prod soft-fail, theme-not-found, pack-collision, pack-version-mismatch warn, locale-bundle-load-failed
  • 11.5 OTel histograms / counters / spans emitted from production paths and asserted with a real in-memory MeterProvider in *.spec-conformance.test.ts
  • 11.6 Lifecycle dedup: pack onRegister / onUnregister fire exactly once per mount/unmount; locale-bundle dynamic import deduped via in-flight Promise
  • 11.7 No retroactive tick — every box reflects work done in this change

12. Behavior-change propagation (AGENTS.md §13a)

  • 12.1 ## Migration section in proposal.md: lists every consumer-visible behaviour change vs the legacy proposal (component drops, theme contract introduction, branded TranslationKey, locale auto-registration). Currently zero genesis consumers, so the section's "search command" output is empty — but it MUST exist for future agents reading the archive
  • 12.2 Sibling change enterprise-react-i18n amendment includes the ContributedTranslationKeys augmentation pattern this package uses
  • 12.3 Sibling change enterprise-react-forms-v2 documents the re-export of Field / FieldGroup / FieldSet / InputGroup from this package

13. Root-doc refresh + markdownlint (AGENTS.md §13b)

  • 13.1 bun scripts/folder-reorg/generate-manifest.ts — refreshes MANIFEST.md
  • 13.2 bun scripts/generate-missing.ts — refreshes MISSING.yml
  • 13.3 Hand-edit STATUS.md: move @suaveplan/react-ui row from Pending → Completed
  • 13.4 Hand-edit SUMMARY.md: per-package paragraph reflecting the shipped surface
  • 13.5 bunx markdownlint-cli2 --fix across touched files; git status clean
  • 13.6 Second pass bunx markdownlint-cli2 — exit 0

14. Publish + archive

  • 14.1 bun scripts/publish.ts --package @suaveplan/react-ui
  • 14.2 Fold spec into openspec/specs/react-ui/spec.md (new capability folder)
  • 14.3 Move openspec/changes/enterprise-react-ui-v2/ → openspec/archive/<yyyy-mm-dd>-enterprise-react-ui-v2/
  • 14.4 Move openspec/changes/enterprise-react-ui/ → openspec/archive/<yyyy-mm-dd>-enterprise-react-ui-superseded/
  • 14.5 Refresh MANIFEST.md, STATUS.md, SUMMARY.md to mark @suaveplan/react-ui shipped