@utopia-studio-design/design-system-cli 0.4.1 → 0.5.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 (125) hide show
  1. package/README.md +23 -0
  2. package/bin/utopia-ds-mcp.mjs +9 -2
  3. package/bin/utopia-ds.mjs +106 -8
  4. package/data/blocks/activity-feed/ActivityFeedBlock.tsx +255 -0
  5. package/data/blocks/activity-feed/README.md +16 -0
  6. package/data/blocks/activity-feed/activity-feed-contract.ts +96 -0
  7. package/data/blocks/activity-feed/activity-feed-fixture.ts +22 -0
  8. package/data/blocks/activity-feed/activity-feed.css +154 -0
  9. package/data/blocks/agent-status/AgentStatusBlock.tsx +313 -0
  10. package/data/blocks/agent-status/README.md +15 -0
  11. package/data/blocks/agent-status/agent-status-contract.ts +133 -0
  12. package/data/blocks/agent-status/agent-status-fixture.ts +79 -0
  13. package/data/blocks/agent-status/agent-status.css +216 -0
  14. package/data/blocks/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
  15. package/data/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  16. package/data/blocks/analytics-dashboard/README.md +17 -0
  17. package/data/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
  18. package/data/blocks/calendar-application/CalendarApplicationBlocks.tsx +519 -0
  19. package/data/blocks/calendar-application/README.md +20 -0
  20. package/data/blocks/calendar-application/calendar-application.css +216 -0
  21. package/data/blocks/calendar-application/calendar-localization.ts +90 -0
  22. package/data/blocks/calendar-application/calendar-panel-contract.ts +72 -0
  23. package/data/blocks/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
  24. package/data/blocks/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
  25. package/data/blocks/dashboard-composer/README.md +16 -0
  26. package/data/blocks/dashboard-composer/RegistryDataBlocks.tsx +238 -0
  27. package/data/blocks/dashboard-composer/dashboard-composer.css +328 -0
  28. package/data/blocks/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
  29. package/data/blocks/database-workspace/README.md +9 -0
  30. package/data/blocks/database-workspace/database-workspace.css +107 -0
  31. package/data/blocks/interaction-workbench/InteractionWorkbench.tsx +271 -0
  32. package/data/blocks/interaction-workbench/README.md +9 -0
  33. package/data/blocks/interaction-workbench/interaction-workbench.css +226 -0
  34. package/data/docs/calendar.md +164 -0
  35. package/data/docs/charts.md +95 -0
  36. package/data/docs/composition-registry.md +174 -0
  37. package/data/docs/dashboard-composer.md +94 -0
  38. package/data/docs/database.md +68 -0
  39. package/data/docs/examples/dashboard-layout.v1.json +65 -0
  40. package/data/docs/foundations.md +1 -1
  41. package/data/docs/interaction-foundation.md +109 -0
  42. package/data/docs/quick-start-ai.md +23 -1
  43. package/data/manifests/blocks.json +392 -0
  44. package/data/manifests/catalog.json +7 -4
  45. package/data/manifests/components.json +179 -0
  46. package/data/manifests/composition-registry.json +427 -0
  47. package/data/manifests/motion-profiles.json +1 -1
  48. package/data/manifests/patterns.json +113 -1
  49. package/data/manifests/templates.json +346 -0
  50. package/data/manifests/theme-renacore.json +142 -0
  51. package/data/manifests/themes.json +314 -28
  52. package/data/templates/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
  53. package/data/templates/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  54. package/data/templates/analytics-dashboard/README.md +6 -0
  55. package/data/templates/analytics-dashboard/analytics-dashboard.css +92 -0
  56. package/data/templates/analytics-dashboard/index.html +13 -0
  57. package/data/templates/analytics-dashboard/main.tsx +196 -0
  58. package/data/templates/analytics-dashboard/styles.css +120 -0
  59. package/data/templates/analytics-dashboard/template.manifest.json +17 -0
  60. package/data/templates/calendar-application/CalendarApplicationBlocks.tsx +519 -0
  61. package/data/templates/calendar-application/README.md +10 -0
  62. package/data/templates/calendar-application/calendar-application.css +216 -0
  63. package/data/templates/calendar-application/calendar-localization.ts +90 -0
  64. package/data/templates/calendar-application/index.html +14 -0
  65. package/data/templates/calendar-application/main.tsx +409 -0
  66. package/data/templates/calendar-application/styles.css +248 -0
  67. package/data/templates/calendar-application/template.manifest.json +27 -0
  68. package/data/templates/composition-registry-lab/README.md +9 -0
  69. package/data/templates/composition-registry-lab/index.html +14 -0
  70. package/data/templates/composition-registry-lab/main.tsx +217 -0
  71. package/data/templates/composition-registry-lab/sample-layout.json +63 -0
  72. package/data/templates/composition-registry-lab/styles.css +343 -0
  73. package/data/templates/composition-registry-lab/template.manifest.json +34 -0
  74. package/data/templates/dashboard-composer/ActivityFeedBlock.tsx +255 -0
  75. package/data/templates/dashboard-composer/AgentStatusBlock.tsx +313 -0
  76. package/data/templates/dashboard-composer/AnalyticsDashboardBlocks.tsx +107 -0
  77. package/data/templates/dashboard-composer/AnalyticsMetricBlocks.tsx +87 -0
  78. package/data/templates/dashboard-composer/CalendarApplicationBlocks.tsx +519 -0
  79. package/data/templates/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
  80. package/data/templates/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
  81. package/data/templates/dashboard-composer/DatabaseWorkspaceBlocks.tsx +240 -0
  82. package/data/templates/dashboard-composer/README.md +53 -0
  83. package/data/templates/dashboard-composer/RegistryDataBlocks.tsx +238 -0
  84. package/data/templates/dashboard-composer/activity-feed-contract.ts +96 -0
  85. package/data/templates/dashboard-composer/activity-feed-fixture.ts +22 -0
  86. package/data/templates/dashboard-composer/activity-feed.css +154 -0
  87. package/data/templates/dashboard-composer/agent-status-contract.ts +133 -0
  88. package/data/templates/dashboard-composer/agent-status-fixture.ts +79 -0
  89. package/data/templates/dashboard-composer/agent-status-lab/index.html +19 -0
  90. package/data/templates/dashboard-composer/agent-status-lab/main.tsx +151 -0
  91. package/data/templates/dashboard-composer/agent-status-lab/styles.css +139 -0
  92. package/data/templates/dashboard-composer/agent-status.css +216 -0
  93. package/data/templates/dashboard-composer/analytics-dashboard.css +92 -0
  94. package/data/templates/dashboard-composer/calendar-application.css +216 -0
  95. package/data/templates/dashboard-composer/calendar-localization.ts +90 -0
  96. package/data/templates/dashboard-composer/calendar-panel-contract.ts +72 -0
  97. package/data/templates/dashboard-composer/dashboard-composer.css +328 -0
  98. package/data/templates/dashboard-composer/database-workspace.css +107 -0
  99. package/data/templates/dashboard-composer/index.html +14 -0
  100. package/data/templates/dashboard-composer/main.tsx +550 -0
  101. package/data/templates/dashboard-composer/sample-layout.json +147 -0
  102. package/data/templates/dashboard-composer/styles.css +198 -0
  103. package/data/templates/dashboard-composer/template.manifest.json +34 -0
  104. package/data/templates/dashboard-composer/visual-editor/PuckEditorSurface.tsx +132 -0
  105. package/data/templates/dashboard-composer/visual-editor/VisualEditorApp.tsx +343 -0
  106. package/data/templates/dashboard-composer/visual-editor/index.html +17 -0
  107. package/data/templates/dashboard-composer/visual-editor/main.tsx +95 -0
  108. package/data/templates/dashboard-composer/visual-editor/styles.css +310 -0
  109. package/data/templates/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
  110. package/data/templates/database-workspace/README.md +5 -0
  111. package/data/templates/database-workspace/database-workspace.css +107 -0
  112. package/data/templates/database-workspace/index.html +14 -0
  113. package/data/templates/database-workspace/main.tsx +247 -0
  114. package/data/templates/database-workspace/styles.css +121 -0
  115. package/data/templates/database-workspace/template.manifest.json +19 -0
  116. package/data/templates/interaction-lab/InteractionWorkbench.tsx +271 -0
  117. package/data/templates/interaction-lab/README.md +7 -0
  118. package/data/templates/interaction-lab/index.html +14 -0
  119. package/data/templates/interaction-lab/interaction-workbench.css +226 -0
  120. package/data/templates/interaction-lab/main.tsx +104 -0
  121. package/data/templates/interaction-lab/styles.css +224 -0
  122. package/data/templates/interaction-lab/template.manifest.json +36 -0
  123. package/lib/api.mjs +72 -5
  124. package/lib/composition-runtime.mjs +382 -0
  125. package/package.json +4 -1
