@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.
- package/README.md +23 -0
- package/bin/utopia-ds-mcp.mjs +9 -2
- package/bin/utopia-ds.mjs +115 -7
- package/data/blocks/activity-feed/ActivityFeedBlock.tsx +255 -0
- package/data/blocks/activity-feed/README.md +16 -0
- package/data/blocks/activity-feed/activity-feed-contract.ts +96 -0
- package/data/blocks/activity-feed/activity-feed-fixture.ts +22 -0
- package/data/blocks/activity-feed/activity-feed.css +154 -0
- package/data/blocks/agent-status/AgentStatusBlock.tsx +313 -0
- package/data/blocks/agent-status/README.md +15 -0
- package/data/blocks/agent-status/agent-status-contract.ts +133 -0
- package/data/blocks/agent-status/agent-status-fixture.ts +79 -0
- package/data/blocks/agent-status/agent-status.css +216 -0
- package/data/blocks/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
- package/data/blocks/analytics-dashboard/README.md +17 -0
- package/data/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
- package/data/blocks/calendar-application/CalendarApplicationBlocks.tsx +519 -0
- package/data/blocks/calendar-application/README.md +20 -0
- package/data/blocks/calendar-application/calendar-application.css +216 -0
- package/data/blocks/calendar-application/calendar-localization.ts +90 -0
- package/data/blocks/calendar-application/calendar-panel-contract.ts +72 -0
- package/data/blocks/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
- package/data/blocks/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
- package/data/blocks/dashboard-composer/README.md +16 -0
- package/data/blocks/dashboard-composer/RegistryDataBlocks.tsx +238 -0
- package/data/blocks/dashboard-composer/dashboard-composer.css +328 -0
- package/data/blocks/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/blocks/database-workspace/README.md +9 -0
- package/data/blocks/database-workspace/database-workspace.css +107 -0
- package/data/blocks/interaction-workbench/InteractionWorkbench.tsx +271 -0
- package/data/blocks/interaction-workbench/README.md +9 -0
- package/data/blocks/interaction-workbench/interaction-workbench.css +226 -0
- package/data/docs/calendar.md +164 -0
- package/data/docs/charts.md +95 -0
- package/data/docs/composition-registry.md +174 -0
- package/data/docs/dashboard-composer.md +94 -0
- package/data/docs/database.md +68 -0
- package/data/docs/examples/dashboard-layout.v1.json +65 -0
- package/data/docs/foundations.md +2 -1
- package/data/docs/interaction-foundation.md +109 -0
- package/data/docs/observability.md +165 -0
- package/data/docs/quick-start-ai.md +55 -1
- package/data/manifests/blocks.json +392 -0
- package/data/manifests/catalog.json +7 -4
- package/data/manifests/components.json +191 -3
- package/data/manifests/composition-registry.json +427 -0
- package/data/manifests/motion-profiles.json +1 -1
- package/data/manifests/patterns.json +113 -1
- package/data/manifests/templates.json +346 -0
- package/data/manifests/theme-renacore.json +142 -0
- package/data/manifests/themes.json +314 -28
- package/data/templates/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/templates/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
- package/data/templates/analytics-dashboard/README.md +6 -0
- package/data/templates/analytics-dashboard/analytics-dashboard.css +92 -0
- package/data/templates/analytics-dashboard/index.html +13 -0
- package/data/templates/analytics-dashboard/main.tsx +196 -0
- package/data/templates/analytics-dashboard/styles.css +120 -0
- package/data/templates/analytics-dashboard/template.manifest.json +17 -0
- package/data/templates/calendar-application/CalendarApplicationBlocks.tsx +519 -0
- package/data/templates/calendar-application/README.md +10 -0
- package/data/templates/calendar-application/calendar-application.css +216 -0
- package/data/templates/calendar-application/calendar-localization.ts +90 -0
- package/data/templates/calendar-application/index.html +14 -0
- package/data/templates/calendar-application/main.tsx +409 -0
- package/data/templates/calendar-application/styles.css +248 -0
- package/data/templates/calendar-application/template.manifest.json +27 -0
- package/data/templates/composition-registry-lab/README.md +9 -0
- package/data/templates/composition-registry-lab/index.html +14 -0
- package/data/templates/composition-registry-lab/main.tsx +217 -0
- package/data/templates/composition-registry-lab/sample-layout.json +63 -0
- package/data/templates/composition-registry-lab/styles.css +343 -0
- package/data/templates/composition-registry-lab/template.manifest.json +34 -0
- package/data/templates/dashboard-composer/ActivityFeedBlock.tsx +255 -0
- package/data/templates/dashboard-composer/AgentStatusBlock.tsx +313 -0
- package/data/templates/dashboard-composer/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/templates/dashboard-composer/AnalyticsMetricBlocks.tsx +87 -0
- package/data/templates/dashboard-composer/CalendarApplicationBlocks.tsx +519 -0
- package/data/templates/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
- package/data/templates/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
- package/data/templates/dashboard-composer/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/templates/dashboard-composer/README.md +53 -0
- package/data/templates/dashboard-composer/RegistryDataBlocks.tsx +238 -0
- package/data/templates/dashboard-composer/activity-feed-contract.ts +96 -0
- package/data/templates/dashboard-composer/activity-feed-fixture.ts +22 -0
- package/data/templates/dashboard-composer/activity-feed.css +154 -0
- package/data/templates/dashboard-composer/agent-status-contract.ts +133 -0
- package/data/templates/dashboard-composer/agent-status-fixture.ts +79 -0
- package/data/templates/dashboard-composer/agent-status-lab/index.html +19 -0
- package/data/templates/dashboard-composer/agent-status-lab/main.tsx +151 -0
- package/data/templates/dashboard-composer/agent-status-lab/styles.css +139 -0
- package/data/templates/dashboard-composer/agent-status.css +216 -0
- package/data/templates/dashboard-composer/analytics-dashboard.css +92 -0
- package/data/templates/dashboard-composer/calendar-application.css +216 -0
- package/data/templates/dashboard-composer/calendar-localization.ts +90 -0
- package/data/templates/dashboard-composer/calendar-panel-contract.ts +72 -0
- package/data/templates/dashboard-composer/dashboard-composer.css +328 -0
- package/data/templates/dashboard-composer/database-workspace.css +107 -0
- package/data/templates/dashboard-composer/index.html +14 -0
- package/data/templates/dashboard-composer/main.tsx +550 -0
- package/data/templates/dashboard-composer/sample-layout.json +147 -0
- package/data/templates/dashboard-composer/styles.css +198 -0
- package/data/templates/dashboard-composer/template.manifest.json +34 -0
- package/data/templates/dashboard-composer/visual-editor/PuckEditorSurface.tsx +132 -0
- package/data/templates/dashboard-composer/visual-editor/VisualEditorApp.tsx +343 -0
- package/data/templates/dashboard-composer/visual-editor/index.html +17 -0
- package/data/templates/dashboard-composer/visual-editor/main.tsx +95 -0
- package/data/templates/dashboard-composer/visual-editor/styles.css +310 -0
- package/data/templates/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/templates/database-workspace/README.md +5 -0
- package/data/templates/database-workspace/database-workspace.css +107 -0
- package/data/templates/database-workspace/index.html +14 -0
- package/data/templates/database-workspace/main.tsx +247 -0
- package/data/templates/database-workspace/styles.css +121 -0
- package/data/templates/database-workspace/template.manifest.json +19 -0
- package/data/templates/interaction-lab/InteractionWorkbench.tsx +271 -0
- package/data/templates/interaction-lab/README.md +7 -0
- package/data/templates/interaction-lab/index.html +14 -0
- package/data/templates/interaction-lab/interaction-workbench.css +226 -0
- package/data/templates/interaction-lab/main.tsx +104 -0
- package/data/templates/interaction-lab/styles.css +224 -0
- package/data/templates/interaction-lab/template.manifest.json +36 -0
- package/lib/api.mjs +72 -5
- package/lib/composition-runtime.mjs +382 -0
- package/lib/telemetry.mjs +74 -0
- 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
|
+
}
|