@utopia-studio-design/design-system-cli 0.4.0 → 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 (127) hide show
  1. package/README.md +23 -0
  2. package/bin/utopia-ds-mcp.mjs +9 -2
  3. package/bin/utopia-ds.mjs +115 -7
  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 +2 -1
  41. package/data/docs/interaction-foundation.md +109 -0
  42. package/data/docs/observability.md +165 -0
  43. package/data/docs/quick-start-ai.md +55 -1
  44. package/data/manifests/blocks.json +392 -0
  45. package/data/manifests/catalog.json +7 -4
  46. package/data/manifests/components.json +191 -3
  47. package/data/manifests/composition-registry.json +427 -0
  48. package/data/manifests/motion-profiles.json +1 -1
  49. package/data/manifests/patterns.json +113 -1
  50. package/data/manifests/templates.json +346 -0
  51. package/data/manifests/theme-renacore.json +142 -0
  52. package/data/manifests/themes.json +314 -28
  53. package/data/templates/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
  54. package/data/templates/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  55. package/data/templates/analytics-dashboard/README.md +6 -0
  56. package/data/templates/analytics-dashboard/analytics-dashboard.css +92 -0
  57. package/data/templates/analytics-dashboard/index.html +13 -0
  58. package/data/templates/analytics-dashboard/main.tsx +196 -0
  59. package/data/templates/analytics-dashboard/styles.css +120 -0
  60. package/data/templates/analytics-dashboard/template.manifest.json +17 -0
  61. package/data/templates/calendar-application/CalendarApplicationBlocks.tsx +519 -0
  62. package/data/templates/calendar-application/README.md +10 -0
  63. package/data/templates/calendar-application/calendar-application.css +216 -0
  64. package/data/templates/calendar-application/calendar-localization.ts +90 -0
  65. package/data/templates/calendar-application/index.html +14 -0
  66. package/data/templates/calendar-application/main.tsx +409 -0
  67. package/data/templates/calendar-application/styles.css +248 -0
  68. package/data/templates/calendar-application/template.manifest.json +27 -0
  69. package/data/templates/composition-registry-lab/README.md +9 -0
  70. package/data/templates/composition-registry-lab/index.html +14 -0
  71. package/data/templates/composition-registry-lab/main.tsx +217 -0
  72. package/data/templates/composition-registry-lab/sample-layout.json +63 -0
  73. package/data/templates/composition-registry-lab/styles.css +343 -0
  74. package/data/templates/composition-registry-lab/template.manifest.json +34 -0
  75. package/data/templates/dashboard-composer/ActivityFeedBlock.tsx +255 -0
  76. package/data/templates/dashboard-composer/AgentStatusBlock.tsx +313 -0
  77. package/data/templates/dashboard-composer/AnalyticsDashboardBlocks.tsx +107 -0
  78. package/data/templates/dashboard-composer/AnalyticsMetricBlocks.tsx +87 -0
  79. package/data/templates/dashboard-composer/CalendarApplicationBlocks.tsx +519 -0
  80. package/data/templates/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
  81. package/data/templates/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
  82. package/data/templates/dashboard-composer/DatabaseWorkspaceBlocks.tsx +240 -0
  83. package/data/templates/dashboard-composer/README.md +53 -0
  84. package/data/templates/dashboard-composer/RegistryDataBlocks.tsx +238 -0
  85. package/data/templates/dashboard-composer/activity-feed-contract.ts +96 -0
  86. package/data/templates/dashboard-composer/activity-feed-fixture.ts +22 -0
  87. package/data/templates/dashboard-composer/activity-feed.css +154 -0
  88. package/data/templates/dashboard-composer/agent-status-contract.ts +133 -0
  89. package/data/templates/dashboard-composer/agent-status-fixture.ts +79 -0
  90. package/data/templates/dashboard-composer/agent-status-lab/index.html +19 -0
  91. package/data/templates/dashboard-composer/agent-status-lab/main.tsx +151 -0
  92. package/data/templates/dashboard-composer/agent-status-lab/styles.css +139 -0
  93. package/data/templates/dashboard-composer/agent-status.css +216 -0
  94. package/data/templates/dashboard-composer/analytics-dashboard.css +92 -0
  95. package/data/templates/dashboard-composer/calendar-application.css +216 -0
  96. package/data/templates/dashboard-composer/calendar-localization.ts +90 -0
  97. package/data/templates/dashboard-composer/calendar-panel-contract.ts +72 -0
  98. package/data/templates/dashboard-composer/dashboard-composer.css +328 -0
  99. package/data/templates/dashboard-composer/database-workspace.css +107 -0
  100. package/data/templates/dashboard-composer/index.html +14 -0
  101. package/data/templates/dashboard-composer/main.tsx +550 -0
  102. package/data/templates/dashboard-composer/sample-layout.json +147 -0
  103. package/data/templates/dashboard-composer/styles.css +198 -0
  104. package/data/templates/dashboard-composer/template.manifest.json +34 -0
  105. package/data/templates/dashboard-composer/visual-editor/PuckEditorSurface.tsx +132 -0
  106. package/data/templates/dashboard-composer/visual-editor/VisualEditorApp.tsx +343 -0
  107. package/data/templates/dashboard-composer/visual-editor/index.html +17 -0
  108. package/data/templates/dashboard-composer/visual-editor/main.tsx +95 -0
  109. package/data/templates/dashboard-composer/visual-editor/styles.css +310 -0
  110. package/data/templates/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
  111. package/data/templates/database-workspace/README.md +5 -0
  112. package/data/templates/database-workspace/database-workspace.css +107 -0
  113. package/data/templates/database-workspace/index.html +14 -0
  114. package/data/templates/database-workspace/main.tsx +247 -0
  115. package/data/templates/database-workspace/styles.css +121 -0
  116. package/data/templates/database-workspace/template.manifest.json +19 -0
  117. package/data/templates/interaction-lab/InteractionWorkbench.tsx +271 -0
  118. package/data/templates/interaction-lab/README.md +7 -0
  119. package/data/templates/interaction-lab/index.html +14 -0
  120. package/data/templates/interaction-lab/interaction-workbench.css +226 -0
  121. package/data/templates/interaction-lab/main.tsx +104 -0
  122. package/data/templates/interaction-lab/styles.css +224 -0
  123. package/data/templates/interaction-lab/template.manifest.json +36 -0
  124. package/lib/api.mjs +72 -5
  125. package/lib/composition-runtime.mjs +382 -0
  126. package/lib/telemetry.mjs +74 -0
  127. package/package.json +4 -1
