@echothink-ui/layout 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +92 -0
  2. package/dist/index.cjs +1620 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.css +149 -0
  5. package/dist/index.css.map +1 -0
  6. package/dist/index.d.ts +24 -0
  7. package/dist/index.js +1546 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/layout-system/builders.d.ts +13 -0
  10. package/dist/layout-system/index.d.ts +24 -0
  11. package/dist/layout-system/inference/context.d.ts +33 -0
  12. package/dist/layout-system/inference/responsive.d.ts +21 -0
  13. package/dist/layout-system/inference/style.d.ts +15 -0
  14. package/dist/layout-system/page-layouts/index.d.ts +8 -0
  15. package/dist/layout-system/primitives/index.d.ts +6 -0
  16. package/dist/layout-system/regions/index.d.ts +4 -0
  17. package/dist/layout-system/registry/builtins.d.ts +8 -0
  18. package/dist/layout-system/registry/registry.d.ts +20 -0
  19. package/dist/layout-system/renderer/context.d.ts +41 -0
  20. package/dist/layout-system/renderer/region.d.ts +10 -0
  21. package/dist/layout-system/renderer/renderer.d.ts +13 -0
  22. package/dist/layout-system/renderer/root.d.ts +24 -0
  23. package/dist/layout-system/runtime/state.d.ts +17 -0
  24. package/dist/layout-system/runtime/viewport.d.ts +9 -0
  25. package/dist/layout-system/schema/types.d.ts +488 -0
  26. package/dist/layout-system/schema/validate.d.ts +15 -0
  27. package/dist/layout-system/tokens/preset-tokens.d.ts +11 -0
  28. package/package.json +47 -0
  29. package/src/index.tsx +42 -0
  30. package/src/layout-system/__tests__/layout-system.test.tsx +169 -0
  31. package/src/layout-system/builders.ts +46 -0
  32. package/src/layout-system/index.ts +87 -0
  33. package/src/layout-system/inference/context.ts +158 -0
  34. package/src/layout-system/inference/responsive.ts +147 -0
  35. package/src/layout-system/inference/style.ts +128 -0
  36. package/src/layout-system/page-layouts/index.tsx +405 -0
  37. package/src/layout-system/primitives/index.tsx +266 -0
  38. package/src/layout-system/regions/index.tsx +90 -0
  39. package/src/layout-system/registry/builtins.ts +19 -0
  40. package/src/layout-system/registry/registry.ts +47 -0
  41. package/src/layout-system/renderer/context.tsx +89 -0
  42. package/src/layout-system/renderer/region.tsx +34 -0
  43. package/src/layout-system/renderer/renderer.tsx +200 -0
  44. package/src/layout-system/renderer/root.tsx +95 -0
  45. package/src/layout-system/runtime/state.ts +80 -0
  46. package/src/layout-system/runtime/viewport.ts +71 -0
  47. package/src/layout-system/schema/types.ts +706 -0
  48. package/src/layout-system/schema/validate.ts +168 -0
  49. package/src/layout-system/tokens/preset-tokens.ts +77 -0
  50. package/src/styles.css +178 -0
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Layout primitives (`layout-spec.md` §14, §19): Panel, SplitPane, Tabs, Stepper.
3
+ *
4
+ * Each is a `LayoutRegistryItem` whose renderer arranges the engine-rendered
5
+ * `slots` map. Tabs/Stepper read their `composition` and render inline content
6
+ * via `SlotContentRenderer`, carrying a11y semantics (§12).
7
+ */
8
+ import * as React from "react";
9
+ import type {
10
+ LayoutRegistryItem,
11
+ LayoutRendererProps,
12
+ ParallelComposition,
13
+ StepperComposition,
14
+ SwitchComposition,
15
+ } from "../schema/types.js";
16
+ import { SlotContentRenderer } from "../renderer/renderer.js";
17
+ import { inlineSlot } from "../renderer/region.js";
18
+
19
+ /* ----------------------------- Panel ----------------------------- */
20
+
21
+ function PanelRenderer({ slots, context }: LayoutRendererProps) {
22
+ return (
23
+ <section
24
+ className="eth-ls-panel"
25
+ style={{
26
+ background: context.tokens.surface,
27
+ border: `1px solid ${context.tokens.border}`,
28
+ borderRadius: context.tokens.radius,
29
+ boxShadow: context.tokens.shadow,
30
+ color: context.tokens.text,
31
+ }}
32
+ >
33
+ {slots.header ? <header className="eth-ls-panel__header">{slots.header}</header> : null}
34
+ <div className="eth-ls-panel__body">{slots.body}</div>
35
+ {slots.footer ? <footer className="eth-ls-panel__footer">{slots.footer}</footer> : null}
36
+ </section>
37
+ );
38
+ }
39
+
40
+ export const PanelLayout: LayoutRegistryItem = {
41
+ type: "Primitive.Panel",
42
+ displayName: "Panel",
43
+ tier: "primitive",
44
+ category: "primitive",
45
+ description: "A bounded surface with optional header/body/footer.",
46
+ slots: [
47
+ { name: "header", role: "toolbar", accepts: [{ kind: "component" }, { kind: "fragment" }] },
48
+ {
49
+ name: "body",
50
+ role: "content",
51
+ required: true,
52
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
53
+ },
54
+ { name: "footer", role: "footer", accepts: [{ kind: "component" }, { kind: "fragment" }] },
55
+ ],
56
+ defaultStyleIntent: { surface: "panel" },
57
+ renderer: PanelRenderer,
58
+ };
59
+
60
+ /* --------------------------- SplitPane --------------------------- */
61
+
62
+ function SplitPaneRenderer({ slots, context, composition, variant }: LayoutRendererProps) {
63
+ const orientation = variant === "vertical" ? "vertical" : "horizontal";
64
+ const parallel = composition?.op === "parallel" ? (composition as ParallelComposition) : undefined;
65
+ const primaryBasis = parallel?.sizing?.primary?.basis;
66
+ return (
67
+ <div
68
+ className={`eth-ls-split eth-ls-split--${orientation}`}
69
+ style={{
70
+ display: "grid",
71
+ gap: parallel?.gapToken ? `var(${parallel.gapToken})` : context.tokens.gap,
72
+ gridTemplateColumns:
73
+ orientation === "horizontal"
74
+ ? `${primaryBasis ? cssSize(primaryBasis) : "1fr"} 1fr`
75
+ : undefined,
76
+ gridTemplateRows: orientation === "vertical" ? `${primaryBasis ? cssSize(primaryBasis) : "1fr"} 1fr` : undefined,
77
+ height: "100%",
78
+ minHeight: 0,
79
+ }}
80
+ >
81
+ <div className="eth-ls-split__pane eth-ls-split__pane--primary" style={{ minWidth: 0, minHeight: 0 }}>
82
+ {slots.primary}
83
+ </div>
84
+ <div
85
+ className="eth-ls-split__pane eth-ls-split__pane--secondary"
86
+ role="separator"
87
+ aria-orientation={orientation}
88
+ style={{ minWidth: 0, minHeight: 0 }}
89
+ >
90
+ {slots.secondary}
91
+ </div>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ function cssSize(value: number | string): string {
97
+ return typeof value === "number" ? `${value}px` : value;
98
+ }
99
+
100
+ export const SplitPaneLayout: LayoutRegistryItem = {
101
+ type: "Primitive.SplitPane",
102
+ displayName: "Split Pane",
103
+ tier: "primitive",
104
+ category: "primitive",
105
+ description: "Two resizable panes split horizontally or vertically.",
106
+ variants: [
107
+ { id: "horizontal", label: "Left / Right" },
108
+ { id: "vertical", label: "Top / Bottom" },
109
+ ],
110
+ slots: [
111
+ {
112
+ name: "primary",
113
+ role: "primary",
114
+ required: true,
115
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "template" }, { kind: "fragment" }],
116
+ },
117
+ {
118
+ name: "secondary",
119
+ role: "secondary",
120
+ required: true,
121
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "template" }, { kind: "fragment" }],
122
+ },
123
+ ],
124
+ defaultComposition: {
125
+ op: "parallel",
126
+ axis: "x",
127
+ slots: ["primary", "secondary"],
128
+ sizing: { primary: { basis: "1fr", resizable: true }, secondary: { grow: 1 } },
129
+ },
130
+ renderer: SplitPaneRenderer,
131
+ };
132
+
133
+ /* ----------------------------- Tabs ------------------------------ */
134
+
135
+ function TabsRenderer({ node, context, composition }: LayoutRendererProps) {
136
+ const switchSpec =
137
+ composition?.op === "switch" ? (composition as SwitchComposition) : undefined;
138
+ const items = switchSpec?.items ?? [];
139
+ const initial = switchSpec?.activeKey ?? items[0]?.key;
140
+ const [active, setActive] = React.useState<string | undefined>(initial);
141
+ const activeItem = items.find((i) => i.key === active) ?? items[0];
142
+
143
+ return (
144
+ <div className="eth-ls-tabs" style={{ color: context.tokens.text }}>
145
+ <div className="eth-ls-tabs__list" role="tablist">
146
+ {items.map((item) => {
147
+ const selected = item.key === activeItem?.key;
148
+ return (
149
+ <button
150
+ key={item.key}
151
+ type="button"
152
+ role="tab"
153
+ id={`tab-${node.id}-${item.key}`}
154
+ aria-selected={selected}
155
+ aria-controls={`tabpanel-${node.id}-${item.key}`}
156
+ tabIndex={selected ? 0 : -1}
157
+ className={`eth-ls-tabs__tab${selected ? " eth-ls-tabs__tab--active" : ""}`}
158
+ onClick={() => setActive(item.key)}
159
+ >
160
+ {item.label}
161
+ </button>
162
+ );
163
+ })}
164
+ </div>
165
+ {activeItem ? (
166
+ <div
167
+ className="eth-ls-tabs__panel"
168
+ role="tabpanel"
169
+ id={`tabpanel-${node.id}-${activeItem.key}`}
170
+ aria-labelledby={`tab-${node.id}-${activeItem.key}`}
171
+ >
172
+ <SlotContentRenderer
173
+ node={node}
174
+ slot={inlineSlot(`tab:${activeItem.key}`, "content")}
175
+ content={activeItem.content}
176
+ parentContext={context}
177
+ />
178
+ </div>
179
+ ) : null}
180
+ </div>
181
+ );
182
+ }
183
+
184
+ export const TabsLayout: LayoutRegistryItem = {
185
+ type: "Primitive.Tabs",
186
+ displayName: "Tabs",
187
+ tier: "primitive",
188
+ category: "primitive",
189
+ description: "Switch composition rendering one panel at a time with tablist semantics.",
190
+ slots: [],
191
+ defaultComposition: { op: "switch", mode: "tabs", items: [] },
192
+ renderer: TabsRenderer,
193
+ };
194
+
195
+ /* ---------------------------- Stepper ---------------------------- */
196
+
197
+ function StepperRenderer({ node, context, composition }: LayoutRendererProps) {
198
+ const stepper =
199
+ composition?.op === "stepper" ? (composition as StepperComposition) : undefined;
200
+ const steps = stepper?.steps ?? [];
201
+ const firstActive = steps.findIndex((s) => s.status === "active");
202
+ const [activeIndex, setActiveIndex] = React.useState<number>(firstActive >= 0 ? firstActive : 0);
203
+ const orientation = stepper?.orientation ?? "horizontal";
204
+ const activeStep = steps[activeIndex];
205
+
206
+ return (
207
+ <div
208
+ className={`eth-ls-stepper eth-ls-stepper--${orientation}`}
209
+ style={{ display: "flex", flexDirection: orientation === "vertical" ? "row" : "column", gap: context.tokens.gap, color: context.tokens.text }}
210
+ >
211
+ <ol className="eth-ls-stepper__steps" aria-label="Steps">
212
+ {steps.map((step, index) => {
213
+ const current = index === activeIndex;
214
+ return (
215
+ <li
216
+ key={step.id}
217
+ className={`eth-ls-stepper__step eth-ls-stepper__step--${step.status ?? "pending"}${current ? " eth-ls-stepper__step--current" : ""}`}
218
+ aria-current={current ? "step" : undefined}
219
+ >
220
+ <button type="button" className="eth-ls-stepper__step-button" onClick={() => setActiveIndex(index)}>
221
+ <span className="eth-ls-stepper__index">{index + 1}</span>
222
+ <span className="eth-ls-stepper__label">{step.label}</span>
223
+ </button>
224
+ </li>
225
+ );
226
+ })}
227
+ </ol>
228
+ {activeStep ? (
229
+ <div className="eth-ls-stepper__content">
230
+ <SlotContentRenderer
231
+ node={node}
232
+ slot={inlineSlot(`step:${activeStep.id}`, "form")}
233
+ content={activeStep.content}
234
+ parentContext={context}
235
+ />
236
+ </div>
237
+ ) : null}
238
+ </div>
239
+ );
240
+ }
241
+
242
+ export const StepperLayout: LayoutRegistryItem = {
243
+ type: "Primitive.Stepper",
244
+ displayName: "Stepper",
245
+ tier: "primitive",
246
+ category: "primitive",
247
+ description: "Flow-oriented step container with validation/navigation semantics.",
248
+ variants: [{ id: "horizontal" }, { id: "vertical" }, { id: "with-preview" }],
249
+ slots: [],
250
+ defaultComposition: {
251
+ op: "stepper",
252
+ orientation: "horizontal",
253
+ stepSlot: "stepper",
254
+ contentSlot: "content",
255
+ steps: [],
256
+ },
257
+ defaultStyleIntent: { taskMode: "wizard", visualEmphasis: "balanced" },
258
+ renderer: StepperRenderer,
259
+ };
260
+
261
+ export const primitiveLayouts: LayoutRegistryItem[] = [
262
+ PanelLayout,
263
+ SplitPaneLayout,
264
+ TabsLayout,
265
+ StepperLayout,
266
+ ];
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Region layouts (`layout-spec.md` §14, §19): Toolbar, Inspector.
3
+ */
4
+ import * as React from "react";
5
+ import type { LayoutRegistryItem, LayoutRendererProps } from "../schema/types.js";
6
+
7
+ /* ---------------------------- Toolbar ---------------------------- */
8
+
9
+ function ToolbarRenderer({ slots, context }: LayoutRendererProps) {
10
+ return (
11
+ <div
12
+ className="eth-ls-toolbar"
13
+ role="toolbar"
14
+ style={{
15
+ display: "flex",
16
+ alignItems: "center",
17
+ gap: context.tokens.gap,
18
+ background: context.tokens.surfaceMuted,
19
+ borderBottom: `1px solid ${context.tokens.border}`,
20
+ color: context.tokens.text,
21
+ }}
22
+ >
23
+ {slots.start ? <div className="eth-ls-toolbar__start">{slots.start}</div> : null}
24
+ {slots.center ? <div className="eth-ls-toolbar__center" style={{ flex: 1, minWidth: 0 }}>{slots.center}</div> : <div style={{ flex: 1 }} />}
25
+ {slots.end ? <div className="eth-ls-toolbar__end">{slots.end}</div> : null}
26
+ </div>
27
+ );
28
+ }
29
+
30
+ export const ToolbarLayout: LayoutRegistryItem = {
31
+ type: "Region.Toolbar",
32
+ displayName: "Toolbar",
33
+ tier: "region",
34
+ category: "region",
35
+ description: "Horizontal toolbar with start/center/end groups and roving focus.",
36
+ slots: [
37
+ { name: "start", role: "toolbar", accepts: [{ kind: "component" }, { kind: "fragment" }] },
38
+ { name: "center", role: "toolbar", accepts: [{ kind: "component" }, { kind: "fragment" }] },
39
+ { name: "end", role: "toolbar", accepts: [{ kind: "component" }, { kind: "fragment" }] },
40
+ ],
41
+ defaultStyleIntent: { surface: "region" },
42
+ renderer: ToolbarRenderer,
43
+ };
44
+
45
+ /* --------------------------- Inspector --------------------------- */
46
+
47
+ function InspectorRenderer({ slots, context }: LayoutRendererProps) {
48
+ return (
49
+ <aside
50
+ className="eth-ls-inspector"
51
+ aria-label="Inspector"
52
+ style={{
53
+ display: "flex",
54
+ flexDirection: "column",
55
+ minHeight: 0,
56
+ background: context.tokens.surface,
57
+ borderInlineStart: `1px solid ${context.tokens.border}`,
58
+ color: context.tokens.text,
59
+ }}
60
+ >
61
+ {slots.header ? <header className="eth-ls-inspector__header">{slots.header}</header> : null}
62
+ <div className="eth-ls-inspector__body" style={{ overflow: "auto", flex: 1, minHeight: 0 }}>
63
+ {slots.body}
64
+ </div>
65
+ {slots.footer ? <footer className="eth-ls-inspector__footer">{slots.footer}</footer> : null}
66
+ </aside>
67
+ );
68
+ }
69
+
70
+ export const InspectorLayout: LayoutRegistryItem = {
71
+ type: "Region.Inspector",
72
+ displayName: "Inspector",
73
+ tier: "region",
74
+ category: "region",
75
+ description: "An inline-end inspector panel with header/body/footer.",
76
+ slots: [
77
+ { name: "header", role: "toolbar", accepts: [{ kind: "component" }, { kind: "fragment" }] },
78
+ {
79
+ name: "body",
80
+ role: "inspector",
81
+ required: true,
82
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
83
+ },
84
+ { name: "footer", role: "footer", accepts: [{ kind: "component" }, { kind: "fragment" }] },
85
+ ],
86
+ defaultStyleIntent: { surface: "panel", interactionMode: "edit" },
87
+ renderer: InspectorRenderer,
88
+ };
89
+
90
+ export const regionLayouts: LayoutRegistryItem[] = [ToolbarLayout, InspectorLayout];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Builtin layout catalog + default registry factory (`layout-spec.md` §19).
3
+ */
4
+ import type { LayoutRegistryItem } from "../schema/types.js";
5
+ import { LayoutRegistry } from "./registry.js";
6
+ import { primitiveLayouts } from "../primitives/index.js";
7
+ import { regionLayouts } from "../regions/index.js";
8
+ import { pageLayouts } from "../page-layouts/index.js";
9
+
10
+ export const builtinLayouts: LayoutRegistryItem[] = [
11
+ ...primitiveLayouts,
12
+ ...regionLayouts,
13
+ ...pageLayouts,
14
+ ];
15
+
16
+ /** A registry preloaded with the MVP builtin layouts. */
17
+ export function createDefaultLayoutRegistry(): LayoutRegistry {
18
+ return new LayoutRegistry().registerAll(builtinLayouts);
19
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Layout registry (`layout-spec.md` §9).
3
+ *
4
+ * Maps a layout `type` (e.g. "PageLayout.Workbench") to its `LayoutRegistryItem`
5
+ * (slots, renderer, default composition/style intent). The recursive renderer
6
+ * and the validator consult it; it is also the source the builder/copilot read
7
+ * to discover available layouts and their slot contracts.
8
+ */
9
+ import type { LayoutRegistryItem } from "../schema/types.js";
10
+
11
+ export class LayoutRegistry {
12
+ private readonly items = new Map<string, LayoutRegistryItem>();
13
+
14
+ register(item: LayoutRegistryItem): this {
15
+ this.items.set(item.type, item);
16
+ return this;
17
+ }
18
+
19
+ registerAll(items: readonly LayoutRegistryItem[]): this {
20
+ for (const item of items) this.register(item);
21
+ return this;
22
+ }
23
+
24
+ get(type: string): LayoutRegistryItem | undefined {
25
+ return this.items.get(type);
26
+ }
27
+
28
+ has(type: string): boolean {
29
+ return this.items.has(type);
30
+ }
31
+
32
+ list(): LayoutRegistryItem[] {
33
+ return [...this.items.values()];
34
+ }
35
+
36
+ types(): string[] {
37
+ return [...this.items.keys()];
38
+ }
39
+
40
+ byTier(tier: LayoutRegistryItem["tier"]): LayoutRegistryItem[] {
41
+ return this.list().filter((i) => i.tier === tier);
42
+ }
43
+
44
+ byCategory(category: LayoutRegistryItem["category"]): LayoutRegistryItem[] {
45
+ return this.list().filter((i) => i.category === category);
46
+ }
47
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * React contexts for the layout engine.
3
+ *
4
+ * Two contexts:
5
+ * - `LayoutRuntimeContext` — the per-node runtime context (style/surface/depth/
6
+ * tokens) that flows down and is recomposed at each slot boundary.
7
+ * - `LayoutEngine` — the engine config (registry + resolvers + state store +
8
+ * diagnostics sink). Decoupled from any component catalog: the host injects a
9
+ * `componentResolver` and optional `templateResolver`.
10
+ */
11
+ import * as React from "react";
12
+ import type {
13
+ LayoutDiagnostic,
14
+ LayoutNode,
15
+ LayoutRuntimeContext,
16
+ } from "../schema/types.js";
17
+ import { createRootContext } from "../inference/context.js";
18
+ import { LayoutRegistry } from "../registry/registry.js";
19
+ import type { LayoutStateStore } from "../runtime/state.js";
20
+ import { createMemoryLayoutStateStore } from "../runtime/state.js";
21
+
22
+ export type ComponentResolver = (
23
+ name: string,
24
+ ) => React.ComponentType<Record<string, unknown>> | undefined;
25
+
26
+ export type TemplateResolver = (name: string) => LayoutNode | undefined;
27
+
28
+ export interface LayoutEngine {
29
+ registry: LayoutRegistry;
30
+ componentResolver: ComponentResolver;
31
+ templateResolver?: TemplateResolver;
32
+ stateStore: LayoutStateStore;
33
+ onDiagnostics?: (diagnostics: LayoutDiagnostic[]) => void;
34
+ }
35
+
36
+ const RuntimeContext = React.createContext<LayoutRuntimeContext>(createRootContext());
37
+ const EngineContext = React.createContext<LayoutEngine | null>(null);
38
+
39
+ export function LayoutContextProvider({
40
+ value,
41
+ children,
42
+ }: {
43
+ value: LayoutRuntimeContext;
44
+ children: React.ReactNode;
45
+ }) {
46
+ return <RuntimeContext.Provider value={value}>{children}</RuntimeContext.Provider>;
47
+ }
48
+
49
+ export function useLayoutContext(): LayoutRuntimeContext {
50
+ return React.useContext(RuntimeContext);
51
+ }
52
+
53
+ export function LayoutEngineProvider({
54
+ value,
55
+ children,
56
+ }: {
57
+ value: LayoutEngine;
58
+ children: React.ReactNode;
59
+ }) {
60
+ return <EngineContext.Provider value={value}>{children}</EngineContext.Provider>;
61
+ }
62
+
63
+ export function useLayoutEngine(): LayoutEngine {
64
+ const engine = React.useContext(EngineContext);
65
+ if (!engine) {
66
+ throw new Error(
67
+ "useLayoutEngine must be used within <LayoutRoot> / <LayoutEngineProvider>.",
68
+ );
69
+ }
70
+ return engine;
71
+ }
72
+
73
+ export interface BuildEngineInput {
74
+ registry: LayoutRegistry;
75
+ componentResolver: ComponentResolver;
76
+ templateResolver?: TemplateResolver;
77
+ stateStore?: LayoutStateStore;
78
+ onDiagnostics?: (diagnostics: LayoutDiagnostic[]) => void;
79
+ }
80
+
81
+ export function buildEngine(input: BuildEngineInput): LayoutEngine {
82
+ return {
83
+ registry: input.registry,
84
+ componentResolver: input.componentResolver,
85
+ templateResolver: input.templateResolver,
86
+ stateStore: input.stateStore ?? createMemoryLayoutStateStore(),
87
+ onDiagnostics: input.onDiagnostics,
88
+ };
89
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Renderer helpers shared by primitive/region/page-layout renderers.
3
+ */
4
+ import type {
5
+ NodeAcceptSpec,
6
+ PhysicalLayoutPlan,
7
+ SlotDefinition,
8
+ SlotRole,
9
+ } from "../schema/types.js";
10
+ import { regionFor } from "../inference/responsive.js";
11
+
12
+ const ANY_ACCEPTS: NodeAcceptSpec[] = [
13
+ { kind: "component" },
14
+ { kind: "layout" },
15
+ { kind: "template" },
16
+ { kind: "fragment" },
17
+ { kind: "empty" },
18
+ ];
19
+
20
+ /** Synthesize a slot definition for composition-embedded content (tabs/steps). */
21
+ export function inlineSlot(name: string, role: SlotRole = "content"): SlotDefinition {
22
+ return { name, role, accepts: ANY_ACCEPTS };
23
+ }
24
+
25
+ /** Whether a slot's physical region is visible (default true when absent). */
26
+ export function isVisible(plan: PhysicalLayoutPlan, slot: string): boolean {
27
+ const region = regionFor(plan, slot);
28
+ return region ? region.visible : true;
29
+ }
30
+
31
+ /** Whether a slot's physical region is collapsed. */
32
+ export function isCollapsed(plan: PhysicalLayoutPlan, slot: string): boolean {
33
+ return regionFor(plan, slot)?.collapsed ?? false;
34
+ }