@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.
- package/README.md +23 -0
- package/bin/utopia-ds-mcp.mjs +9 -2
- package/bin/utopia-ds.mjs +106 -8
- 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 +1 -1
- package/data/docs/interaction-foundation.md +109 -0
- package/data/docs/quick-start-ai.md +23 -1
- package/data/manifests/blocks.json +392 -0
- package/data/manifests/catalog.json +7 -4
- package/data/manifests/components.json +179 -0
- 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/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.
|