@@ -0,0 +1,337 @@
1
+ import * as React from 'react'
2
+ import { Badge } from '@utopia-studio-design/design-system/Badge'
3
+ import { Button } from '@utopia-studio-design/design-system/Button'
4
+ import { Checkbox, TextInput } from '@utopia-studio-design/design-system/Forms'
5
+ import {
6
+ getCeramicBlockDefinition,
7
+ validateCeramicAgentMutation,
8
+ validateCeramicLayout,
9
+ type CeramicLayoutBlock,
10
+ type CeramicLayoutDocument,
11
+ } from '@utopia-studio-design/design-system/Composition'
12
+ import './dashboard-composer.css'
13
+
14
+ export type TodoFilter = 'all' | 'today' | 'upcoming' | 'completed'
15
+ export type TodoPriority = 'low' | 'medium' | 'high'
16
+
17
+ export interface TodoItem {
18
+ id: string
19
+ title: string
20
+ completed: boolean
21
+ dueLabel?: string
22
+ priority?: TodoPriority
23
+ }
24
+
25
+ export interface TodoListBlockProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'title'> {
26
+ canCreate?: boolean
27
+ canDelete?: boolean
28
+ defaultItems?: TodoItem[]
29
+ density?: 'compact' | 'default' | 'comfortable'
30
+ filter?: TodoFilter
31
+ items?: TodoItem[]
32
+ onItemsChange?: (items: TodoItem[]) => void
33
+ showCompleted?: boolean
34
+ state?: 'loading' | 'empty' | 'error' | 'ready'
35
+ title?: string
36
+ }
37
+
38
+ export function TodoListBlock({
39
+ canCreate = true,
40
+ canDelete = true,
41
+ defaultItems = [],
42
+ density = 'default',
43
+ filter = 'all',
44
+ items: controlledItems,
45
+ onItemsChange,
46
+ showCompleted = true,
47
+ state: stateProp,
48
+ title = 'Today’s tasks',
49
+ ...props
50
+ }: TodoListBlockProps) {
51
+ const [uncontrolledItems, setUncontrolledItems] = React.useState(defaultItems)
52
+ const [draft, setDraft] = React.useState('')
53
+ const nextId = React.useRef(defaultItems.length + 1)
54
+ const titleId = React.useId()
55
+ const inputId = React.useId()
56
+ const items = controlledItems ?? uncontrolledItems
57
+ const state = stateProp ?? (items.length ? 'ready' : 'empty')
58
+ const update = (next: TodoItem[]) => {
59
+ if (controlledItems === undefined) setUncontrolledItems(next)
60
+ onItemsChange?.(next)
61
+ }
62
+ const visibleItems = items.filter((item) => {
63
+ if (!showCompleted && item.completed) return false
64
+ if (filter === 'completed') return item.completed
65
+ if (filter === 'today') return item.dueLabel === 'Today'
66
+ if (filter === 'upcoming') return item.dueLabel !== 'Today'
67
+ return true
68
+ })
69
+ const completedCount = items.filter((item) => item.completed).length
70
+
71
+ return (
72
+ <section
73
+ aria-busy={state === 'loading'}
74
+ aria-labelledby={titleId}
75
+ className="ceramic-todo-list"
76
+ data-density={density}
77
+ data-state={state}
78
+ {...props}
79
+ >
80
+ <header>
81
+ <div>
82
+ <span>To-do</span>
83
+ <h2 id={titleId}>{title}</h2>
84
+ </div>
85
+ <Badge variant="outline">{completedCount}/{items.length} done</Badge>
86
+ </header>
87
+
88
+ {state === 'loading' ? (
89
+ <div className="ceramic-todo-state" role="status">Loading tasks…</div>
90
+ ) : null}
91
+ {state === 'error' ? (
92
+ <div className="ceramic-todo-state" role="alert">
93
+ <strong>Tasks unavailable</strong>
94
+ <span>Reconnect the task source and try again.</span>
95
+ </div>
96
+ ) : null}
97
+ {state === 'empty' || (state === 'ready' && visibleItems.length === 0) ? (
98
+ <div className="ceramic-todo-state" role="status">
99
+ <strong>No tasks in this view</strong>
100
+ <span>{canCreate ? 'Add the next task when you are ready.' : 'Change the task filter.'}</span>
101
+ </div>
102
+ ) : null}
103
+ {state === 'ready' && visibleItems.length ? (
104
+ <ul className="ceramic-todo-items">
105
+ {visibleItems.map((item) => (
106
+ <li data-completed={item.completed || undefined} key={item.id}>
107
+ <label>
108
+ <Checkbox
109
+ aria-label={`Mark ${item.title} ${item.completed ? 'incomplete' : 'complete'}`}
110
+ checked={item.completed}
111
+ onCheckedChange={(checked) => update(items.map((candidate) => (
112
+ candidate.id === item.id ? { ...candidate, completed: checked === true } : candidate
113
+ )))}
114
+ />
115
+ <span>{item.title}</span>
116
+ </label>
117
+ <div>
118
+ {item.dueLabel ? <span>{item.dueLabel}</span> : null}
119
+ {item.priority ? <Badge variant={item.priority === 'high' ? 'destructive' : 'outline'}>{item.priority}</Badge> : null}
120
+ {canDelete ? (
121
+ <Button
122
+ aria-label={`Delete ${item.title}`}
123
+ isIconOnly
124
+ size="sm"
125
+ variant="ghost"
126
+ onClick={() => update(items.filter((candidate) => candidate.id !== item.id))}
127
+ >
128
+ ×
129
+ </Button>
130
+ ) : null}
131
+ </div>
132
+ </li>
133
+ ))}
134
+ </ul>
135
+ ) : null}
136
+
137
+ {canCreate ? (
138
+ <form
139
+ className="ceramic-todo-create"
140
+ onSubmit={(event) => {
141
+ event.preventDefault()
142
+ const titleValue = draft.trim()
143
+ if (!titleValue) return
144
+ while (items.some((item) => item.id === `todo-${nextId.current}`)) nextId.current += 1
145
+ update([...items, {
146
+ id: `todo-${nextId.current++}`,
147
+ title: titleValue,
148
+ completed: false,
149
+ dueLabel: 'Today',
150
+ priority: 'medium',
151
+ }])
152
+ setDraft('')
153
+ }}
154
+ >
155
+ <label className="uds-visually-hidden" htmlFor={inputId}>New task</label>
156
+ <TextInput
157
+ id={inputId}
158
+ placeholder="Add a task"
159
+ value={draft}
160
+ onChange={(event) => setDraft(event.target.value)}
161
+ />
162
+ <Button size="sm" type="submit">Add</Button>
163
+ </form>
164
+ ) : null}
165
+ <output aria-live="polite" className="uds-visually-hidden">
166
+ {completedCount} of {items.length} tasks completed.
167
+ </output>
168
+ </section>
169
+ )
170
+ }
171
+
172
+ export interface CeramicBlockRendererProps {
173
+ block: CeramicLayoutBlock
174
+ direction: 'ltr' | 'rtl'
175
+ }
176
+
177
+ export type CeramicBlockRenderer = React.ComponentType<CeramicBlockRendererProps>
178
+
179
+ export interface CeramicDashboardRendererProps extends React.HTMLAttributes<HTMLElement> {
180
+ direction?: 'ltr' | 'rtl'
181
+ editable?: boolean
182
+ layout: CeramicLayoutDocument
183
+ onLayoutChange?: (layout: CeramicLayoutDocument) => void
184
+ renderers: Record<string, CeramicBlockRenderer>
185
+ }
186
+
187
+ export function CeramicDashboardRenderer({
188
+ direction = 'ltr',
189
+ editable = false,
190
+ layout,
191
+ onLayoutChange,
192
+ renderers,
193
+ ...props
194
+ }: CeramicDashboardRendererProps) {
195
+ const [feedback, setFeedback] = React.useState('')
196
+ const validation = validateCeramicLayout(layout)
197
+ const ordered = [...layout.blocks].sort((left, right) => left.placement.order - right.placement.order)
198
+
199
+ const commit = (next: CeramicLayoutDocument, message: string) => {
200
+ const result = validateCeramicAgentMutation(layout, next)
201
+ if (!result.ok) {
202
+ setFeedback(result.issues[0]?.message ?? 'The registry rejected this layout change.')
203
+ return
204
+ }
205
+ onLayoutChange?.(next)
206
+ setFeedback(message)
207
+ }
208
+
209
+ const move = (blockId: string, offset: -1 | 1) => {
210
+ const currentIndex = ordered.findIndex((block) => block.id === blockId)
211
+ const targetIndex = currentIndex + offset
212
+ if (currentIndex < 0 || targetIndex < 0 || targetIndex >= ordered.length) return
213
+ const current = ordered[currentIndex]
214
+ const target = ordered[targetIndex]
215
+ const firstColumn = Math.min(current.placement.column, target.placement.column)
216
+ const firstOrder = Math.min(current.placement.order, target.placement.order)
217
+ const secondOrder = Math.max(current.placement.order, target.placement.order)
218
+ const firstBlock = offset < 0 ? current : target
219
+ const secondBlock = offset < 0 ? target : current
220
+ const nextBlocks = layout.blocks.map((block) => {
221
+ if (block.id === firstBlock.id) {
222
+ return {
223
+ ...block,
224
+ placement: {
225
+ ...block.placement,
226
+ column: firstColumn,
227
+ order: firstOrder,
228
+ },
229
+ }
230
+ }
231
+ if (block.id === secondBlock.id) {
232
+ return {
233
+ ...block,
234
+ placement: {
235
+ ...block.placement,
236
+ column: firstColumn + firstBlock.placement.span,
237
+ order: secondOrder,
238
+ },
239
+ }
240
+ }
241
+ return block
242
+ })
243
+ commit({ ...layout, blocks: nextBlocks }, `${current.id} moved ${offset < 0 ? 'earlier' : 'later'}.`)
244
+ }
245
+
246
+ if (!validation.ok) {
247
+ return (
248
+ <section className="ceramic-dashboard-error" role="alert" {...props}>
249
+ <strong>Dashboard layout is invalid</strong>
250
+ <span>{validation.issues[0]?.message}</span>
251
+ </section>
252
+ )
253
+ }
254
+
255
+ return (
256
+ <section
257
+ aria-label="Rendered Ceramic dashboard"
258
+ className="ceramic-dashboard-renderer"
259
+ dir={direction}
260
+ {...props}
261
+ >
262
+ {ordered.map((block, index) => {
263
+ const definition = getCeramicBlockDefinition(block.type)
264
+ const Renderer = renderers[block.type]
265
+ return (
266
+ <article
267
+ className="ceramic-dashboard-block"
268
+ data-block-type={block.type}
269
+ data-locked={block.locked || undefined}
270
+ key={block.id}
271
+ style={{
272
+ '--ceramic-block-column': block.placement.column,
273
+ '--ceramic-block-order': block.placement.order,
274
+ '--ceramic-block-span': block.placement.span,
275
+ '--ceramic-block-compact-span': block.placement.compactSpan ?? layout.columns,
276
+ } as React.CSSProperties}
277
+ >
278
+ {editable ? (
279
+ <header className="ceramic-dashboard-block-toolbar">
280
+ <div>
281
+ <Badge variant={block.locked ? 'secondary' : 'outline'}>
282
+ {block.locked ? 'Locked' : 'Editable'}
283
+ </Badge>
284
+ <strong>{definition?.title ?? block.type}</strong>
285
+ </div>
286
+ <div aria-label={`${definition?.title ?? block.type} layout actions`} role="toolbar">
287
+ <Button
288
+ aria-label={`Move ${definition?.title ?? block.type} earlier`}
289
+ disabled={block.locked || index === 0}
290
+ isIconOnly
291
+ size="sm"
292
+ variant="ghost"
293
+ onClick={() => move(block.id, -1)}
294
+ >
295
+
296
+ </Button>
297
+ <Button
298
+ aria-label={`Move ${definition?.title ?? block.type} later`}
299
+ disabled={block.locked || index === ordered.length - 1}
300
+ isIconOnly
301
+ size="sm"
302
+ variant="ghost"
303
+ onClick={() => move(block.id, 1)}
304
+ >
305
+
306
+ </Button>
307
+ <Button
308
+ aria-label={`Remove ${definition?.title ?? block.type}`}
309
+ disabled={block.locked}
310
+ isIconOnly
311
+ size="sm"
312
+ variant="ghost"
313
+ onClick={() => commit(
314
+ { ...layout, blocks: layout.blocks.filter((candidate) => candidate.id !== block.id) },
315
+ `${block.id} removed.`,
316
+ )}
317
+ >
318
+ ×
319
+ </Button>
320
+ </div>
321
+ </header>
322
+ ) : null}
323
+ {Renderer ? (
324
+ <Renderer block={block} direction={direction} />
325
+ ) : (
326
+ <div className="ceramic-dashboard-missing" role="status">
327
+ <strong>{definition?.title ?? block.type}</strong>
328
+ <span>No renderer has been connected for this block.</span>
329
+ </div>
330
+ )}
331
+ </article>
332
+ )
333
+ })}
334
+ <output aria-live="polite" className="uds-visually-hidden">{feedback}</output>
335
+ </section>
336
+ )
337
+ }
@@ -0,0 +1,16 @@
1
+ # Dashboard Composer Blocks
2
+
3
+ Copyable blocks for rendering and editing a validated Ceramic Layout JSON
4
+ document.
5
+
6
+ ## Exports
7
+
8
+ - `TodoListBlock`: a controlled or uncontrolled task list with complete states,
9
+ density and semantic filters.
10
+ - `CeramicDashboardRenderer`: maps registered layout blocks to product-supplied
11
+ React renderers and exposes keyboard/touch-accessible move and remove controls
12
+ in edit mode.
13
+
14
+ The renderer validates every layout and agent-style mutation through
15
+ `@utopia-studio-design/design-system/Composition`. Product data stays outside
16
+ Layout JSON and is supplied by the application renderer map.
@@ -0,0 +1,238 @@
1
+ import * as React from 'react'
2
+ import { Button } from '@utopia-studio-design/design-system/Button'
3
+ import type {
4
+ ChartAnnotationDefinition,
5
+ ChartDatum,
6
+ ChartSeriesDefinition,
7
+ ChartType,
8
+ } from '@utopia-studio-design/design-system/Charts'
9
+ import type {
10
+ DatabaseQuery,
11
+ DatabaseRecord,
12
+ DatabaseSchema,
13
+ } from '@utopia-studio-design/design-system/Database'
14
+ import type { CeramicLayoutBlock } from '@utopia-studio-design/design-system/Composition'
15
+ import { DatabaseTableBlock } from '../database-workspace/DatabaseWorkspaceBlocks'
16
+
17
+ const LazyAnalyticsChartCard = React.lazy(async () => {
18
+ const module = await import('../analytics-dashboard/AnalyticsDashboardBlocks')
19
+ return { default: module.AnalyticsChartCard }
20
+ })
21
+
22
+ const issueSchema: DatabaseSchema = {
23
+ id: 'composer-issues',
24
+ title: 'Delivery issues',
25
+ primaryPropertyId: 'title',
26
+ properties: [
27
+ { id: 'title', label: 'Issue', type: 'text', required: true, width: 260 },
28
+ {
29
+ id: 'status',
30
+ label: 'Status',
31
+ type: 'select',
32
+ width: 150,
33
+ options: [
34
+ { id: 'active', label: 'Active', tone: 'warning' },
35
+ { id: 'backlog', label: 'Backlog' },
36
+ { id: 'blocked', label: 'Blocked', tone: 'negative' },
37
+ { id: 'completed', label: 'Completed', tone: 'positive' },
38
+ ],
39
+ },
40
+ {
41
+ id: 'priority',
42
+ label: 'Priority',
43
+ type: 'select',
44
+ width: 130,
45
+ options: [
46
+ { id: 'high', label: 'High', tone: 'negative' },
47
+ { id: 'medium', label: 'Medium', tone: 'warning' },
48
+ { id: 'low', label: 'Low' },
49
+ ],
50
+ },
51
+ { id: 'owner', label: 'Owner', type: 'person', width: 140 },
52
+ { id: 'due', label: 'Due', type: 'date', width: 150 },
53
+ ],
54
+ }
55
+
56
+ const issueRecords: DatabaseRecord[] = [
57
+ { id: 'issue-1', values: { title: 'Audit Arabic calendar navigation', status: 'active', priority: 'high', owner: 'Mina', due: '2026-08-01' } },
58
+ { id: 'issue-2', values: { title: 'Review visual-editor payload', status: 'active', priority: 'medium', owner: 'Aiden', due: '2026-08-03' } },
59
+ { id: 'issue-3', values: { title: 'Document registry permissions', status: 'backlog', priority: 'medium', owner: 'Haajar', due: '2026-08-05' } },
60
+ { id: 'issue-4', values: { title: 'Resolve mobile table overflow', status: 'blocked', priority: 'high', owner: 'Sora', due: '2026-07-31' } },
61
+ { id: 'issue-5', values: { title: 'Verify Button asChild regression', status: 'completed', priority: 'high', owner: 'Mina', due: '2026-07-29' } },
62
+ { id: 'issue-6', values: { title: 'Publish Calendar QA fixture', status: 'completed', priority: 'low', owner: 'Aiden', due: '2026-07-30' } },
63
+ ]
64
+
65
+ const chartData: ChartDatum[] = [
66
+ { id: 'mon', label: 'Mon', values: { completed: 18, previous: 14 } },
67
+ { id: 'tue', label: 'Tue', values: { completed: 23, previous: 17 } },
68
+ { id: 'wed', label: 'Wed', values: { completed: 20, previous: 19 } },
69
+ { id: 'thu', label: 'Thu', values: { completed: 28, previous: 21 } },
70
+ { id: 'fri', label: 'Fri', values: { completed: 31, previous: 25 } },
71
+ ]
72
+
73
+ const currentSeries: ChartSeriesDefinition = {
74
+ id: 'completed',
75
+ label: 'Completed work',
76
+ tone: 'primary',
77
+ }
78
+ const previousSeries: ChartSeriesDefinition = {
79
+ id: 'previous',
80
+ label: 'Previous period',
81
+ pattern: 'dash',
82
+ tone: 'benchmark',
83
+ }
84
+
85
+ function issueQuery(view: unknown): DatabaseQuery {
86
+ if (view === 'active') {
87
+ return {
88
+ filters: [{
89
+ id: 'active-issues',
90
+ propertyId: 'status',
91
+ operator: 'not-equals',
92
+ value: 'completed',
93
+ }],
94
+ }
95
+ }
96
+ if (view === 'backlog' || view === 'blocked' || view === 'completed') {
97
+ return {
98
+ filters: [{
99
+ id: `${view}-issues`,
100
+ propertyId: 'status',
101
+ operator: 'equals',
102
+ value: view,
103
+ }],
104
+ }
105
+ }
106
+ return {}
107
+ }
108
+
109
+ export function IssueTableRegistryBlock({
110
+ block,
111
+ direction,
112
+ }: {
113
+ block: CeramicLayoutBlock
114
+ direction: 'ltr' | 'rtl'
115
+ }) {
116
+ const requestedColumns = Array.isArray(block.props.columns)
117
+ ? block.props.columns.filter((column): column is string => typeof column === 'string')
118
+ : []
119
+ const visibleColumnIds = new Set(
120
+ requestedColumns.length
121
+ ? ['title', ...requestedColumns]
122
+ : issueSchema.properties.map((property) => property.id),
123
+ )
124
+ const density = (
125
+ block.props.density === 'compact'
126
+ || block.props.density === 'comfortable'
127
+ || block.props.density === 'default'
128
+ )
129
+ ? block.props.density
130
+ : 'default'
131
+
132
+ return (
133
+ <div className="ceramic-issue-table-block">
134
+ <DatabaseTableBlock
135
+ density={density}
136
+ direction={direction}
137
+ permissions={{
138
+ bulkActions: false,
139
+ edit: false,
140
+ reorderColumns: false,
141
+ reorderRows: false,
142
+ resizeColumns: false,
143
+ }}
144
+ query={issueQuery(block.props.view)}
145
+ records={issueRecords}
146
+ schema={{
147
+ ...issueSchema,
148
+ properties: issueSchema.properties.map((property) => ({
149
+ ...property,
150
+ hidden: !visibleColumnIds.has(property.id),
151
+ })),
152
+ }}
153
+ state="ready"
154
+ />
155
+ </div>
156
+ )
157
+ }
158
+
159
+ class ChartLoadBoundary extends React.Component<
160
+ { children: React.ReactNode },
161
+ { failed: boolean }
162
+ > {
163
+ state = { failed: false }
164
+
165
+ static getDerivedStateFromError() {
166
+ return { failed: true }
167
+ }
168
+
169
+ componentDidCatch(error: Error) {
170
+ console.error('The registered chart renderer failed to load.', error)
171
+ }
172
+
173
+ render() {
174
+ if (this.state.failed) {
175
+ return (
176
+ <div className="ceramic-chart-panel-state" role="alert">
177
+ <strong>Chart preview could not load.</strong>
178
+ <span>The layout remains valid. Retry the optional visualization module.</span>
179
+ <Button size="sm" variant="outline" onClick={() => window.location.reload()}>
180
+ Retry chart
181
+ </Button>
182
+ </div>
183
+ )
184
+ }
185
+ return this.props.children
186
+ }
187
+ }
188
+
189
+ export function ChartPanelRegistryBlock({
190
+ block,
191
+ direction,
192
+ }: {
193
+ block: CeramicLayoutBlock
194
+ direction: 'ltr' | 'rtl'
195
+ }) {
196
+ const chartType = (
197
+ ['line', 'bar', 'area', 'stacked', 'scatter', 'donut'] as ChartType[]
198
+ ).includes(block.props.chartType as ChartType)
199
+ ? block.props.chartType as ChartType
200
+ : 'line'
201
+ const comparison = typeof block.props.comparison === 'string'
202
+ ? block.props.comparison
203
+ : 'previous-period'
204
+ const annotations: ChartAnnotationDefinition[] = comparison === 'benchmark'
205
+ ? [{ id: 'benchmark', label: 'Team benchmark', value: 24, tone: 'neutral' }]
206
+ : comparison === 'target'
207
+ ? [{ id: 'target', label: 'Weekly target', value: 30, tone: 'neutral' }]
208
+ : []
209
+ const series = comparison === 'previous-period'
210
+ ? [currentSeries, previousSeries]
211
+ : [currentSeries]
212
+
213
+ return (
214
+ <div className="ceramic-chart-panel-block" dir={direction}>
215
+ <ChartLoadBoundary>
216
+ <React.Suspense
217
+ fallback={(
218
+ <div className="ceramic-chart-panel-state" role="status">
219
+ Loading chart preview…
220
+ </div>
221
+ )}
222
+ >
223
+ <LazyAnalyticsChartCard
224
+ annotations={annotations}
225
+ data={chartData}
226
+ description="Completed delivery work by weekday"
227
+ direction={direction}
228
+ series={series}
229
+ showDataTable={block.props.showTable === true}
230
+ state="ready"
231
+ title="Delivery throughput"
232
+ type={chartType}
233
+ />
234
+ </React.Suspense>
235
+ </ChartLoadBoundary>
236
+ </div>
237
+ )
238
+ }