@echothink-ui/templates 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.
- package/dist/index.cjs +429 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +395 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/bright-events.d.ts +2 -0
- package/dist/templates/glass-flowchart.d.ts +2 -0
- package/dist/templates/soft-card-campaign.d.ts +2 -0
- package/dist/templates/studio-dark-check-box.d.ts +2 -0
- package/dist/types.d.ts +36 -0
- package/package.json +43 -0
- package/src/__tests__/templates.test.tsx +74 -0
- package/src/index.ts +38 -0
- package/src/templates/bright-events.ts +64 -0
- package/src/templates/glass-flowchart.ts +84 -0
- package/src/templates/soft-card-campaign.ts +135 -0
- package/src/templates/studio-dark-check-box.ts +68 -0
- package/src/types.ts +84 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@echothink-ui/templates` — genuine templates built from the layout system.
|
|
3
|
+
*
|
|
4
|
+
* Each template is a declarative `LayoutNode` AST composed from
|
|
5
|
+
* `@echothink-ui/layout` page-layouts and populated with `@echothink-ui`
|
|
6
|
+
* components by name. Render via `LayoutRoot` (with a component resolver) or
|
|
7
|
+
* flatten into Puck data for editing in the builder.
|
|
8
|
+
*/
|
|
9
|
+
import { softCardCampaignTemplate } from "./templates/soft-card-campaign.js";
|
|
10
|
+
import { brightEventsTemplate } from "./templates/bright-events.js";
|
|
11
|
+
import { glassFlowchartTemplate } from "./templates/glass-flowchart.js";
|
|
12
|
+
import { studioDarkCheckBoxTemplate } from "./templates/studio-dark-check-box.js";
|
|
13
|
+
import { TemplateRegistry, type TemplateDefinition } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export type { TemplateDefinition } from "./types.js";
|
|
16
|
+
export { TemplateRegistry, collectComponentNames } from "./types.js";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
softCardCampaignTemplate,
|
|
20
|
+
brightEventsTemplate,
|
|
21
|
+
glassFlowchartTemplate,
|
|
22
|
+
studioDarkCheckBoxTemplate,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** All builtin templates, in catalog order. */
|
|
26
|
+
export const templateCatalog: TemplateDefinition[] = [
|
|
27
|
+
softCardCampaignTemplate,
|
|
28
|
+
brightEventsTemplate,
|
|
29
|
+
glassFlowchartTemplate,
|
|
30
|
+
studioDarkCheckBoxTemplate,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export type TemplateId = (typeof templateCatalog)[number]["id"];
|
|
34
|
+
|
|
35
|
+
/** A registry preloaded with the builtin templates. */
|
|
36
|
+
export function createTemplateRegistry(): TemplateRegistry {
|
|
37
|
+
return new TemplateRegistry().registerAll(templateCatalog);
|
|
38
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { layout, componentSlot, layoutSlot, fragmentSlot } from "@echothink-ui/layout";
|
|
2
|
+
import type { TemplateDefinition } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bright event workspace — a `DataGridInspector`: a task list grid with a toolbar
|
|
6
|
+
* and a task-detail inspector. Replaces the former `BrightEventWorkspaceTemplate`.
|
|
7
|
+
*/
|
|
8
|
+
const taskColumns = [
|
|
9
|
+
{ key: "task", header: "Task" },
|
|
10
|
+
{ key: "statusLabel", header: "Status" },
|
|
11
|
+
{ key: "due", header: "Due" },
|
|
12
|
+
{ key: "comments", header: "Comments", align: "end" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const taskRows = [
|
|
16
|
+
{ id: "t1", task: "Schedule endocrinologist appointment", statusLabel: "At risk", due: "15d", comments: 6 },
|
|
17
|
+
{ id: "t2", task: "Help DStudio get more customers", statusLabel: "In progress", due: "15d", comments: 23 },
|
|
18
|
+
{ id: "t3", task: "Plan an event", statusLabel: "Running", due: "15d", comments: 7 },
|
|
19
|
+
{ id: "t4", task: "Return a package", statusLabel: "Completed", due: "15d", comments: 34 },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const brightEventsTemplate: TemplateDefinition = {
|
|
23
|
+
id: "bright-events",
|
|
24
|
+
name: "Bright event workspace",
|
|
25
|
+
description: "Task list with toolbar filters and a task-detail inspector.",
|
|
26
|
+
stylePreset: "bright",
|
|
27
|
+
tags: ["tasks", "events", "list-detail", "bright"],
|
|
28
|
+
layout: layout({
|
|
29
|
+
id: "bright-events",
|
|
30
|
+
type: "PageLayout.DataGridInspector",
|
|
31
|
+
styleIntent: { preset: "bright", taskMode: "crud" },
|
|
32
|
+
slots: {
|
|
33
|
+
toolbar: fragmentSlot(
|
|
34
|
+
[
|
|
35
|
+
componentSlot("CarbonSearchInput", { placeholder: "Search tasks" }),
|
|
36
|
+
componentSlot("CarbonButton", { label: "Filter", intent: "tertiary" }),
|
|
37
|
+
componentSlot("CarbonButton", { label: "New task", intent: "primary" }),
|
|
38
|
+
],
|
|
39
|
+
{ op: "stack", direction: "horizontal", slots: [] },
|
|
40
|
+
),
|
|
41
|
+
grid: componentSlot("CarbonDataTable", { rows: taskRows, columns: taskColumns, rowKey: "id" }),
|
|
42
|
+
inspector: layoutSlot(
|
|
43
|
+
layout({
|
|
44
|
+
id: "task-detail",
|
|
45
|
+
type: "Primitive.Panel",
|
|
46
|
+
styleIntent: { surface: "panel" },
|
|
47
|
+
slots: {
|
|
48
|
+
header: componentSlot("CarbonSectionHeader", { title: "Task detail" }),
|
|
49
|
+
body: fragmentSlot(
|
|
50
|
+
[
|
|
51
|
+
componentSlot("CarbonStatusDot", { status: "in-progress", label: "15d left" }),
|
|
52
|
+
componentSlot("CarbonBadge", { severity: "info", children: "Medium priority" }),
|
|
53
|
+
componentSlot("CarbonTextInput", { labelText: "Owner", defaultValue: "Alex Rivera" }),
|
|
54
|
+
componentSlot("CarbonTextarea", { labelText: "Notes", rows: 5, defaultValue: "" }),
|
|
55
|
+
componentSlot("CarbonButton", { label: "Mark complete", intent: "primary" }),
|
|
56
|
+
],
|
|
57
|
+
{ op: "stack", direction: "vertical", slots: [] },
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { layout, componentSlot, layoutSlot, fragmentSlot } from "@echothink-ui/layout";
|
|
2
|
+
import type { TemplateDefinition } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Liquid-glass flowchart studio — a `CanvasInspector`: a full-bleed visualization
|
|
6
|
+
* canvas with a floating toolbar and a node-config inspector. Replaces the former
|
|
7
|
+
* `LiquidGlassFlowchartTemplate`.
|
|
8
|
+
*/
|
|
9
|
+
const throughput = [
|
|
10
|
+
{ label: "00:00", value: 120 },
|
|
11
|
+
{ label: "04:00", value: 98 },
|
|
12
|
+
{ label: "08:00", value: 187 },
|
|
13
|
+
{ label: "12:00", value: 240 },
|
|
14
|
+
{ label: "16:00", value: 210 },
|
|
15
|
+
{ label: "20:00", value: 168 },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const glassFlowchartTemplate: TemplateDefinition = {
|
|
19
|
+
id: "glass-flowchart",
|
|
20
|
+
name: "Liquid glass flowchart",
|
|
21
|
+
description: "Visualization canvas with KPI overlays and a node-config inspector.",
|
|
22
|
+
stylePreset: "glass",
|
|
23
|
+
tags: ["canvas", "flow", "analytics", "glass"],
|
|
24
|
+
layout: layout({
|
|
25
|
+
id: "glass-flowchart",
|
|
26
|
+
type: "PageLayout.CanvasInspector",
|
|
27
|
+
styleIntent: { preset: "glass", taskMode: "authoring", visualEmphasis: "immersive" },
|
|
28
|
+
slots: {
|
|
29
|
+
toolbar: fragmentSlot(
|
|
30
|
+
[
|
|
31
|
+
componentSlot("CarbonSectionHeader", { title: "Flow studio" }),
|
|
32
|
+
componentSlot("CarbonButton", { label: "Run", intent: "primary" }),
|
|
33
|
+
componentSlot("CarbonButton", { label: "Snapshot", intent: "tertiary" }),
|
|
34
|
+
],
|
|
35
|
+
{ op: "stack", direction: "horizontal", slots: [] },
|
|
36
|
+
),
|
|
37
|
+
canvas: fragmentSlot(
|
|
38
|
+
[
|
|
39
|
+
fragmentSlot(
|
|
40
|
+
[
|
|
41
|
+
componentSlot("CarbonKPIBlock", { title: "Throughput", value: "240/min", description: "peak 12:00" }),
|
|
42
|
+
componentSlot("CarbonKPIBlock", { title: "Errors", value: "0.4%", description: "-0.1% today" }),
|
|
43
|
+
componentSlot("CarbonKPIBlock", { title: "Latency p95", value: "182ms", description: "stable" }),
|
|
44
|
+
],
|
|
45
|
+
{ op: "stack", direction: "horizontal", slots: [] },
|
|
46
|
+
),
|
|
47
|
+
componentSlot("CarbonAreaChart", {
|
|
48
|
+
title: "Pipeline throughput",
|
|
49
|
+
data: throughput,
|
|
50
|
+
valueKey: "value",
|
|
51
|
+
}),
|
|
52
|
+
],
|
|
53
|
+
{ op: "stack", direction: "vertical", slots: [] },
|
|
54
|
+
),
|
|
55
|
+
inspector: layoutSlot(
|
|
56
|
+
layout({
|
|
57
|
+
id: "node-config",
|
|
58
|
+
type: "Primitive.Panel",
|
|
59
|
+
styleIntent: { surface: "panel" },
|
|
60
|
+
slots: {
|
|
61
|
+
header: componentSlot("CarbonSectionHeader", { title: "Node config" }),
|
|
62
|
+
body: fragmentSlot(
|
|
63
|
+
[
|
|
64
|
+
componentSlot("CarbonTextInput", { labelText: "Node name", defaultValue: "Transform" }),
|
|
65
|
+
componentSlot("CarbonSelect", {
|
|
66
|
+
labelText: "Kind",
|
|
67
|
+
defaultValue: "map",
|
|
68
|
+
options: [
|
|
69
|
+
{ value: "map", label: "Map" },
|
|
70
|
+
{ value: "filter", label: "Filter" },
|
|
71
|
+
{ value: "aggregate", label: "Aggregate" },
|
|
72
|
+
],
|
|
73
|
+
}),
|
|
74
|
+
componentSlot("CarbonToggle", { labelText: "Enabled", defaultToggled: true }),
|
|
75
|
+
componentSlot("CarbonTextarea", { labelText: "Expression", rows: 4, defaultValue: "" }),
|
|
76
|
+
],
|
|
77
|
+
{ op: "stack", direction: "vertical", slots: [] },
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { layout, componentSlot, layoutSlot, fragmentSlot } from "@echothink-ui/layout";
|
|
2
|
+
import type { TemplateDefinition } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Soft-card campaign workspace — an `AdminShell` whose content is a master/detail
|
|
6
|
+
* split: a campaign dashboard (KPIs + chart + table) beside a campaign-info form
|
|
7
|
+
* panel. Replaces the former hard-coded `SoftCardCampaignTemplate`.
|
|
8
|
+
*/
|
|
9
|
+
const campaignColumns = [
|
|
10
|
+
{ key: "name", header: "Campaign" },
|
|
11
|
+
{ key: "channel", header: "Channel" },
|
|
12
|
+
{ key: "statusLabel", header: "Status" },
|
|
13
|
+
{ key: "audience", header: "Audience", align: "end" },
|
|
14
|
+
{ key: "updated", header: "Updated" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const campaignRows = [
|
|
18
|
+
{ id: "c1", name: "Taste the Future", channel: "Instagram + Ads", statusLabel: "Running", audience: "10,000", updated: "24 Sep" },
|
|
19
|
+
{ id: "c2", name: "Cybersecurity Week", channel: "Facebook", statusLabel: "In Progress", audience: "6,420", updated: "22 Sep" },
|
|
20
|
+
{ id: "c3", name: "E-learning Essentials", channel: "Email", statusLabel: "Approval", audience: "3,890", updated: "21 Sep" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const channelChart = [
|
|
24
|
+
{ label: "Mon", sent: 32, opened: 19 },
|
|
25
|
+
{ label: "Tue", sent: 46, opened: 22 },
|
|
26
|
+
{ label: "Wed", sent: 38, opened: 27 },
|
|
27
|
+
{ label: "Thu", sent: 54, opened: 31 },
|
|
28
|
+
{ label: "Fri", sent: 49, opened: 36 },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const softCardCampaignTemplate: TemplateDefinition = {
|
|
32
|
+
id: "soft-card-campaign",
|
|
33
|
+
name: "Soft card campaign",
|
|
34
|
+
description: "Campaign dashboard with a master/detail configuration panel.",
|
|
35
|
+
stylePreset: "soft-card",
|
|
36
|
+
tags: ["dashboard", "marketing", "crud", "soft-card"],
|
|
37
|
+
layout: layout({
|
|
38
|
+
id: "soft-card-campaign",
|
|
39
|
+
type: "PageLayout.AdminShell",
|
|
40
|
+
styleIntent: { preset: "soft-card", taskMode: "crud" },
|
|
41
|
+
slots: {
|
|
42
|
+
topbar: fragmentSlot(
|
|
43
|
+
[
|
|
44
|
+
componentSlot("CarbonSectionHeader", { title: "Metricmap" }),
|
|
45
|
+
componentSlot("CarbonSearchInput", { placeholder: "Search campaigns" }),
|
|
46
|
+
componentSlot("CarbonButton", { label: "New campaign", intent: "primary" }),
|
|
47
|
+
],
|
|
48
|
+
{ op: "stack", direction: "horizontal", slots: [] },
|
|
49
|
+
),
|
|
50
|
+
navigation: componentSlot("CarbonSideNav", {
|
|
51
|
+
items: [
|
|
52
|
+
{ label: "Summary of Key Metrics", href: "#summary" },
|
|
53
|
+
{ label: "Recent Campaigns", href: "#recent", current: true },
|
|
54
|
+
{ label: "Performance Snapshot", href: "#performance" },
|
|
55
|
+
{ label: "Integrations", href: "#integrations" },
|
|
56
|
+
{ label: "Social Media Metrics", href: "#social" },
|
|
57
|
+
],
|
|
58
|
+
}),
|
|
59
|
+
content: layoutSlot(
|
|
60
|
+
layout({
|
|
61
|
+
id: "campaign-split",
|
|
62
|
+
type: "Primitive.SplitPane",
|
|
63
|
+
variant: "horizontal",
|
|
64
|
+
composition: {
|
|
65
|
+
op: "parallel",
|
|
66
|
+
axis: "x",
|
|
67
|
+
slots: ["primary", "secondary"],
|
|
68
|
+
sizing: { primary: { basis: "1.7fr" }, secondary: { basis: "1fr" } },
|
|
69
|
+
},
|
|
70
|
+
slots: {
|
|
71
|
+
primary: fragmentSlot(
|
|
72
|
+
[
|
|
73
|
+
componentSlot("CarbonPageHeader", {
|
|
74
|
+
title: "Recent Campaign",
|
|
75
|
+
description: "Campaign configuration, audience rules, and channel performance in one workspace.",
|
|
76
|
+
}),
|
|
77
|
+
fragmentSlot(
|
|
78
|
+
[
|
|
79
|
+
componentSlot("CarbonKPIBlock", { title: "Sent", value: "203 Mail", description: "+6% from last week" }),
|
|
80
|
+
componentSlot("CarbonKPIBlock", { title: "Opened", value: "18%", description: "+3% from last week" }),
|
|
81
|
+
componentSlot("CarbonKPIBlock", { title: "Conversions", value: "42", description: "12 awaiting review" }),
|
|
82
|
+
],
|
|
83
|
+
{ op: "stack", direction: "horizontal", slots: [] },
|
|
84
|
+
),
|
|
85
|
+
componentSlot("CarbonChartBlock", {
|
|
86
|
+
title: "Channel performance",
|
|
87
|
+
subtitle: "Chosen period versus last period",
|
|
88
|
+
chartType: "bar",
|
|
89
|
+
data: channelChart,
|
|
90
|
+
series: [
|
|
91
|
+
{ key: "sent", label: "Chosen period" },
|
|
92
|
+
{ key: "opened", label: "Last period" },
|
|
93
|
+
],
|
|
94
|
+
}),
|
|
95
|
+
componentSlot("CarbonDataTable", { rows: campaignRows, columns: campaignColumns, rowKey: "id" }),
|
|
96
|
+
],
|
|
97
|
+
{ op: "stack", direction: "vertical", slots: [] },
|
|
98
|
+
),
|
|
99
|
+
secondary: layoutSlot(
|
|
100
|
+
layout({
|
|
101
|
+
id: "campaign-form",
|
|
102
|
+
type: "Primitive.Panel",
|
|
103
|
+
styleIntent: { surface: "card" },
|
|
104
|
+
slots: {
|
|
105
|
+
header: componentSlot("CarbonSectionHeader", { title: "Campaign Info" }),
|
|
106
|
+
body: fragmentSlot(
|
|
107
|
+
[
|
|
108
|
+
componentSlot("CarbonTextInput", { labelText: "Campaign name", defaultValue: "Taste the Future" }),
|
|
109
|
+
componentSlot("CarbonTextInput", { labelText: "Brand", defaultValue: "Damory Food Indonesia" }),
|
|
110
|
+
componentSlot("CarbonSelect", {
|
|
111
|
+
labelText: "Channel",
|
|
112
|
+
defaultValue: "instagram",
|
|
113
|
+
options: [
|
|
114
|
+
{ value: "instagram", label: "Instagram + Google Ads" },
|
|
115
|
+
{ value: "email", label: "Email only" },
|
|
116
|
+
{ value: "social", label: "Social media" },
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
componentSlot("CarbonTextarea", {
|
|
120
|
+
labelText: "Description",
|
|
121
|
+
rows: 4,
|
|
122
|
+
defaultValue: "A social media and Google Ads campaign showcasing convenience and taste.",
|
|
123
|
+
}),
|
|
124
|
+
],
|
|
125
|
+
{ op: "stack", direction: "vertical", slots: [] },
|
|
126
|
+
),
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { layout, componentSlot, fragmentSlot } from "@echothink-ui/layout";
|
|
2
|
+
import type { TemplateDefinition } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Studio-dark monitoring board — a `MonitoringOps` layout: toolbar + metric strip
|
|
6
|
+
* + primary table + console activity feed. Replaces the former
|
|
7
|
+
* `StudioDarkCheckBoxTemplate`.
|
|
8
|
+
*/
|
|
9
|
+
const monitorColumns = [
|
|
10
|
+
{ key: "check", header: "Check" },
|
|
11
|
+
{ key: "statusLabel", header: "Status" },
|
|
12
|
+
{ key: "latency", header: "Latency", align: "end" },
|
|
13
|
+
{ key: "updated", header: "Updated" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const monitorRows = [
|
|
17
|
+
{ id: "m1", check: "Gateway /execute", statusLabel: "Healthy", latency: "42ms", updated: "now" },
|
|
18
|
+
{ id: "m2", check: "Worker queue lease", statusLabel: "Healthy", latency: "11ms", updated: "now" },
|
|
19
|
+
{ id: "m3", check: "Outcome committer", statusLabel: "Degraded", latency: "318ms", updated: "1m" },
|
|
20
|
+
{ id: "m4", check: "Artifact store", statusLabel: "Healthy", latency: "73ms", updated: "now" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const trend = [
|
|
24
|
+
{ label: "T-5", value: 99.9 },
|
|
25
|
+
{ label: "T-4", value: 99.8 },
|
|
26
|
+
{ label: "T-3", value: 99.95 },
|
|
27
|
+
{ label: "T-2", value: 99.7 },
|
|
28
|
+
{ label: "T-1", value: 99.85 },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const studioDarkCheckBoxTemplate: TemplateDefinition = {
|
|
32
|
+
id: "studio-dark-check-box",
|
|
33
|
+
name: "Studio dark monitoring",
|
|
34
|
+
description: "Realtime ops board with metric strip, health table, and activity console.",
|
|
35
|
+
stylePreset: "studio-dark",
|
|
36
|
+
tags: ["monitoring", "ops", "realtime", "studio-dark"],
|
|
37
|
+
layout: layout({
|
|
38
|
+
id: "studio-dark-check-box",
|
|
39
|
+
type: "PageLayout.MonitoringOps",
|
|
40
|
+
styleIntent: { preset: "studio-dark", taskMode: "monitoring", dataIntensity: "realtime" },
|
|
41
|
+
slots: {
|
|
42
|
+
toolbar: fragmentSlot(
|
|
43
|
+
[
|
|
44
|
+
componentSlot("CarbonSectionHeader", { title: "Runtime health" }),
|
|
45
|
+
componentSlot("CarbonButton", { label: "Acknowledge", intent: "tertiary" }),
|
|
46
|
+
componentSlot("CarbonButton", { label: "Snapshot", intent: "primary" }),
|
|
47
|
+
],
|
|
48
|
+
{ op: "stack", direction: "horizontal", slots: [] },
|
|
49
|
+
),
|
|
50
|
+
metrics: fragmentSlot(
|
|
51
|
+
[
|
|
52
|
+
componentSlot("CarbonMetricTrend", { title: "Uptime", value: "99.85%", data: trend, valueKey: "value" }),
|
|
53
|
+
componentSlot("CarbonKPIBlock", { title: "Active jobs", value: "37", description: "4 retrying" }),
|
|
54
|
+
componentSlot("CarbonKPIBlock", { title: "Error rate", value: "0.6%", description: "+0.2%" }),
|
|
55
|
+
],
|
|
56
|
+
{ op: "stack", direction: "horizontal", slots: [] },
|
|
57
|
+
),
|
|
58
|
+
primary: componentSlot("CarbonDataTable", { rows: monitorRows, columns: monitorColumns, rowKey: "id" }),
|
|
59
|
+
console: componentSlot("CarbonActivityList", {
|
|
60
|
+
items: [
|
|
61
|
+
{ id: "a1", title: "Outcome committer latency spike", time: "1m ago", severity: "warning" },
|
|
62
|
+
{ id: "a2", title: "Worker pool scaled to 8", time: "3m ago", severity: "info" },
|
|
63
|
+
{ id: "a3", title: "Snapshot snap-29 committed", time: "5m ago", severity: "success" },
|
|
64
|
+
],
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@echothink-ui/templates` — template contract.
|
|
3
|
+
*
|
|
4
|
+
* A template is now a GENUINE template: a declarative `LayoutNode` AST built from
|
|
5
|
+
* `@echothink-ui/layout` page-layouts and populated with `@echothink-ui`
|
|
6
|
+
* components (referenced by name). It is rendered through the layout engine
|
|
7
|
+
* (`LayoutRoot`) and can be flattened into Puck data for editing in the builder.
|
|
8
|
+
*/
|
|
9
|
+
import type { EthStylePreset } from "@echothink-ui/style";
|
|
10
|
+
import type { LayoutNode, SlotContent } from "@echothink-ui/layout";
|
|
11
|
+
|
|
12
|
+
export interface TemplateDefinition {
|
|
13
|
+
/** Stable id. */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Human label. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** One-line description. */
|
|
18
|
+
description: string;
|
|
19
|
+
/** Preset this template was designed around (the engine still infers per-slot). */
|
|
20
|
+
stylePreset: EthStylePreset;
|
|
21
|
+
/** Discovery tags. */
|
|
22
|
+
tags: string[];
|
|
23
|
+
/** The root layout AST. */
|
|
24
|
+
layout: LayoutNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Walk a layout AST and collect every referenced component name (deduped). */
|
|
28
|
+
export function collectComponentNames(node: LayoutNode): string[] {
|
|
29
|
+
const names = new Set<string>();
|
|
30
|
+
const visitContent = (content: SlotContent): void => {
|
|
31
|
+
switch (content.kind) {
|
|
32
|
+
case "component":
|
|
33
|
+
names.add(content.component);
|
|
34
|
+
break;
|
|
35
|
+
case "layout":
|
|
36
|
+
visitNode(content.layout);
|
|
37
|
+
break;
|
|
38
|
+
case "fragment":
|
|
39
|
+
content.items.forEach(visitContent);
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const visitNode = (n: LayoutNode): void => {
|
|
46
|
+
for (const content of Object.values(n.slots)) visitContent(content);
|
|
47
|
+
const comp = n.composition;
|
|
48
|
+
if (comp?.op === "switch") comp.items.forEach((i) => visitContent(i.content));
|
|
49
|
+
if (comp?.op === "stepper") comp.steps.forEach((s) => visitContent(s.content));
|
|
50
|
+
};
|
|
51
|
+
visitNode(node);
|
|
52
|
+
return [...names].sort();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A tiny registry over template definitions. */
|
|
56
|
+
export class TemplateRegistry {
|
|
57
|
+
private readonly items = new Map<string, TemplateDefinition>();
|
|
58
|
+
|
|
59
|
+
register(def: TemplateDefinition): this {
|
|
60
|
+
this.items.set(def.id, def);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
registerAll(defs: readonly TemplateDefinition[]): this {
|
|
65
|
+
for (const def of defs) this.register(def);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get(id: string): TemplateDefinition | undefined {
|
|
70
|
+
return this.items.get(id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
has(id: string): boolean {
|
|
74
|
+
return this.items.has(id);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
list(): TemplateDefinition[] {
|
|
78
|
+
return [...this.items.values()];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
byPreset(preset: EthStylePreset): TemplateDefinition[] {
|
|
82
|
+
return this.list().filter((t) => t.stylePreset === preset);
|
|
83
|
+
}
|
|
84
|
+
}
|