@shobman/strata-ui
v0.1.0
Published
Composable layout protocol for React — named slot projection via portals
Maintainers
Readme
@shobman/strata-ui
Runtime components for the Strata layout protocol. Named slot projection via React portals with zero-context-rerender architecture.
Installation
npm install @shobman/strata-uiPeer dependencies: react ^18 || ^19, react-dom ^18 || ^19
API
<SlotProvider>
Creates an independent slot registry. Wrap each layout that declares slots.
import { SlotProvider } from "@shobman/strata-ui";
function MyLayout() {
return (
<SlotProvider>
<SlotTarget name="header" />
<main>
<Outlet />
</main>
</SlotProvider>
);
}| Prop | Type | Description |
|------|------|-------------|
| children | ReactNode | Layout content containing SlotTarget and child routes |
Each SlotProvider instance gets its own registry. Nested providers do not share state with ancestors — this is by design, so each layout level manages its own slots independently.
<SlotTarget>
Declares a named slot in a layout. Renders a <div> when filled, returns null when empty.
import { SlotTarget } from "@shobman/strata-ui";
<SlotTarget name="actions" />
// Renders: <div data-strata-slot="actions">...projected content...</div>
// Or: null (when no FillSlot targets this name)| Prop | Type | Description |
|------|------|-------------|
| name | string | Slot name matching _contract.yml slot declarations |
Container visibility: When no FillSlot targets a slot, SlotTarget returns null — the container doesn't render at all. This means layouts can conditionally show/hide regions based on whether child pages fill them.
data-strata-slot attribute: Every rendered SlotTarget includes a data-strata-slot attribute with the slot name. Use this for testing (screen.getByTestId patterns with wrapper elements) and debugging (inspect the DOM to see which slots are active).
<FillSlot>
Projects content into a named SlotTarget via a React portal.
import { FillSlot } from "@shobman/strata-ui";
function PerformancePage() {
return (
<>
<FillSlot name="tabs">
<TabStrip items={["Returns", "Benchmark", "Risk"]} />
</FillSlot>
<FillSlot name="actions">
<Button>Run Benchmark</Button>
</FillSlot>
<PerformanceCharts />
</>
);
}| Prop | Type | Description |
|------|------|-------------|
| name | string | Slot name to project into |
| children | ReactNode | Content to render inside the slot |
Portal stack behaviour: Multiple FillSlot components targeting the same slot form a stack. The most recently mounted (deepest in the route tree) fill renders. When it unmounts, the previous fill reasserts.
/funds/3/performance
actions slot stack:
performance fills: [Run Benchmark] [Compare] ← top, rendered
[fundId] fills: [Edit Fund] [Export] ← underneath
Navigate to /funds/3/overview (doesn't fill actions):
[fundId] fills: [Edit Fund] [Export] ← now on top, renderedSelf-filling slots: A layout can fill its own slots to provide defaults that child routes can override:
function FundsLayout() {
return (
<>
<FillSlot name="listActions">
<Button>Default Action</Button>
</FillSlot>
<SlotProvider>
<SlotTarget name="listActions" />
<Outlet />
</SlotProvider>
</>
);
}Design Highlights
useSyncExternalStore architecture: Both SlotTarget and FillSlot subscribe to the slot registry using React's useSyncExternalStore. This means:
- Per-slot subscriptions — only components interested in a specific slot re-render when it changes
- Zero context-driven re-renders — the registry is a ref-stable object, not context state
- Concurrent mode compatible —
useSyncExternalStorehandles tearing prevention
Exported Types
import type {
SlotProviderProps,
SlotTargetProps,
FillSlotProps,
StrataContract,
StrataLevel,
StrataLayout,
} from "@shobman/strata-ui";| Type | Description |
|------|-------------|
| SlotProviderProps | { children: ReactNode } |
| SlotTargetProps | { name: string } |
| FillSlotProps | { name: string; children: ReactNode } |
| StrataContract | Full contract shape matching _contract.yml |
| StrataLevel | { name: "atom" \| "molecule" \| "organism" \| "route"; rank: 0 \| 1 \| 2 \| 3 } |
| StrataLayout | { slots?: Record<string, "required" \| null>; default?: string; redirect?: string } |