@@ -0,0 +1,107 @@
1
+ .ceramic-database-toolbar {
2
+ display: grid;
3
+ grid-template-columns: minmax(12rem, 1fr) auto auto;
4
+ align-items: center;
5
+ gap: var(--space-3);
6
+ }
7
+
8
+ .ceramic-database-toolbar label,
9
+ .ceramic-database-toolbar input {
10
+ min-inline-size: 0;
11
+ inline-size: 100%;
12
+ }
13
+
14
+ .ceramic-database-toolbar input {
15
+ min-block-size: var(--button-height-sm);
16
+ border: var(--border-width-hairline) solid var(--input);
17
+ padding-inline: var(--space-3);
18
+ background: var(--background);
19
+ color: var(--foreground);
20
+ font: inherit;
21
+ }
22
+
23
+ .ceramic-database-toolbar > div,
24
+ .ceramic-saved-views {
25
+ display: flex;
26
+ flex-wrap: wrap;
27
+ align-items: center;
28
+ gap: var(--space-1);
29
+ }
30
+
31
+ .ceramic-saved-views {
32
+ border-block-end: var(--border-width-hairline) solid var(--border);
33
+ }
34
+
35
+ .ceramic-record-drawer-backdrop {
36
+ position: fixed;
37
+ z-index: var(--z-overlay);
38
+ inset: 0;
39
+ background: var(--overlay);
40
+ }
41
+
42
+ .ceramic-record-drawer {
43
+ position: fixed;
44
+ inset-block: 0;
45
+ inset-inline-end: 0;
46
+ z-index: var(--z-modal);
47
+ inline-size: min(100%, 28rem);
48
+ overflow: auto;
49
+ padding: var(--space-5);
50
+ border-inline-start: var(--border-width-strong) solid var(--border);
51
+ background: var(--popover-surface);
52
+ color: var(--foreground);
53
+ }
54
+
55
+ .ceramic-record-drawer header {
56
+ display: flex;
57
+ align-items: start;
58
+ justify-content: space-between;
59
+ gap: var(--space-4);
60
+ }
61
+
62
+ .ceramic-record-drawer header div,
63
+ .ceramic-record-drawer dl {
64
+ display: grid;
65
+ gap: var(--space-2);
66
+ }
67
+
68
+ .ceramic-record-drawer :is(h2, dl, dd) {
69
+ margin: 0;
70
+ }
71
+
72
+ .ceramic-record-drawer header span,
73
+ .ceramic-record-drawer dt {
74
+ color: var(--muted-foreground);
75
+ font-size: var(--font-size-sm);
76
+ }
77
+
78
+ .ceramic-record-drawer dl > div {
79
+ display: grid;
80
+ gap: var(--space-1);
81
+ padding-block: var(--space-3);
82
+ border-block-end: var(--border-width-hairline) solid var(--border);
83
+ }
84
+
85
+ .ceramic-record-drawer .uds-database-property-cell {
86
+ padding-inline: 0;
87
+ }
88
+
89
+ .ceramic-bulk-action-bar {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: var(--space-2);
93
+ inline-size: fit-content;
94
+ padding: var(--space-2);
95
+ border: var(--border-width-strong) solid var(--border);
96
+ background: var(--surface-elevated);
97
+ }
98
+
99
+ @media (max-width: 44rem) {
100
+ .ceramic-database-toolbar {
101
+ grid-template-columns: 1fr;
102
+ }
103
+
104
+ .ceramic-database-toolbar > :last-child {
105
+ inline-size: 100%;
106
+ }
107
+ }
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en" dir="ltr" data-theme="utopia-default" data-color-mode="light">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="A live Ceramic dashboard composed from validated Layout JSON." />
7
+ <title>Dashboard Composer — Ceramic Design System</title>
8
+ </head>
9
+ <body>
10
+ <a class="skip-link" href="#main-content">Skip to dashboard composer</a>
11
+ <div id="root"></div>
12
+ <script type="module" src="./main.tsx"></script>
13
+ </body>
14
+ </html>
@@ -0,0 +1,550 @@
1
+ import {
2
+ StrictMode,
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react'
10
+ import { createRoot } from 'react-dom/client'
11
+ import { Badge } from '@utopia-studio-design/design-system/Badge'
12
+ import { Button } from '@utopia-studio-design/design-system/Button'
13
+ import {
14
+ getCeramicBlockDefinition,
15
+ validateCeramicAgentMutation,
16
+ validateCeramicLayout,
17
+ type CeramicLayoutBlock,
18
+ type CeramicLayoutDocument,
19
+ } from '@utopia-studio-design/design-system/Composition'
20
+ import type {
21
+ SchedulerEvent,
22
+ } from '@utopia-studio-design/design-system/Calendar'
23
+ import {
24
+ AgentStatusBlock,
25
+ type AgentStatusItem,
26
+ } from './AgentStatusBlock'
27
+ import { resolveAgentStatusConfiguration } from './agent-status-contract'
28
+ import { getAgentStatusFixture } from './agent-status-fixture'
29
+ import {
30
+ ActivityFeedBlock,
31
+ } from './ActivityFeedBlock'
32
+ import { resolveActivityFeedConfiguration } from './activity-feed-contract'
33
+ import { getActivityFeedFixture } from './activity-feed-fixture'
34
+ import { KPIGrid, type MetricDefinition } from './AnalyticsMetricBlocks'
35
+ import { SchedulerShell } from './CalendarApplicationBlocks'
36
+ import { resolveCalendarPanelConfiguration } from './calendar-panel-contract'
37
+ import {
38
+ CeramicDashboardRenderer,
39
+ TodoListBlock,
40
+ type CeramicBlockRenderer,
41
+ type TodoFilter,
42
+ type TodoItem,
43
+ } from './DashboardComposerBlocks'
44
+ import {
45
+ ChartPanelRegistryBlock,
46
+ IssueTableRegistryBlock,
47
+ } from './RegistryDataBlocks'
48
+ import '@utopia-studio-design/design-system/core.css'
49
+ import '@utopia-studio-design/design-system/themes/utopia-default.css'
50
+ import './agent-status.css'
51
+ import './activity-feed.css'
52
+ import './analytics-dashboard.css'
53
+ import './calendar-application.css'
54
+ import './dashboard-composer.css'
55
+ import initialLayoutDocument from './sample-layout.json'
56
+ import './styles.css'
57
+
58
+ const initialLayout = initialLayoutDocument as CeramicLayoutDocument
59
+ const anchorDate = new Date(2026, 2, 9, 12)
60
+
61
+ const initialTasks: TodoItem[] = [
62
+ { id: 'todo-1', title: 'Review the Calendar block API', completed: true, dueLabel: 'Today', priority: 'high' },
63
+ { id: 'todo-2', title: 'Approve the dashboard layout', completed: false, dueLabel: 'Today', priority: 'high' },
64
+ { id: 'todo-3', title: 'Prepare the npm release notes', completed: false, dueLabel: 'Today', priority: 'medium' },
65
+ { id: 'todo-4', title: 'Plan the Puck adapter spike', completed: false, dueLabel: 'Tomorrow', priority: 'low' },
66
+ ]
67
+
68
+ const initialEvents: SchedulerEvent[] = [
69
+ {
70
+ id: 'design-review',
71
+ title: 'Composer review',
72
+ start: '2026-03-09T01:00:00.000Z',
73
+ end: '2026-03-09T02:00:00.000Z',
74
+ tone: 'accent',
75
+ },
76
+ {
77
+ id: 'release-check',
78
+ title: 'Release QA',
79
+ start: '2026-03-09T03:30:00.000Z',
80
+ end: '2026-03-09T04:15:00.000Z',
81
+ tone: 'positive',
82
+ },
83
+ {
84
+ id: 'focus-block',
85
+ title: 'Focus block',
86
+ start: '2026-03-09T05:00:00.000Z',
87
+ end: '2026-03-09T06:00:00.000Z',
88
+ recurrence: { frequency: 'daily', count: 5 },
89
+ },
90
+ {
91
+ id: 'publish-window',
92
+ title: 'Publish window',
93
+ start: '2026-03-11T00:00:00.000Z',
94
+ end: '2026-03-12T00:00:00.000Z',
95
+ allDay: true,
96
+ tone: 'warning',
97
+ },
98
+ ]
99
+
100
+ const arabicEventTitles: Record<string, string> = {
101
+ 'design-review': 'مراجعة التخطيط',
102
+ 'focus-block': 'وقت التركيز',
103
+ 'publish-window': 'نافذة النشر',
104
+ 'release-check': 'فحص الإصدار',
105
+ }
106
+
107
+ const metrics: MetricDefinition[] = [
108
+ { id: 'open', label: 'Open tasks', value: 3, change: -25, changeLabel: 'vs yesterday' },
109
+ { id: 'completed', label: 'Completed', value: 8, change: 14, changeLabel: 'this week' },
110
+ { id: 'focus', label: 'Focus hours', value: 12.5, change: 6, changeLabel: 'this week' },
111
+ ]
112
+
113
+ interface ComposerData {
114
+ agents: AgentStatusItem[]
115
+ compact: boolean
116
+ date: Date
117
+ direction: 'ltr' | 'rtl'
118
+ events: SchedulerEvent[]
119
+ layout: CeramicLayoutDocument
120
+ setDate: (date: Date) => void
121
+ setAgents: (items: AgentStatusItem[]) => void
122
+ setEvents: (events: SchedulerEvent[]) => void
123
+ setLayout: (layout: CeramicLayoutDocument) => void
124
+ setTasks: (items: TodoItem[]) => void
125
+ tasks: TodoItem[]
126
+ }
127
+
128
+ const ComposerDataContext = createContext<ComposerData | null>(null)
129
+
130
+ function useComposerData() {
131
+ const value = useContext(ComposerDataContext)
132
+ if (!value) throw new Error('Dashboard block renderer must be used inside ComposerDataContext.')
133
+ return value
134
+ }
135
+
136
+ function updateBlockProps(
137
+ layout: CeramicLayoutDocument,
138
+ blockId: string,
139
+ props: Record<string, unknown>,
140
+ ) {
141
+ return {
142
+ ...layout,
143
+ blocks: layout.blocks.map((candidate) => (
144
+ candidate.id === blockId
145
+ ? { ...candidate, props: { ...candidate.props, ...props } }
146
+ : candidate
147
+ )),
148
+ }
149
+ }
150
+
151
+ function TodoRenderer({ block }: { block: CeramicLayoutBlock }) {
152
+ const { tasks, setTasks } = useComposerData()
153
+ return (
154
+ <TodoListBlock
155
+ density={(block.props.density as 'compact' | 'default' | 'comfortable') ?? 'default'}
156
+ filter={(block.props.filter as TodoFilter) ?? 'all'}
157
+ items={tasks}
158
+ showCompleted={block.props.showCompleted !== false}
159
+ title={(block.props.title as string) ?? 'Today’s tasks'}
160
+ onItemsChange={setTasks}
161
+ />
162
+ )
163
+ }
164
+
165
+ function CalendarRenderer({ block, direction }: { block: CeramicLayoutBlock; direction: 'ltr' | 'rtl' }) {
166
+ const { compact, date, events, layout, setDate, setEvents, setLayout } = useComposerData()
167
+ const configuration = resolveCalendarPanelConfiguration(block.props, { compact, direction })
168
+ const displayEvents = configuration.locale.startsWith('ar')
169
+ ? events.map((event) => ({
170
+ ...event,
171
+ title: arabicEventTitles[event.id] ?? event.title,
172
+ }))
173
+ : events
174
+ return (
175
+ <SchedulerShell
176
+ {...configuration}
177
+ date={date}
178
+ events={displayEvents}
179
+ permissions={{ create: true, edit: true, move: true, resize: true, duplicate: true, delete: true }}
180
+ onDateChange={setDate}
181
+ onEventCreate={(selectedDate) => {
182
+ const start = new Date(selectedDate)
183
+ start.setHours(14, 0, 0, 0)
184
+ setEvents([...events, {
185
+ id: `event-${events.length + 1}`,
186
+ title: configuration.locale.startsWith('ar') ? 'حدث جديد' : 'New event',
187
+ start: start.toISOString(),
188
+ end: new Date(start.valueOf() + 30 * 60_000).toISOString(),
189
+ }])
190
+ }}
191
+ onEventsChange={setEvents}
192
+ onViewChange={(nextView) => setLayout(updateBlockProps(layout, block.id, { view: nextView }))}
193
+ />
194
+ )
195
+ }
196
+
197
+ function MetricsRenderer({ block }: { block: CeramicLayoutBlock }) {
198
+ const metricIds = new Set((block.props.metrics as string[]) ?? metrics.map((metric) => metric.id))
199
+ return (
200
+ <KPIGrid
201
+ density={(block.props.density as 'compact' | 'default' | 'comfortable') ?? 'compact'}
202
+ metrics={metrics.filter((metric) => metricIds.has(metric.id))}
203
+ />
204
+ )
205
+ }
206
+
207
+ function ActivityRenderer({ block, direction }: { block: CeramicLayoutBlock; direction: 'ltr' | 'rtl' }) {
208
+ const configuration = resolveActivityFeedConfiguration(block.props, { direction })
209
+ return (
210
+ <ActivityFeedBlock
211
+ {...configuration}
212
+ items={getActivityFeedFixture(configuration.locale)}
213
+ />
214
+ )
215
+ }
216
+
217
+ function AgentStatusRenderer({ block, direction }: { block: CeramicLayoutBlock; direction: 'ltr' | 'rtl' }) {
218
+ const { agents, setAgents } = useComposerData()
219
+ const configuration = resolveAgentStatusConfiguration(block.props, { direction })
220
+ const stateById = new Map(agents.map((item) => [item.id, item]))
221
+ const localizedItems = getAgentStatusFixture(configuration.locale).map((item) => {
222
+ const runtimeItem = stateById.get(item.id)
223
+ return runtimeItem
224
+ ? {
225
+ ...item,
226
+ approval: runtimeItem.approval ? item.approval : undefined,
227
+ progress: runtimeItem.progress,
228
+ state: runtimeItem.state,
229
+ }
230
+ : item
231
+ })
232
+ const updateApproval = (item: AgentStatusItem, approved: boolean) => {
233
+ setAgents(agents.map((candidate) => (
234
+ candidate.id === item.id
235
+ ? {
236
+ ...candidate,
237
+ approval: undefined,
238
+ progress: approved ? Math.max(1, candidate.progress ?? 0) : candidate.progress,
239
+ state: approved ? 'running' : 'blocked',
240
+ }
241
+ : candidate
242
+ )))
243
+ }
244
+ return (
245
+ <AgentStatusBlock
246
+ {...configuration}
247
+ items={localizedItems}
248
+ onApprove={(item) => updateApproval(item, true)}
249
+ onReject={(item) => updateApproval(item, false)}
250
+ />
251
+ )
252
+ }
253
+
254
+ const blockRenderers: Record<string, CeramicBlockRenderer> = {
255
+ 'agent-status': AgentStatusRenderer,
256
+ 'activity-feed': ActivityRenderer,
257
+ 'chart-panel': ChartPanelRegistryBlock,
258
+ 'todo-list': TodoRenderer,
259
+ 'calendar-panel': CalendarRenderer,
260
+ 'issue-table': IssueTableRegistryBlock,
261
+ 'metric-grid': MetricsRenderer,
262
+ }
263
+
264
+ function App() {
265
+ const [layout, setLayout] = useState<CeramicLayoutDocument>(() => structuredClone(initialLayout))
266
+ const [agents, setAgents] = useState<AgentStatusItem[]>(() => getAgentStatusFixture('en-US'))
267
+ const [tasks, setTasks] = useState(initialTasks)
268
+ const [events, setEvents] = useState(initialEvents)
269
+ const [date, setDate] = useState(anchorDate)
270
+ const [direction, setDirection] = useState<'ltr' | 'rtl'>('ltr')
271
+ const [dark, setDark] = useState(false)
272
+ const [editable, setEditable] = useState(true)
273
+ const [compact, setCompact] = useState(() => window.matchMedia('(max-width: 48rem)').matches)
274
+ const [feedback, setFeedback] = useState('Layout JSON validated. Seven blocks are rendered.')
275
+ const nextBlockId = useRef(1)
276
+ const validation = validateCeramicLayout(layout)
277
+ const previewsArabic = layout.blocks.some((block) => (
278
+ (block.type === 'calendar-panel' || block.type === 'activity-feed' || block.type === 'agent-status')
279
+ && block.props.locale === 'ar-SA'
280
+ ))
281
+
282
+ useEffect(() => {
283
+ const media = window.matchMedia('(max-width: 48rem)')
284
+ const handleChange = () => setCompact(media.matches)
285
+ media.addEventListener('change', handleChange)
286
+ return () => media.removeEventListener('change', handleChange)
287
+ }, [])
288
+
289
+ useEffect(() => {
290
+ document.documentElement.dir = direction
291
+ return () => {
292
+ document.documentElement.dir = 'ltr'
293
+ }
294
+ }, [direction])
295
+
296
+ const commitAgentChange = (next: CeramicLayoutDocument, successMessage: string) => {
297
+ const result = validateCeramicAgentMutation(layout, next)
298
+ if (!result.ok) {
299
+ setFeedback(`Registry rejected the proposal: ${result.issues[0]?.message}`)
300
+ return
301
+ }
302
+ setLayout(next)
303
+ setFeedback(successMessage)
304
+ }
305
+
306
+ const addBlock = (
307
+ type:
308
+ | 'todo-list'
309
+ | 'calendar-panel'
310
+ | 'activity-feed'
311
+ | 'agent-status'
312
+ | 'issue-table'
313
+ | 'chart-panel',
314
+ ) => {
315
+ const definition = getCeramicBlockDefinition(type)
316
+ if (!definition) return
317
+ const nextOrder = Math.max(0, ...layout.blocks.map((block) => block.placement.order)) + 1
318
+ const block: CeramicLayoutBlock = {
319
+ id: `${type}-${nextBlockId.current++}`,
320
+ type,
321
+ version: definition.version,
322
+ placement: { column: 1, span: 12, order: nextOrder, compactSpan: 12 },
323
+ props: type === 'todo-list'
324
+ ? { title: 'New task list', filter: 'all', density: 'default', showCompleted: true }
325
+ : type === 'issue-table'
326
+ ? {
327
+ columns: ['title', 'status', 'priority', 'owner', 'due'],
328
+ density: 'default',
329
+ view: 'active',
330
+ }
331
+ : type === 'chart-panel'
332
+ ? {
333
+ chartType: 'line',
334
+ comparison: 'previous-period',
335
+ showTable: false,
336
+ }
337
+ : type === 'agent-status'
338
+ ? {
339
+ density: 'default',
340
+ direction: 'inherit',
341
+ filter: 'all',
342
+ locale: 'en-US',
343
+ maxItems: 6,
344
+ showApprovals: true,
345
+ timeZone: 'Asia/Seoul',
346
+ }
347
+ : type === 'activity-feed'
348
+ ? {
349
+ density: 'default',
350
+ direction: 'inherit',
351
+ groupBy: 'day',
352
+ locale: 'en-US',
353
+ pageSize: 4,
354
+ showSources: true,
355
+ timeZone: 'Asia/Seoul',
356
+ }
357
+ : {
358
+ direction: 'inherit',
359
+ initialScrollHour: 8,
360
+ locale: 'en-US',
361
+ secondaryCalendar: 'none',
362
+ showAgendaAlternative: false,
363
+ timeZone: 'Asia/Seoul',
364
+ view: 'week',
365
+ },
366
+ locked: false,
367
+ }
368
+ commitAgentChange({ ...layout, blocks: [...layout.blocks, block] }, `${definition.title} added by the agent.`)
369
+ }
370
+
371
+ const toggleLocalizedBlocks = () => {
372
+ const locale = previewsArabic ? 'en-US' : 'ar-SA'
373
+ const directionMode = previewsArabic ? 'inherit' : 'auto'
374
+ const next = {
375
+ ...layout,
376
+ blocks: layout.blocks.map((block) => (
377
+ block.type === 'calendar-panel' || block.type === 'activity-feed' || block.type === 'agent-status'
378
+ ? {
379
+ ...block,
380
+ props: {
381
+ ...block.props,
382
+ direction: directionMode,
383
+ locale,
384
+ },
385
+ }
386
+ : block
387
+ )),
388
+ }
389
+ commitAgentChange(
390
+ next,
391
+ previewsArabic
392
+ ? 'Localized blocks restored to English.'
393
+ : 'Calendar, Activity Feed, and Agent Status now preview Arabic with automatic RTL.',
394
+ )
395
+ }
396
+
397
+ const proposeSwap = () => {
398
+ const todo = layout.blocks.find((block) => block.type === 'todo-list')
399
+ const calendar = layout.blocks.find((block) => block.type === 'calendar-panel')
400
+ if (!todo || !calendar) {
401
+ setFeedback('Add both a To-do List and Calendar before asking the agent to rearrange them.')
402
+ return
403
+ }
404
+ const next = {
405
+ ...layout,
406
+ blocks: layout.blocks.map((block) => {
407
+ if (block.id === todo.id) {
408
+ return {
409
+ ...block,
410
+ placement: {
411
+ ...block.placement,
412
+ column: Math.min(todo.placement.column, calendar.placement.column) + calendar.placement.span,
413
+ order: Math.max(todo.placement.order, calendar.placement.order),
414
+ },
415
+ }
416
+ }
417
+ if (block.id === calendar.id) {
418
+ return {
419
+ ...block,
420
+ placement: {
421
+ ...block.placement,
422
+ column: Math.min(todo.placement.column, calendar.placement.column),
423
+ order: Math.min(todo.placement.order, calendar.placement.order),
424
+ },
425
+ }
426
+ }
427
+ return block
428
+ }),
429
+ }
430
+ commitAgentChange(next, 'Agent proposal applied: Calendar and To-do positions were exchanged.')
431
+ }
432
+
433
+ const data = useMemo<ComposerData>(() => ({
434
+ agents,
435
+ compact,
436
+ date,
437
+ direction,
438
+ events,
439
+ layout,
440
+ setDate,
441
+ setAgents,
442
+ setEvents,
443
+ setLayout,
444
+ setTasks,
445
+ tasks,
446
+ }), [agents, compact, date, direction, events, layout, tasks])
447
+
448
+ return (
449
+ <ComposerDataContext.Provider value={data}>
450
+ <main
451
+ className="dashboard-composer-template"
452
+ data-color-mode={dark ? 'dark' : 'light'}
453
+ data-theme="utopia-default"
454
+ dir={direction}
455
+ id="main-content"
456
+ >
457
+ <header className="composer-header">
458
+ <div>
459
+ <span>Live composition</span>
460
+ <h1>Arrange real Ceramic blocks, not placeholders</h1>
461
+ <p>To-do, Calendar, Issue Table, Chart, Activity, Agent Status, and Metrics are rendered from the same validated Layout JSON an agent can edit.</p>
462
+ </div>
463
+ <div aria-label="Preview controls">
464
+ <Button asChild>
465
+ <a href="/templates/dashboard-composer/visual-editor/">Open visual editor</a>
466
+ </Button>
467
+ <Button asChild variant="outline">
468
+ <a href="/templates/dashboard-composer/agent-status-lab/">Open Agent Status State Lab</a>
469
+ </Button>
470
+ <Button variant="outline" onClick={toggleLocalizedBlocks}>
471
+ {previewsArabic ? 'Preview English blocks' : 'Preview Arabic blocks'}
472
+ </Button>
473
+ <Button variant="outline" onClick={() => setDirection((value) => value === 'ltr' ? 'rtl' : 'ltr')}>
474
+ {direction === 'ltr' ? 'Switch to RTL' : 'Switch to LTR'}
475
+ </Button>
476
+ <Button variant="outline" onClick={() => setDark((value) => !value)}>
477
+ {dark ? 'Switch to light' : 'Switch to dark'}
478
+ </Button>
479
+ </div>
480
+ </header>
481
+
482
+ <section aria-labelledby="composer-controls-title" className="composer-controls">
483
+ <div>
484
+ <span>Dashboard controls</span>
485
+ <h2 id="composer-controls-title">Agent and human use the same contract</h2>
486
+ </div>
487
+ <div>
488
+ <Button aria-pressed={editable} variant={editable ? 'default' : 'outline'} onClick={() => setEditable((value) => !value)}>
489
+ {editable ? 'Finish arranging' : 'Arrange blocks'}
490
+ </Button>
491
+ <Button variant="outline" onClick={proposeSwap}>Ask agent to swap To-do and Calendar</Button>
492
+ <Button
493
+ variant="outline"
494
+ onClick={() => {
495
+ setLayout(structuredClone(initialLayout))
496
+ setFeedback('Original dashboard layout restored.')
497
+ }}
498
+ >
499
+ Reset layout
500
+ </Button>
501
+ </div>
502
+ <output aria-live="polite" data-valid={validation.ok}>
503
+ <Badge variant={validation.ok ? 'secondary' : 'destructive'}>
504
+ {validation.ok ? 'Schema valid' : 'Schema failed'}
505
+ </Badge>
506
+ <span>{feedback}</span>
507
+ </output>
508
+ </section>
509
+
510
+ <section aria-labelledby="palette-heading" className="composer-palette">
511
+ <div>
512
+ <span>Registry palette</span>
513
+ <h2 id="palette-heading">Add another real block</h2>
514
+ </div>
515
+ <div>
516
+ <Button size="sm" variant="outline" onClick={() => addBlock('todo-list')}>Add To-do List</Button>
517
+ <Button size="sm" variant="outline" onClick={() => addBlock('calendar-panel')}>Add Calendar</Button>
518
+ <Button size="sm" variant="outline" onClick={() => addBlock('activity-feed')}>Add Activity Feed</Button>
519
+ <Button size="sm" variant="outline" onClick={() => addBlock('agent-status')}>Add Agent Status</Button>
520
+ <Button size="sm" variant="outline" onClick={() => addBlock('issue-table')}>Add Issue Table</Button>
521
+ <Button size="sm" variant="outline" onClick={() => addBlock('chart-panel')}>Add Chart Panel</Button>
522
+ </div>
523
+ </section>
524
+
525
+ <CeramicDashboardRenderer
526
+ direction={direction}
527
+ editable={editable}
528
+ layout={layout}
529
+ renderers={blockRenderers}
530
+ onLayoutChange={(next) => {
531
+ setLayout(next)
532
+ setFeedback('Layout updated through registered block controls.')
533
+ }}
534
+ />
535
+
536
+ <details className="composer-json">
537
+ <summary>Inspect the live Layout JSON</summary>
538
+ <p>This document stores semantic block choices and placement. Product tasks and events remain in application data.</p>
539
+ <pre>{JSON.stringify(layout, null, 2)}</pre>
540
+ </details>
541
+ </main>
542
+ </ComposerDataContext.Provider>
543
+ )
544
+ }
545
+
546
+ createRoot(document.getElementById('root')!).render(
547
+ <StrictMode>
548
+ <App />
549
+ </StrictMode>,
550
+ )