@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,128 @@
1
+ /**
2
+ * Component style inference (`layout-spec.md` §7).
3
+ *
4
+ * Components do NOT pick their own preset/density/typeScale. The engine infers
5
+ * `ComponentStyleParams` from the layout runtime context using a scoring
6
+ * resolver for `preset` plus rule tables for `density` and `typeScale`.
7
+ */
8
+ import type {
9
+ ComponentDensity,
10
+ ComponentStyleParams,
11
+ ComponentStylePreset,
12
+ ComponentTypeScale,
13
+ StyleInferenceInput,
14
+ } from "../schema/types.js";
15
+
16
+ export const defaultComponentStyle: ComponentStyleParams = {
17
+ preset: "carbon-like",
18
+ palette: "default",
19
+ density: "standard",
20
+ typeScale: "standard",
21
+ };
22
+
23
+ function matches(value: string | undefined, candidates: string[]): boolean {
24
+ if (!value) return false;
25
+ return candidates.some((c) => value === c || value.endsWith(`.${c}`) || value.includes(c));
26
+ }
27
+
28
+ function chooseHighestScore(
29
+ scores: Record<ComponentStylePreset, number>,
30
+ fallback: ComponentStylePreset,
31
+ ): ComponentStylePreset {
32
+ let best: ComponentStylePreset = fallback;
33
+ let bestScore = -Infinity;
34
+ // Deterministic: iterate in a fixed key order.
35
+ const order: ComponentStylePreset[] = [
36
+ "carbon-like",
37
+ "soft-card",
38
+ "glass",
39
+ "bright",
40
+ "studio-dark",
41
+ ];
42
+ for (const key of order) {
43
+ if (scores[key] > bestScore) {
44
+ bestScore = scores[key];
45
+ best = key;
46
+ }
47
+ }
48
+ return bestScore <= 0 ? fallback : best;
49
+ }
50
+
51
+ /** §7.4 density rules. */
52
+ export function inferDensity(input: StyleInferenceInput): ComponentDensity {
53
+ if (input.explicit?.density) return input.explicit.density;
54
+ const role = input.slotRole;
55
+ if (input.dataIntensity === "high" || input.dataIntensity === "realtime") return "compact";
56
+ if (
57
+ matches(role, ["console", "timeline", "filter", "toolbar", "navigation"]) ||
58
+ matches(input.layoutType, ["DataGridInspector", "MonitoringOps"])
59
+ ) {
60
+ return "compact";
61
+ }
62
+ if (input.visualEmphasis === "expressive" || input.visualEmphasis === "immersive") {
63
+ return "comfortable";
64
+ }
65
+ if (matches(role, ["form", "inspector", "summary", "content", "preview"])) return "standard";
66
+ if (input.taskMode === "wizard" || input.taskMode === "copilot") return "comfortable";
67
+ if (input.breakpoint === "mobile" && input.parent?.density === "compact") return "standard";
68
+ return input.parent?.density ?? "standard";
69
+ }
70
+
71
+ /** §7.5 typeScale rules. */
72
+ export function inferTypeScale(input: StyleInferenceInput): ComponentTypeScale {
73
+ if (input.explicit?.typeScale) return input.explicit.typeScale;
74
+ const role = input.slotRole;
75
+ if (typeof input.containerWidth === "number" && input.containerWidth < 360) return "compact";
76
+ if (matches(role, ["toolbar", "navigation", "console", "timeline"])) return "compact";
77
+ if (matches(input.layoutType, ["DataGridInspector", "MonitoringOps"])) return "compact";
78
+ if (input.taskMode === "wizard" || matches(role, ["summary", "emptyState"])) return "large";
79
+ if (input.breakpoint === "mobile") return "standard";
80
+ if (matches(role, ["form", "inspector", "content", "preview"])) return "standard";
81
+ return input.parent?.typeScale ?? "standard";
82
+ }
83
+
84
+ /** §7.6 scoring preset resolver. */
85
+ export function inferComponentStyle(input: StyleInferenceInput): ComponentStyleParams {
86
+ const presetScores: Record<ComponentStylePreset, number> = {
87
+ "carbon-like": 0,
88
+ "soft-card": 0,
89
+ glass: 0,
90
+ bright: 0,
91
+ "studio-dark": 0,
92
+ };
93
+
94
+ if (matches(input.layoutType, ["AdminShell", "DataGridInspector", "SettingsConsole"])) {
95
+ presetScores["carbon-like"] += 4;
96
+ }
97
+ if (matches(input.layoutType, ["DashboardBento", "SearchCatalog"])) {
98
+ presetScores["soft-card"] += 4;
99
+ }
100
+ if (matches(input.layoutType, ["SpatialHud"]) || input.surface === "floating") {
101
+ presetScores.glass += 4;
102
+ }
103
+ if (matches(input.layoutType, ["WizardComposer"]) || input.taskMode === "wizard") {
104
+ presetScores.bright += 4;
105
+ }
106
+ if (
107
+ matches(input.layoutType, ["Workbench", "CanvasInspector", "MonitoringOps", "Playground"]) ||
108
+ matches(input.slotRole, ["console", "canvas", "timeline"])
109
+ ) {
110
+ presetScores["studio-dark"] += 4;
111
+ }
112
+
113
+ // Deep nested form/filter/inspector → bias toward stable carbon.
114
+ if (input.depth >= 2 && matches(input.slotRole, ["form", "filter", "inspector"])) {
115
+ presetScores["carbon-like"] += 2;
116
+ }
117
+
118
+ const preset =
119
+ input.explicit?.preset ??
120
+ chooseHighestScore(presetScores, input.parent?.preset ?? defaultComponentStyle.preset);
121
+
122
+ return {
123
+ preset,
124
+ palette: input.explicit?.palette ?? input.parent?.palette ?? "default",
125
+ density: inferDensity(input),
126
+ typeScale: inferTypeScale(input),
127
+ };
128
+ }
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Page layouts (`layout-spec.md` §14, §19): AdminShell, DataGridInspector,
3
+ * Workbench, WizardComposer, MonitoringOps, CanvasInspector.
4
+ *
5
+ * Each renderer consumes the resolved `plan` so the same AST yields different
6
+ * desktop/tablet/mobile arrangements (collapsed/hidden supporting panes).
7
+ */
8
+ import * as React from "react";
9
+ import type { LayoutRegistryItem, LayoutRendererProps } from "../schema/types.js";
10
+ import { isVisible, isCollapsed } from "../renderer/region.js";
11
+
12
+ function frameStyle(context: LayoutRendererProps["context"]): React.CSSProperties {
13
+ return {
14
+ display: "grid",
15
+ height: "100%",
16
+ minHeight: 0,
17
+ background: context.tokens.surfaceMuted,
18
+ color: context.tokens.text,
19
+ };
20
+ }
21
+
22
+ /* --------------------------- AdminShell -------------------------- */
23
+
24
+ function AdminShellRenderer({ slots, context, plan }: LayoutRendererProps) {
25
+ const navVisible = slots.navigation && isVisible(plan, "navigation");
26
+ const navCollapsed = isCollapsed(plan, "navigation");
27
+ return (
28
+ <div
29
+ className="eth-ls-admin-shell"
30
+ style={{
31
+ ...frameStyle(context),
32
+ gridTemplateRows: "auto 1fr auto",
33
+ gridTemplateColumns: navVisible ? `${navCollapsed ? "3.5rem" : "16rem"} 1fr` : "1fr",
34
+ gridTemplateAreas: navVisible
35
+ ? `"topbar topbar" "nav content" "footer footer"`
36
+ : `"topbar" "content" "footer"`,
37
+ }}
38
+ >
39
+ {slots.topbar ? (
40
+ <header className="eth-ls-admin-shell__topbar" style={{ gridArea: "topbar" }}>
41
+ {slots.topbar}
42
+ </header>
43
+ ) : null}
44
+ {navVisible ? (
45
+ <nav className="eth-ls-admin-shell__nav" style={{ gridArea: "nav", overflow: "auto", borderInlineEnd: `1px solid ${context.tokens.border}` }}>
46
+ {slots.navigation}
47
+ </nav>
48
+ ) : null}
49
+ <main className="eth-ls-admin-shell__content" style={{ gridArea: "content", overflow: "auto", minWidth: 0 }}>
50
+ {slots.content}
51
+ </main>
52
+ {slots.footer ? (
53
+ <footer className="eth-ls-admin-shell__footer" style={{ gridArea: "footer", borderBlockStart: `1px solid ${context.tokens.border}` }}>
54
+ {slots.footer}
55
+ </footer>
56
+ ) : null}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ export const AdminShellLayout: LayoutRegistryItem = {
62
+ type: "PageLayout.AdminShell",
63
+ displayName: "Admin Shell",
64
+ tier: "page",
65
+ category: "app-shell",
66
+ description: "Enterprise CRUD shell: topbar, navigation rail, content, footer.",
67
+ slots: [
68
+ { name: "topbar", role: "toolbar", accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }] },
69
+ {
70
+ name: "navigation",
71
+ role: "navigation",
72
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
73
+ priority: { value: 40, behaviorOnNarrow: "move-to-drawer" },
74
+ responsive: { collapseBelow: "laptop" },
75
+ },
76
+ {
77
+ name: "content",
78
+ role: "content",
79
+ required: true,
80
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "template" }, { kind: "fragment" }],
81
+ priority: { value: 100, behaviorOnNarrow: "keep" },
82
+ },
83
+ { name: "footer", role: "footer", accepts: [{ kind: "component" }, { kind: "fragment" }] },
84
+ ],
85
+ defaultStyleIntent: { surface: "app", taskMode: "crud" },
86
+ renderer: AdminShellRenderer,
87
+ };
88
+
89
+ /* ----------------------- DataGridInspector ----------------------- */
90
+
91
+ function DataGridInspectorRenderer({ slots, context, plan }: LayoutRendererProps) {
92
+ const inspectorVisible = slots.inspector && isVisible(plan, "inspector");
93
+ return (
94
+ <div
95
+ className="eth-ls-datagrid"
96
+ style={{
97
+ ...frameStyle(context),
98
+ gridTemplateRows: "auto 1fr",
99
+ gridTemplateColumns: inspectorVisible ? "1fr minmax(18rem, 24rem)" : "1fr",
100
+ gridTemplateAreas: inspectorVisible ? `"toolbar toolbar" "grid inspector"` : `"toolbar" "grid"`,
101
+ }}
102
+ >
103
+ {slots.toolbar ? (
104
+ <div className="eth-ls-datagrid__toolbar" style={{ gridArea: "toolbar" }}>{slots.toolbar}</div>
105
+ ) : null}
106
+ <div className="eth-ls-datagrid__grid" style={{ gridArea: "grid", overflow: "auto", minWidth: 0 }}>
107
+ {slots.grid}
108
+ </div>
109
+ {inspectorVisible ? (
110
+ <div className="eth-ls-datagrid__inspector" style={{ gridArea: "inspector", overflow: "auto", borderInlineStart: `1px solid ${context.tokens.border}` }}>
111
+ {slots.inspector}
112
+ </div>
113
+ ) : null}
114
+ </div>
115
+ );
116
+ }
117
+
118
+ export const DataGridInspectorLayout: LayoutRegistryItem = {
119
+ type: "PageLayout.DataGridInspector",
120
+ displayName: "Data Grid + Inspector",
121
+ tier: "page",
122
+ category: "page",
123
+ description: "High-density grid with a toolbar and an inline-end inspector.",
124
+ slots: [
125
+ { name: "toolbar", role: "toolbar", accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }] },
126
+ {
127
+ name: "grid",
128
+ role: "content",
129
+ required: true,
130
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
131
+ priority: { value: 100, behaviorOnNarrow: "keep" },
132
+ },
133
+ {
134
+ name: "inspector",
135
+ role: "inspector",
136
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
137
+ priority: { value: 50, behaviorOnNarrow: "move-to-drawer" },
138
+ },
139
+ ],
140
+ defaultStyleIntent: { surface: "page", taskMode: "crud", dataIntensity: "high" },
141
+ renderer: DataGridInspectorRenderer,
142
+ };
143
+
144
+ /* ---------------------------- Workbench -------------------------- */
145
+
146
+ function WorkbenchRenderer({ slots, context, plan }: LayoutRendererProps) {
147
+ const explorerVisible = slots.explorer && isVisible(plan, "explorer");
148
+ const inspectorVisible = slots.inspector && isVisible(plan, "inspector");
149
+ const consoleVisible = slots.console && isVisible(plan, "console");
150
+ const cols = [explorerVisible ? "16rem" : null, "1fr", inspectorVisible ? "minmax(18rem, 22rem)" : null]
151
+ .filter(Boolean)
152
+ .join(" ");
153
+ const areaMid = [explorerVisible ? "explorer" : null, "primary", inspectorVisible ? "inspector" : null]
154
+ .filter(Boolean)
155
+ .join(" ");
156
+ return (
157
+ <div
158
+ className="eth-ls-workbench"
159
+ style={{
160
+ ...frameStyle(context),
161
+ gridTemplateColumns: cols,
162
+ gridTemplateRows: `auto 1fr ${consoleVisible ? "minmax(8rem, 16rem)" : "0"}`,
163
+ gridTemplateAreas: `"${new Array(cols.split(" ").length).fill("toolbar").join(" ")}" "${areaMid}" "${new Array(cols.split(" ").length).fill("console").join(" ")}"`,
164
+ }}
165
+ >
166
+ {slots.toolbar ? <div className="eth-ls-workbench__toolbar" style={{ gridArea: "toolbar" }}>{slots.toolbar}</div> : null}
167
+ {explorerVisible ? (
168
+ <nav className="eth-ls-workbench__explorer" style={{ gridArea: "explorer", overflow: "auto", borderInlineEnd: `1px solid ${context.tokens.border}` }}>
169
+ {slots.explorer}
170
+ </nav>
171
+ ) : null}
172
+ <main className="eth-ls-workbench__primary" style={{ gridArea: "primary", overflow: "auto", minWidth: 0, minHeight: 0 }}>
173
+ {slots.primary}
174
+ </main>
175
+ {inspectorVisible ? (
176
+ <div className="eth-ls-workbench__inspector" style={{ gridArea: "inspector", overflow: "auto", borderInlineStart: `1px solid ${context.tokens.border}` }}>
177
+ {slots.inspector}
178
+ </div>
179
+ ) : null}
180
+ {consoleVisible ? (
181
+ <div className="eth-ls-workbench__console" style={{ gridArea: "console", overflow: "auto", borderBlockStart: `1px solid ${context.tokens.border}` }}>
182
+ {slots.console}
183
+ </div>
184
+ ) : null}
185
+ </div>
186
+ );
187
+ }
188
+
189
+ export const WorkbenchLayout: LayoutRegistryItem = {
190
+ type: "PageLayout.Workbench",
191
+ displayName: "Workbench",
192
+ tier: "page",
193
+ category: "page",
194
+ description: "Authoring workbench: toolbar, explorer, primary canvas, inspector, console.",
195
+ variants: [{ id: "canvas-primary" }, { id: "split-primary" }],
196
+ slots: [
197
+ { name: "toolbar", role: "toolbar", accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }] },
198
+ {
199
+ name: "explorer",
200
+ role: "navigation",
201
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
202
+ priority: { value: 40, behaviorOnNarrow: "move-to-drawer" },
203
+ responsive: { collapseBelow: "laptop" },
204
+ },
205
+ {
206
+ name: "primary",
207
+ role: "primary",
208
+ required: true,
209
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "template" }, { kind: "fragment" }],
210
+ priority: { value: 100, behaviorOnNarrow: "keep" },
211
+ },
212
+ {
213
+ name: "inspector",
214
+ role: "inspector",
215
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
216
+ priority: { value: 50, behaviorOnNarrow: "move-to-drawer" },
217
+ },
218
+ {
219
+ name: "console",
220
+ role: "console",
221
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
222
+ priority: { value: 30, behaviorOnNarrow: "move-to-bottom-sheet" },
223
+ },
224
+ ],
225
+ defaultStyleIntent: { surface: "workspace", taskMode: "authoring", visualEmphasis: "immersive" },
226
+ renderer: WorkbenchRenderer,
227
+ };
228
+
229
+ /* ------------------------- WizardComposer ------------------------ */
230
+
231
+ function WizardComposerRenderer({ slots, context, plan }: LayoutRendererProps) {
232
+ const previewVisible = slots.preview && isVisible(plan, "preview");
233
+ return (
234
+ <div
235
+ className="eth-ls-wizard"
236
+ style={{
237
+ ...frameStyle(context),
238
+ gridTemplateColumns: previewVisible ? "1fr minmax(20rem, 28rem)" : "1fr",
239
+ gridTemplateRows: "auto 1fr auto",
240
+ gridTemplateAreas: previewVisible
241
+ ? `"stepper stepper" "content preview" "footer footer"`
242
+ : `"stepper" "content" "footer"`,
243
+ background: context.tokens.surface,
244
+ }}
245
+ >
246
+ {slots.stepper ? <div className="eth-ls-wizard__stepper" style={{ gridArea: "stepper" }}>{slots.stepper}</div> : null}
247
+ <main className="eth-ls-wizard__content" style={{ gridArea: "content", overflow: "auto", minWidth: 0 }}>
248
+ {slots.content}
249
+ </main>
250
+ {previewVisible ? (
251
+ <div className="eth-ls-wizard__preview" style={{ gridArea: "preview", overflow: "auto", borderInlineStart: `1px solid ${context.tokens.border}` }}>
252
+ {slots.preview}
253
+ </div>
254
+ ) : null}
255
+ {slots.footerActions ? (
256
+ <footer className="eth-ls-wizard__footer" style={{ gridArea: "footer", borderBlockStart: `1px solid ${context.tokens.border}` }}>
257
+ {slots.footerActions}
258
+ </footer>
259
+ ) : null}
260
+ </div>
261
+ );
262
+ }
263
+
264
+ export const WizardComposerLayout: LayoutRegistryItem = {
265
+ type: "PageLayout.WizardComposer",
266
+ displayName: "Wizard Composer",
267
+ tier: "page",
268
+ category: "page",
269
+ description: "Guided multi-step creation flow with optional live preview.",
270
+ slots: [
271
+ { name: "stepper", role: "stepper", accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }] },
272
+ {
273
+ name: "content",
274
+ role: "form",
275
+ required: true,
276
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "template" }, { kind: "fragment" }],
277
+ priority: { value: 100, behaviorOnNarrow: "keep" },
278
+ },
279
+ {
280
+ name: "preview",
281
+ role: "preview",
282
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
283
+ priority: { value: 40, behaviorOnNarrow: "hide" },
284
+ },
285
+ { name: "footerActions", role: "footer", accepts: [{ kind: "component" }, { kind: "fragment" }] },
286
+ ],
287
+ defaultStyleIntent: { surface: "page", taskMode: "wizard", visualEmphasis: "balanced" },
288
+ renderer: WizardComposerRenderer,
289
+ };
290
+
291
+ /* ------------------------- MonitoringOps ------------------------- */
292
+
293
+ function MonitoringOpsRenderer({ slots, context, plan }: LayoutRendererProps) {
294
+ const consoleVisible = slots.console && isVisible(plan, "console");
295
+ return (
296
+ <div
297
+ className="eth-ls-monitoring"
298
+ style={{
299
+ ...frameStyle(context),
300
+ gridTemplateRows: `auto auto 1fr ${consoleVisible ? "minmax(8rem, 14rem)" : "0"}`,
301
+ gridTemplateAreas: `"toolbar" "metrics" "primary" "console"`,
302
+ }}
303
+ >
304
+ {slots.toolbar ? <div className="eth-ls-monitoring__toolbar" style={{ gridArea: "toolbar" }}>{slots.toolbar}</div> : null}
305
+ {slots.metrics ? <div className="eth-ls-monitoring__metrics" style={{ gridArea: "metrics" }}>{slots.metrics}</div> : null}
306
+ <main className="eth-ls-monitoring__primary" style={{ gridArea: "primary", overflow: "auto", minHeight: 0 }}>
307
+ {slots.primary}
308
+ </main>
309
+ {consoleVisible ? (
310
+ <div className="eth-ls-monitoring__console" style={{ gridArea: "console", overflow: "auto", borderBlockStart: `1px solid ${context.tokens.border}` }}>
311
+ {slots.console}
312
+ </div>
313
+ ) : null}
314
+ </div>
315
+ );
316
+ }
317
+
318
+ export const MonitoringOpsLayout: LayoutRegistryItem = {
319
+ type: "PageLayout.MonitoringOps",
320
+ displayName: "Monitoring Ops",
321
+ tier: "page",
322
+ category: "page",
323
+ description: "Realtime ops dashboard: toolbar, metric strip, primary view, console.",
324
+ slots: [
325
+ { name: "toolbar", role: "toolbar", accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }] },
326
+ { name: "metrics", role: "summary", accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }] },
327
+ {
328
+ name: "primary",
329
+ role: "content",
330
+ required: true,
331
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
332
+ priority: { value: 100, behaviorOnNarrow: "keep" },
333
+ },
334
+ {
335
+ name: "console",
336
+ role: "console",
337
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
338
+ priority: { value: 30, behaviorOnNarrow: "move-to-bottom-sheet" },
339
+ },
340
+ ],
341
+ defaultStyleIntent: { surface: "page", taskMode: "monitoring", dataIntensity: "realtime" },
342
+ renderer: MonitoringOpsRenderer,
343
+ };
344
+
345
+ /* ------------------------ CanvasInspector ------------------------ */
346
+
347
+ function CanvasInspectorRenderer({ slots, context, plan }: LayoutRendererProps) {
348
+ const inspectorVisible = slots.inspector && isVisible(plan, "inspector");
349
+ return (
350
+ <div
351
+ className="eth-ls-canvas"
352
+ style={{
353
+ ...frameStyle(context),
354
+ gridTemplateRows: "auto 1fr",
355
+ gridTemplateColumns: inspectorVisible ? "1fr minmax(18rem, 24rem)" : "1fr",
356
+ gridTemplateAreas: inspectorVisible ? `"toolbar toolbar" "canvas inspector"` : `"toolbar" "canvas"`,
357
+ }}
358
+ >
359
+ {slots.toolbar ? <div className="eth-ls-canvas__toolbar" style={{ gridArea: "toolbar" }}>{slots.toolbar}</div> : null}
360
+ <div className="eth-ls-canvas__stage" style={{ gridArea: "canvas", overflow: "hidden", minWidth: 0, minHeight: 0 }}>
361
+ {slots.canvas}
362
+ </div>
363
+ {inspectorVisible ? (
364
+ <div className="eth-ls-canvas__inspector" style={{ gridArea: "inspector", overflow: "auto", borderInlineStart: `1px solid ${context.tokens.border}` }}>
365
+ {slots.inspector}
366
+ </div>
367
+ ) : null}
368
+ </div>
369
+ );
370
+ }
371
+
372
+ export const CanvasInspectorLayout: LayoutRegistryItem = {
373
+ type: "PageLayout.CanvasInspector",
374
+ displayName: "Canvas + Inspector",
375
+ tier: "page",
376
+ category: "page",
377
+ description: "Full-bleed canvas with a toolbar and inline-end inspector.",
378
+ slots: [
379
+ { name: "toolbar", role: "toolbar", accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }] },
380
+ {
381
+ name: "canvas",
382
+ role: "canvas",
383
+ required: true,
384
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
385
+ priority: { value: 100, behaviorOnNarrow: "keep" },
386
+ },
387
+ {
388
+ name: "inspector",
389
+ role: "inspector",
390
+ accepts: [{ kind: "component" }, { kind: "layout" }, { kind: "fragment" }],
391
+ priority: { value: 50, behaviorOnNarrow: "move-to-drawer" },
392
+ },
393
+ ],
394
+ defaultStyleIntent: { surface: "canvas", taskMode: "authoring", visualEmphasis: "immersive" },
395
+ renderer: CanvasInspectorRenderer,
396
+ };
397
+
398
+ export const pageLayouts: LayoutRegistryItem[] = [
399
+ AdminShellLayout,
400
+ DataGridInspectorLayout,
401
+ WorkbenchLayout,
402
+ WizardComposerLayout,
403
+ MonitoringOpsLayout,
404
+ CanvasInspectorLayout,
405
+ ];