first commit
This commit is contained in:
85
docs/ui/react/react-ui/src/theme/layer-stack/layer-stack.md
Normal file
85
docs/ui/react/react-ui/src/theme/layer-stack/layer-stack.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# `layer-stack` — module-level mutation-layer engine
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `layer-stack` module is the runtime engine that backs `<ThemeProvider>`, `useThemeMutator()`, and `useResolvedThemeTokens()`. It owns a single ordered list of `MutationLayer` records, each carrying that layer's contribution to the CSS custom-property bag applied on `document.documentElement` (or whatever root element the provider has bound). Each `<ThemeProvider>` pushes one `plugin` layer when it mounts and replaces it whenever `setTheme(...)` resolves. Each `useThemeMutator()` push s its own `mutator` layer on mount and removes it on unmount. The stack engine is the single point that touches the DOM — every consumer surface ultimately delegates to it, which is how nested providers and stacked mutators end up with a coherent innermost-wins-per-key semantics on a shared root element.
|
||||
|
||||
The stack is module-level by design: every provider in the tree targets the same root, so per-provider state would still need a cross-provider tiebreaker. Putting the stack at the module level means the ordering is exactly the React mount order, which is the only ordering with a sensible "innermost wins" interpretation.
|
||||
|
||||
## Features
|
||||
|
||||
- Push / update / remove a layer, identified by a stable `id` generated via `newLayerId(...)`.
|
||||
- Track every CSS custom-property name the stack has ever written, so unmount cleanly removes any orphan.
|
||||
- Subscribe to stack changes via `subscribe(listener)` — fired AFTER the DOM has been re-applied so listeners observe the live state.
|
||||
- Read the merged result against an active-plugin baseline via `getMergedResolved(baseline)` for consumer hooks.
|
||||
- Test-only entry points (`__resetLayerStackForTests`, `bindRoot`) so each test can mount into a clean root and tear down without leaking state across files.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
bindRoot,
|
||||
newLayerId,
|
||||
newProviderId,
|
||||
pushLayer,
|
||||
removeLayer,
|
||||
} from "@suaveplan/react-ui/theme/layer-stack";
|
||||
|
||||
bindRoot(document.documentElement);
|
||||
const providerId = newProviderId();
|
||||
const layerId = newLayerId("plugin");
|
||||
pushLayer({
|
||||
id: layerId,
|
||||
providerId,
|
||||
origin: "plugin",
|
||||
chassis: { primary: "rgb(10 10 10)" },
|
||||
extensions: { "--brand-font": "Inter" },
|
||||
});
|
||||
// later
|
||||
removeLayer(layerId);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
A consumer that wants to react to merged stack changes — for example a recharts colour palette consumer — subscribes via `subscribe()` and reads the live snapshot:
|
||||
|
||||
```ts
|
||||
import {
|
||||
getMergedResolved,
|
||||
subscribe,
|
||||
} from "@suaveplan/react-ui/theme/layer-stack";
|
||||
import { lightTheme } from "@suaveplan/react-ui/theme";
|
||||
|
||||
const unsubscribe = subscribe(() => {
|
||||
const { chassis, extensions } = getMergedResolved(lightTheme.cssVariables);
|
||||
repaint(chassis.primary, extensions["--brand-accent"] ?? chassis.accent);
|
||||
});
|
||||
|
||||
// teardown
|
||||
unsubscribe();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `newProviderId(): string` — mint a unique provider identifier.
|
||||
- `newLayerId(prefix: "plugin" | "mutator"): string` — mint a unique layer identifier.
|
||||
- `pushLayer({ id, providerId, origin, chassis?, extensions?, motionTokens?, fontTokens? })` — append a new layer and re-apply.
|
||||
- `updateLayer(id, { chassis?, extensions?, motionTokens?, fontTokens? })` — replace a layer's bags (each bag is replaced, not merged).
|
||||
- `removeLayer(id)` — drop a layer and re-apply.
|
||||
- `subscribe(listener)` — register a change listener; returns an `unsubscribe` function.
|
||||
- `getMergedResolved(baseline)` — compute the live read state against a baseline chassis bag.
|
||||
- `snapshotLayers()` — read-only copy of the current layer list. For tests and `useSyncExternalStore`.
|
||||
- `chassisTokenToCssVar(name)` — `cardForeground` → `--card-foreground`.
|
||||
- `bindRoot(root)` / `__resetLayerStackForTests()` — test-only.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The merge function walks layers in mount order; the last layer to declare a name wins. After computing the merged set, every name that was previously tracked but is no longer present gets `removeProperty(...)`, which is how unmount cleanup falls out for free. Tracked names are never forgotten — that is intentional, because a name first written by layer N and later supplied only by layer M still has to be removed if M itself unmounts.
|
||||
|
||||
`getMergedResolved` deliberately copies its inputs into fresh `Record<string,string>` bags so consumers cannot mutate the live layer storage by accident. The returned `extensions` bag is frozen.
|
||||
|
||||
`pushLayer` notifies AFTER applying the DOM, never before. This guarantees that a listener reading `getMergedResolved(...)` (or `getPropertyValue(...)`) on the next tick sees the post-write state. A pre-write notify would race with `useSyncExternalStore`.
|
||||
Reference in New Issue
Block a user