diff --git a/.changeset/stage-5-theming-core.md b/.changeset/stage-5-theming-core.md new file mode 100644 index 0000000..012bd78 --- /dev/null +++ b/.changeset/stage-5-theming-core.md @@ -0,0 +1,10 @@ +--- +'@reactive/silk-core': minor +--- + +Stage 5 theming maturity (core): palette generation, paired dark derivation, and contrast auditing. + +- Add `generateScale(seedHex, colorScheme)` — OKLCH 12-step ramps from canonical sRGB hex. +- Add `generatePairedPalette(brandHex)` — tenant recipe producing full light+dark palettes (accent + brand-tinted gray; optional danger/success seeds). +- Add `checkThemeContrast`, `contrastRatio`, `relativeLuminance`, and `parseCanonicalHex` for CI/tooling. +- Depends on `culori` for OKLCH conversion and gamut mapping (private to the generator). diff --git a/.changeset/stage-5-theming-web.md b/.changeset/stage-5-theming-web.md new file mode 100644 index 0000000..fb83b04 --- /dev/null +++ b/.changeset/stage-5-theming-web.md @@ -0,0 +1,9 @@ +--- +'@reactive/silk': minor +--- + +Stage 5 theming maturity (web): portal variable channels and re-exports. + +- Split theme-scope CSS vars into `semanticVars` (replaced by nested `theme`/`colorScheme`) and `customVars` (component hooks that inherit through named children into portals). +- Re-export `generateScale`, `generatePairedPalette`, `checkThemeContrast`, and related types from `@reactive/silk`. +- Document the frozen public component CSS-variable list via `silkComponentVarMeta` / `formatComponentVarDocsTable`. diff --git a/README.md b/README.md index d5aad4f..c9d90c9 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ yarn docs # http://localhost:6006 ## Status -Stage 2 (visual primitives & forms): `Surface`, `Card`, `Heading`, `Badge`, status primitives, `Field`/`Input`/`Textarea`, Radix-backed form controls, token audit (`success`, elevation shadows, contrast), SettingsForm fixture, and [pre-1.0 API policy](docs/API_POLICY.md). Stage 1 layout vocabulary remains. The staged plan is in [docs/ROADMAP.md](docs/ROADMAP.md); the project charter is [docs/PRINCIPLES.md](docs/PRINCIPLES.md). +Stage 5 (theming maturity): `generatePairedPalette` / `generateScale`, `checkThemeContrast`, nested portal variable channels, TenantGallery + ThemePlayground in docs, frozen public component CSS-var list. Prior stages shipped layout, visual/forms, interaction primitives, and composites. The staged plan is in [docs/ROADMAP.md](docs/ROADMAP.md); the project charter is [docs/PRINCIPLES.md](docs/PRINCIPLES.md). ## Packages @@ -89,10 +89,13 @@ export function App() { } ``` -Custom / tenant themes use the style-attribute path: +Custom / tenant themes use the style-attribute path. For brand seeds with paired light/dark: ```tsx - +import { createTheme, generatePairedPalette } from '@reactive/silk'; + +const paired = generatePairedPalette('#0ea5e9'); + ``` diff --git a/apps/docs/src/Theming.mdx b/apps/docs/src/Theming.mdx index 12f2845..95151cb 100644 --- a/apps/docs/src/Theming.mdx +++ b/apps/docs/src/Theming.mdx @@ -40,6 +40,37 @@ const theme = createTheme({ Prefer **either** `theme` **or** `colorScheme`. If both are passed, `theme` wins for `data-theme` and inline variables. +### Palette generation and dark derivation + +For multi-tenant branding, generate a full light+dark palette from one brand hex: + +```tsx +import { + createTheme, + generatePairedPalette, + checkThemeContrast, +} from '@reactive/silk'; + +const paired = generatePairedPalette('#0ea5e9'); +const light = createTheme({ colorScheme: 'light', palette: paired.light }); +const dark = createTheme({ colorScheme: 'dark', palette: paired.dark }); + +// CI / playground guard — hex-only semantic audit +const { ok, violations } = checkThemeContrast(light); +``` + +Slot mapping for `generatePairedPalette(brand)`: + +| Palette scale | Source | +| --- | --- | +| `blue` (accent) | Brand chromatic OKLCH ramp | +| `gray` (surfaces / neutral / text) | Low-chroma brand-hue ramp | +| `red` / `green` | Built-in defaults, or `dangerSeedHex` / `successSeedHex` | + +`generateScale(seed, colorScheme)` is the lower-level primitive (12-step OKLCH ramp). Seed input is canonical sRGB hex (`#RGB` / `#RRGGBB`); invalid input throws. Algorithm curves may improve between minors; the hex contract and 12-step shape are stable. + +See **Theme/TenantGallery** (two tenants × light/dark side by side) and **Theme/ThemePlayground** (live controls + contrast readout). + ## Typography and fonts Silk exposes three font-family tokens (inspired by Claude Cowork's sans / serif / mono roles) and maps typography roles onto them: @@ -88,23 +119,66 @@ Defaults are a typed map — not a runtime component registry. ## Nesting and portals -Nesting works via DOM CSS variable inheritance in normal flow. - -Portals (for example Dialog) still render under `document.body` by default, but Silk reconstitutes the **nearest** `ThemeProvider` / `SilkProvider` scope on the portaled tree: the same theme class, `data-theme` (when set), and custom `createTheme` CSS variables. Nested providers therefore theme their dialogs correctly without an explicit portal container. +Nesting works via DOM CSS variable inheritance in normal flow, with two variable **channels**: -For cases where you need the portal DOM to live inside a particular subtree (stacking, clipping, or measuring against that subtree), pass Dialog `container`. Nested theming itself does **not** require `container` — see **Components/Interaction/Dialog → NestedThemePortal**. - -## Component CSS variable hooks - -Public hooks like `--silk-button-bg` resolve through private vars: +| Channel | Contents | Nested `theme` / `colorScheme` | +| --- | --- | --- | +| Semantic | `--silk-color-*`, radii, type, motion, shadows, focus geometry, space source scales | **Replaced** — named children do not carry outer tenant semantics into portals | +| Custom | Component hooks (`--silk-button-bg`, …) and other `--silk-*` extensions | **Inherited** — portals under an inner named scheme still see outer hooks | + +Supported patterns: + +1. **Tenant → named** — outer `theme={tenant}`, inner `colorScheme="dark"`: inner (and its portals) use named dark semantics; outer component hooks still apply. +2. **Named → tenant** — outer named scheme, inner custom theme: inner semantics win for that subtree and its portals. +3. **Tenant → tenant** — inner custom theme fully replaces outer semantics; custom hooks merge. + +Portals (Dialog, Popover, Select, …) reconstitute the nearest scope: theme class, `data-theme`, semantic vars, and custom vars. Nested theming does **not** require Dialog `container` — pass `container` only when the portal DOM must live inside a particular subtree. + +Constant SSR `