Files
cadence-ui/docs/rfcs/multi-style-architecture.md
T

628 lines
15 KiB
Markdown

# RFC: Multi-Style Architecture
## Status
Proposed
## Last Updated
2026-03-20
## Why this document exists
This document records the current plan for making Cadence UI support multiple visual
styles without forking component behavior or losing the existing source-owned model.
It is written as a handoff document. A different agent should be able to read this
file, inspect the listed repo files, and continue the work without reconstructing the
intent from chat history.
## Objective
Support runtime switching across multiple visual styles such as:
- `minimal`
- `glass`
- `pixel`
while preserving:
- the current public React component APIs
- accessibility behavior
- reduced-motion support
- source ownership inside this repository
## Summary Decision
Cadence UI should not treat "style" as a single token pack.
The working model should be:
1. `theme`: color, typography, radius, shadow baseline
2. `skin`: component appearance recipe
3. `motion`: interaction timing and effect vocabulary
4. `layout`: page composition patterns, handled outside the base component package
For the first milestone, this repo should implement `theme + skin + motion`. Layout
patterns such as `bento`, `sidebar`, or `magazine` should remain a docs or blocks
concern, not a base `@ai-ui/ui` concern.
## External Reference Synthesis
The site `https://ui-gallery.codebanana.app/` is useful because it separates style into
multiple dimensions:
- `UI 风格`
- `色调`
- `字体`
- `布局`
- `动效`
That separation matches the conclusion above: styles like `glassmorphism` or
`pixel art` are not just color swaps. They change component surfaces, borders,
decoration, motion, and sometimes page composition.
## Current Repo State
### What already exists
- `packages/tokens` already defines semantic CSS variables for color, typography,
radius, shadow, and motion.
- `packages/tokens` already supports multiple themes through `data-theme`.
- `packages/tokens` already supports reduced-motion behavior through `data-motion`
and `prefers-reduced-motion`.
- `packages/ui` already consumes semantic token variables in many component recipes.
- `packages/ui` now exposes a public skin contract through `skinNames`, `defaultSkin`,
`skinDetails`, and `setSkin`.
- `packages/ui` now exports a dedicated `@ai-ui/ui/skins.css` entrypoint.
- `apps/docs` already acts as the review surface and imports token CSS globally.
- `apps/docs` Storybook globals now apply `theme`, `skin`, and `motion` together.
- `apps/docs` now includes a `Foundation/Style Contract` page that documents the Phase 1
runtime contract.
### What does not exist yet
- No component skin layer exists.
- No style provider exists that treats `theme`, `skin`, and `motion` as separate axes.
- No existing component family fully restyles across multiple skins yet.
### Why the repo is not "multi-style" yet
The current system is multi-theme, not multi-style.
Many visual decisions are still written directly inside `packages/ui`, for example:
- button sheen gradient and animation
- skeleton shimmer gradient
- white switch thumb
- fixed rounded forms like `rounded-full`
- fixed spacing and size choices that encode a particular visual language
That means changing `tokens` alone can shift tone, but it cannot reliably produce
strongly different styles like `glass` or `pixel`.
## Read These Files First
Another agent resuming this work should inspect these files first:
- `roadmap.md`
- `CONTRIBUTING.md`
- `packages/tokens/src/index.ts`
- `packages/tokens/src/tokens.css`
- `packages/tokens/src/motion.css`
- `packages/ui/src/lib/skin.ts`
- `packages/ui/src/skins.css`
- `apps/docs/src/preview.css`
- `apps/docs/.storybook/preview.ts`
- `apps/docs/src/style-contract.stories.tsx`
- `packages/ui/src/lib/motion.ts`
- `packages/ui/src/components/button.tsx`
- `packages/ui/src/components/button.variants.ts`
- `packages/ui/src/components/skeleton.tsx`
- `packages/ui/src/components/switch.variants.ts`
These files capture the current token contract, motion contract, docs wiring, and the
best concrete examples of hardcoded visual decisions that block style switching.
## Working Definitions
### Theme
Theme sets the foundational visual baseline:
- color roles
- font families
- font scales
- radius scale
- shadow scale
Examples:
- `editorial`
- `minimal`
- `dark-luxury`
### Skin
Skin defines how components look while keeping structure and behavior stable.
Skin can change:
- surface treatment
- border style
- blur and transparency
- decoration layers
- contrast level
- component-specific gradients
- special effects such as sheen or pixelated shadows
Examples:
- `minimal`
- `glass`
- `pixel`
### Motion Pack
Motion pack defines interaction feel:
- durations
- easing
- hover feedback
- entrance and exit behavior
- loading behavior
Examples:
- `calm`
- `micro`
- `spring`
### Layout Pattern
Layout pattern is page composition, not component skin.
Examples:
- `bento-grid`
- `sidebar-app`
- `magazine`
- `single-column-longform`
These should live in docs, blocks, or app-level patterns. They should not be mixed into
the core style-switching milestone.
## Proposed Architecture
### Layer 1: Semantic Foundation Tokens
Keep `packages/tokens` as the owner of global semantic variables.
Do not split multiple token packages yet. Keep a single package with multiple themes
until the contract is stable.
The existing token groups remain the baseline:
- `--color-*`
- `--font-*`
- `--text-*`
- `--radius-*`
- `--shadow-*`
- `--dur-*`
- `--ease-*`
- `--distance-*`
- `--scale-*`
### Layer 2: Skin Contract
Add a new root attribute:
```html
<html data-theme="minimal" data-skin="glass" data-motion="micro">
```
`data-skin` should be the runtime contract for component appearance.
Recommended initial values:
- `minimal`
- `glass`
- `pixel`
### Layer 3: Component Semantic Style Variables
The current repo uses global semantic tokens directly in many components. That is good
for theme switching, but it is not enough for skin switching.
The next step should be introducing component-semantic variables or skin hooks for the
highest-value components first.
Examples:
- button:
- `--button-bg`
- `--button-border`
- `--button-fg`
- `--button-shadow`
- `--button-sheen-opacity`
- card:
- `--card-bg`
- `--card-border`
- `--card-shadow`
- input:
- `--input-bg`
- `--input-border`
- `--input-shadow`
- dialog or popover:
- `--panel-bg`
- `--panel-border`
- `--panel-shadow`
- `--panel-backdrop-blur`
- switch:
- `--switch-track-bg`
- `--switch-thumb-bg`
The rule is:
- global semantic tokens define product-wide roles
- component semantic variables define component appearance
- skins map global tokens to component variables
### Layer 4: Skin CSS Ownership
The recommended ownership split is:
- `packages/tokens` owns global theme and motion variables
- `packages/ui` owns component skin CSS
Reason:
- `theme` is a design-system baseline concern
- `skin` is a component recipe concern
That means the likely future CSS imports are:
```css
@import "@ai-ui/tokens/styles.css";
@import "@ai-ui/ui/skins.css";
```
Consumer ergonomics can later be improved by providing a convenience entry such as:
```css
@import "@ai-ui/ui/styles.css";
```
where `@ai-ui/ui/styles.css` re-exports both token and skin styles.
## Implementation Rules
### Rule 1: Preserve behavior and API
Do not fork component logic per skin.
Skins should change appearance, not:
- props
- ARIA behavior
- controlled or uncontrolled patterns
- slot naming
- `data-*` state contract
### Rule 2: Separate structure from appearance
Leave these in component code:
- layout structure
- sizing variants
- slot and state attributes
- accessibility and interaction logic
Move these toward skin hooks:
- surface fill
- border treatment
- shadow treatment
- blur
- decoration layers
- visual effect toggles
### Rule 3: Do not put layout patterns into the skin milestone
Page layouts are a separate concern and should not block the component skin work.
### Rule 4: Default gracefully
If a consumer does not specify `data-skin`, the system should default to the current
closest appearance, likely `minimal`.
### Rule 5: Reduced motion remains authoritative
No skin may bypass reduced-motion expectations. `data-motion` and
`prefers-reduced-motion` still govern the final motion behavior.
## Recommended First Slice
Do not start by updating every component.
Start with a focused pilot set that proves the architecture:
- `Button`
- `Card`
- `Input`
- `Dialog`
- `Switch`
- `Skeleton`
These are enough to validate:
- basic surfaces
- overlays
- focus treatments
- loading affordances
- special effects
## Suggested Skin Definitions
### Minimal
Intent:
- remove ornamental effects
- keep surfaces solid
- keep shadows subtle
- prefer clarity over decoration
Expected traits:
- no sheen
- no blur
- low-contrast borders
- restrained shadows
### Glass
Intent:
- translucent layers
- blurred panels
- floating surfaces
- higher use of overlays and edge highlights
Expected traits:
- semi-transparent surfaces
- stronger border highlights
- backdrop blur on floating panels
- controlled glow or sheen on selected components
### Pixel
Intent:
- square geometry
- hard edges
- crisp borders
- step-like shadow language
Expected traits:
- zero or near-zero radius
- no blur
- hard shadows
- no soft sheen
- sharper, less fluid motion
## Suggested Runtime API
The end state should support runtime switching without remounting components.
Minimum contract:
```ts
type ThemeName = "light" | "dark" | "brand" | "minimal";
type SkinName = "minimal" | "glass" | "pixel";
type MotionPackName = "calm" | "snappy" | "spring";
type MotionAccessibilityName = "system" | "full" | "reduced";
```
Likely helpers:
- `setTheme(theme, root?)`
- `setSkin(skin, root?)`
- `setMotionPack(pack, root?)`
- `setMotionAccessibility(mode, root?)`
- `setMotionMode(mode, root?)` as a backward-compatible alias for accessibility mode
Provider shape if needed:
```tsx
<StyleProvider
theme="minimal"
skin="glass"
motionPack="spring"
motionAccessibility="system"
>
<App />
</StyleProvider>
```
This provider is optional. Direct root attributes are acceptable if they keep the system
simple.
## Delivery Plan
### Phase 0: Analysis and Contract Draft
Goal:
- document the problem clearly
- separate `theme`, `skin`, `motion`, and `layout`
- identify which components prove the architecture
Deliverables:
- this RFC
- explicit initial skin names
- initial pilot component list
Status:
- completed
### Phase 1: Runtime Skin Contract
Goal:
- introduce `data-skin`
- define `skinNames` and related helpers
- decide final CSS file ownership and export path
Deliverables:
- root skin attribute contract
- helper API
- docs wiring for skin switching
Status:
- completed
### Phase 2: Pilot Skin Extraction
Goal:
- extract hardcoded visual decisions from the pilot components into skin-aware hooks
Deliverables:
- `Button`, `Card`, `Input`, `Dialog`, `Switch`, and `Skeleton` render distinctly under
`minimal`, `glass`, and `pixel`
- no public component API breakage
Status:
- completed
### Phase 3: Docs Validation Surface
Goal:
- create a docs page or stories that compare the same components across all supported
skins and motion modes
Deliverables:
- a stable review surface in `apps/docs`
- screenshot-friendly comparisons
- clear regression target for future work
Status:
- completed
### Phase 4: Expand Coverage
Goal:
- apply the same pattern to the rest of the component library
Priority order:
- form controls
- overlays
- navigation and menus
- feedback components
- advanced patterns such as `DataTable`
Status:
- completed
### Phase 5: Packaging and Consumer Ergonomics
Goal:
- make skin CSS consumable outside this repo without forcing consumers to understand the
internal file graph
Deliverables:
- final CSS import path
- final package exports
- install guidance in release and registry docs
Status:
- completed
## Acceptance Criteria For The First Real Milestone
The first milestone should be considered complete only when all of these are true:
- the same `Button`, `Card`, `Input`, `Dialog`, `Switch`, and `Skeleton` instances can
switch across `minimal`, `glass`, and `pixel`
- switching uses root attributes, not per-component prop forks
- reduced motion still works correctly
- Storybook or docs demonstrate the comparison clearly
- no public component APIs were broken to achieve the visual switching
## Risks
### Risk 1: Tailwind classes currently mix structure and appearance
This will make extraction repetitive. The work must stay disciplined and not turn into
ad hoc component rewrites.
### Risk 2: Some skins may require more than variables
`glass` and `pixel` may need extra selectors, pseudo-elements, or special wrapper
classes. That is acceptable, but those decisions must remain in the skin layer.
### Risk 3: Layout can derail the effort
The external reference site includes layout categories. If layout gets pulled into the
first milestone, the implementation scope will expand too quickly.
### Risk 4: Packaging decisions can arrive too early
Do not split multiple npm packages for separate skins until the internal contract is
proven inside this repo.
## Open Questions
- Should `skin` helpers live in `@ai-ui/tokens` or `@ai-ui/ui`? The current
recommendation is `@ai-ui/ui`.
- Should the runtime API expose a `StyleProvider`, or should root attributes remain the
only public contract at first?
- Should the current `calm / snappy / spring` set remain the long-term pack list, or
should product-specific packs be introduced later?
- Should `minimal` become the new default appearance, or should the current warm
editorial look remain the default and be renamed explicitly?
## Current Completion Snapshot
As of 2026-03-20, the project is at this point:
- problem analysis completed
- architecture direction chosen
- scope boundaries chosen
- phase 1 runtime skin contract completed
- `@ai-ui/ui/skins.css` export added
- Storybook globals now switch `skin`
- docs switching surface added in `Foundation/Style Contract`
- pilot recipe extraction completed for `Button`, `Card`, `Input`, `Dialog`, `Switch`,
and `Skeleton`
- screenshot-friendly validation surface added in `Foundation/Style Matrix`
- scoped `data-motion="reduced"` now works for nested docs wrappers
- motion now uses real packs through `data-motion-pack`
- reduced motion remains available as a separate accessibility override layer
- shared skin-aware treatment now extends across the broader component library surface,
including controls, menus, overlays, feedback, and data-heavy patterns
- package consumers can now import a single combined stylesheet from
`@ai-ui/ui/styles.css`
The original Phase 0-5 implementation sequence is now complete. Further work should be
treated as iteration: refining skins, expanding motion packs, or hardening package and
registry distribution.