@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,240 @@
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 {
5
+ DatabasePropertyCell,
6
+ DatabaseRoot,
7
+ type DatabaseMutationAdapter,
8
+ type DatabasePermissions,
9
+ type DatabaseQuery,
10
+ type DatabaseRecord,
11
+ type DatabaseSchema,
12
+ type DatabaseState,
13
+ type DatabaseView,
14
+ } from '@utopia-studio-design/design-system/Database'
15
+ import './database-workspace.css'
16
+
17
+ export interface DatabaseToolbarProps extends React.HTMLAttributes<HTMLElement> {
18
+ activeFilterCount?: number
19
+ activeSortCount?: number
20
+ groupLabel?: string
21
+ onCreate?: () => void
22
+ onFilter?: () => void
23
+ onSearchChange?: (value: string) => void
24
+ onSort?: () => void
25
+ searchValue?: string
26
+ }
27
+
28
+ export function DatabaseToolbar({
29
+ activeFilterCount = 0,
30
+ activeSortCount = 0,
31
+ groupLabel,
32
+ onCreate,
33
+ onFilter,
34
+ onSearchChange,
35
+ onSort,
36
+ searchValue = '',
37
+ ...props
38
+ }: DatabaseToolbarProps) {
39
+ return (
40
+ <header className="ceramic-database-toolbar" {...props}>
41
+ <label>
42
+ <span className="uds-visually-hidden">Search records</span>
43
+ <input
44
+ placeholder="Search records"
45
+ type="search"
46
+ value={searchValue}
47
+ onChange={(event) => onSearchChange?.(event.target.value)}
48
+ />
49
+ </label>
50
+ <div>
51
+ <Button size="sm" variant="outline" onClick={onFilter}>
52
+ Filter {activeFilterCount ? `(${activeFilterCount})` : ''}
53
+ </Button>
54
+ <Button size="sm" variant="outline" onClick={onSort}>
55
+ Sort {activeSortCount ? `(${activeSortCount})` : ''}
56
+ </Button>
57
+ {groupLabel ? <Badge variant="outline">Grouped by {groupLabel}</Badge> : null}
58
+ </div>
59
+ <Button size="sm" onClick={onCreate}>New record</Button>
60
+ </header>
61
+ )
62
+ }
63
+
64
+ export interface SavedViewsBarProps extends React.HTMLAttributes<HTMLElement> {
65
+ activeViewId: string
66
+ onViewChange?: (viewId: string) => void
67
+ views: DatabaseView[]
68
+ }
69
+
70
+ export function SavedViewsBar({
71
+ activeViewId,
72
+ onViewChange,
73
+ views,
74
+ ...props
75
+ }: SavedViewsBarProps) {
76
+ return (
77
+ <nav aria-label="Workspace views" className="ceramic-saved-views" {...props}>
78
+ {views.map((view) => (
79
+ <Button
80
+ aria-current={activeViewId === view.id ? 'page' : undefined}
81
+ key={view.id}
82
+ size="sm"
83
+ variant={activeViewId === view.id ? 'secondary' : 'ghost'}
84
+ onClick={() => onViewChange?.(view.id)}
85
+ >
86
+ {view.label}
87
+ </Button>
88
+ ))}
89
+ </nav>
90
+ )
91
+ }
92
+
93
+ export interface DatabaseTableBlockProps extends React.HTMLAttributes<HTMLElement> {
94
+ adapter?: DatabaseMutationAdapter
95
+ density?: 'compact' | 'default' | 'comfortable'
96
+ direction?: 'ltr' | 'rtl'
97
+ locale?: string
98
+ onRecordOpen?: (record: DatabaseRecord) => void
99
+ onCopyCell?: (text: string, recordId: string, propertyId: string) => void
100
+ onPasteCell?: (recordId: string, propertyId: string) => void
101
+ onRecordsChange?: (records: DatabaseRecord[]) => void
102
+ permissions?: DatabasePermissions
103
+ query?: DatabaseQuery
104
+ records: DatabaseRecord[]
105
+ schema: DatabaseSchema
106
+ state?: DatabaseState
107
+ views?: DatabaseView[]
108
+ }
109
+
110
+ export function DatabaseTableBlock({
111
+ adapter,
112
+ density,
113
+ direction,
114
+ locale,
115
+ onRecordOpen,
116
+ onCopyCell,
117
+ onPasteCell,
118
+ onRecordsChange,
119
+ permissions,
120
+ query,
121
+ records,
122
+ schema,
123
+ state,
124
+ views,
125
+ ...props
126
+ }: DatabaseTableBlockProps) {
127
+ return (
128
+ <DatabaseRoot
129
+ adapter={adapter}
130
+ density={density}
131
+ direction={direction}
132
+ locale={locale}
133
+ permissions={permissions}
134
+ query={query}
135
+ records={records}
136
+ schema={schema}
137
+ state={state}
138
+ views={views}
139
+ onRecordOpen={onRecordOpen}
140
+ onCopyCell={onCopyCell}
141
+ onPasteCell={onPasteCell}
142
+ onRecordsChange={onRecordsChange}
143
+ {...props}
144
+ />
145
+ )
146
+ }
147
+
148
+ export interface RecordDetailDrawerProps extends React.HTMLAttributes<HTMLElement> {
149
+ onClose?: () => void
150
+ open: boolean
151
+ record: DatabaseRecord | null
152
+ schema: DatabaseSchema
153
+ }
154
+
155
+ export function RecordDetailDrawer({
156
+ onClose,
157
+ open,
158
+ record,
159
+ schema,
160
+ ...props
161
+ }: RecordDetailDrawerProps) {
162
+ const drawerRef = React.useRef<HTMLElement>(null)
163
+ const closeRef = React.useRef<HTMLButtonElement>(null)
164
+ React.useLayoutEffect(() => {
165
+ if (!open || !record) return
166
+ const trigger = document.activeElement instanceof HTMLElement ? document.activeElement : null
167
+ closeRef.current?.focus()
168
+ return () => trigger?.focus()
169
+ }, [open, record])
170
+ if (!open || !record) return null
171
+ return (
172
+ <>
173
+ <div aria-hidden="true" className="ceramic-record-drawer-backdrop" onClick={onClose} />
174
+ <aside
175
+ aria-label="Record details"
176
+ aria-modal="true"
177
+ className="ceramic-record-drawer"
178
+ ref={drawerRef}
179
+ role="dialog"
180
+ onKeyDown={(event) => {
181
+ if (event.key === 'Escape') {
182
+ event.preventDefault()
183
+ onClose?.()
184
+ return
185
+ }
186
+ if (event.key !== 'Tab') return
187
+ const focusable = [...(drawerRef.current?.querySelectorAll<HTMLElement>(
188
+ 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
189
+ ) ?? [])]
190
+ if (focusable.length === 0) return
191
+ const first = focusable[0]
192
+ const last = focusable.at(-1)!
193
+ if (event.shiftKey && document.activeElement === first) {
194
+ event.preventDefault()
195
+ last.focus()
196
+ } else if (!event.shiftKey && document.activeElement === last) {
197
+ event.preventDefault()
198
+ first.focus()
199
+ }
200
+ }}
201
+ {...props}
202
+ >
203
+ <header>
204
+ <div>
205
+ <span>Record detail</span>
206
+ <h2>{String(record.values[schema.primaryPropertyId] ?? 'Untitled')}</h2>
207
+ </div>
208
+ <Button ref={closeRef} aria-label="Close record details" isIconOnly variant="ghost" onClick={onClose}>×</Button>
209
+ </header>
210
+ <dl>
211
+ {schema.properties.map((property) => (
212
+ <div key={property.id}>
213
+ <dt>{property.label}</dt>
214
+ <dd>
215
+ <DatabasePropertyCell property={property} value={record.values[property.id]} />
216
+ </dd>
217
+ </div>
218
+ ))}
219
+ </dl>
220
+ </aside>
221
+ </>
222
+ )
223
+ }
224
+
225
+ export interface BulkActionBarProps extends React.HTMLAttributes<HTMLElement> {
226
+ count: number
227
+ onClear?: () => void
228
+ onDelete?: () => void
229
+ }
230
+
231
+ export function BulkActionBar({ count, onClear, onDelete, ...props }: BulkActionBarProps) {
232
+ if (count === 0) return null
233
+ return (
234
+ <div aria-label="Selected record actions" className="ceramic-bulk-action-bar" role="toolbar" {...props}>
235
+ <strong>{count} selected</strong>
236
+ <Button size="sm" variant="ghost" onClick={onClear}>Clear</Button>
237
+ <Button size="sm" variant="destructive" onClick={onDelete}>Delete</Button>
238
+ </div>
239
+ )
240
+ }
@@ -0,0 +1,53 @@
1
+ # Dashboard Composer
2
+
3
+ A runnable example of the missing runtime layer between Ceramic Layout JSON and
4
+ real dashboard blocks.
5
+
6
+ It renders To-do, Calendar, Issue Table, Chart Panel, Activity Feed, Agent
7
+ Status, and Metrics through a product-supplied renderer map, validates every
8
+ layout and agent mutation, preserves locked regions, and exposes
9
+ keyboard/touch-accessible arrange controls.
10
+
11
+ `/visual-editor/` adds Puck as an optional editing layer. Its component palette,
12
+ editable fields, defaults, and permissions are generated from the Ceramic
13
+ Composition Registry. Puck Data is converted back to Layout JSON and must pass
14
+ `validateCeramicAgentMutation` before publish is accepted. Product tasks,
15
+ events, authorization, and persistence remain outside both documents. The route
16
+ renders a 3 KB Ceramic boot shell first, then lazy-loads registered block
17
+ previews and editing controls, followed by the Puck surface and its CSS. Runtime
18
+ and state-lab routes never download that optional editor payload. The editor
19
+ uses Puck's `no-external.css` entry so it does not request a hosted Inter font;
20
+ its chrome maps Puck's public CSS variables to Ceramic semantic surface, text,
21
+ interaction, focus, radius, typography, and motion tokens in light and dark
22
+ modes.
23
+
24
+ `/agent-status-lab/` renders loading, empty, recoverable error, filtered-empty,
25
+ and ready/approval states from the same production block. It is the deterministic
26
+ browser fixture for resilience, retry feedback, Arabic/RTL, dark mode, and
27
+ mobile reflow.
28
+
29
+ The Calendar renderer uses the shared `calendar-panel-contract` resolver.
30
+ Changing locale, direction mode, timezone, week start, secondary calendar, or
31
+ view through Layout JSON therefore produces the same Scheduler configuration
32
+ in the runtime and the visual editor.
33
+
34
+ The Activity Feed renderer uses `activity-feed-contract` for density, grouping,
35
+ locale, direction, timezone, source visibility, and pagination size. The
36
+ application continues to own localized activity records and backend cursors.
37
+
38
+ The Agent Status renderer uses `agent-status-contract` for density, semantic
39
+ filtering, locale, direction, timezone, approval visibility, and item limits.
40
+ The application continues to own execution records and approve/reject
41
+ callbacks; summaries must already be sanitized.
42
+
43
+ The Metrics renderer imports `KPIGrid` from `AnalyticsMetricBlocks.tsx`.
44
+ Metric-only compositions therefore keep the locale-aware KPI API without
45
+ preloading Recharts or the analytics chart explorer.
46
+
47
+ The Issue Table consumes `view`, `density`, and governed `columns` while issue
48
+ records, permissions, and persistence remain product-owned. The Chart Panel
49
+ consumes `chartType`, `comparison`, and `showTable`; its Recharts-backed
50
+ renderer is lazy-loaded only when a registered chart block is present.
51
+
52
+ Install `@puckeditor/core@^0.22.4` when copying the optional visual-editor
53
+ route into a consumer application.