@utopia-studio-design/design-system-cli 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +23 -0
  2. package/bin/utopia-ds-mcp.mjs +9 -2
  3. package/bin/utopia-ds.mjs +106 -8
  4. package/data/blocks/activity-feed/ActivityFeedBlock.tsx +255 -0
  5. package/data/blocks/activity-feed/README.md +16 -0
  6. package/data/blocks/activity-feed/activity-feed-contract.ts +96 -0
  7. package/data/blocks/activity-feed/activity-feed-fixture.ts +22 -0
  8. package/data/blocks/activity-feed/activity-feed.css +154 -0
  9. package/data/blocks/agent-status/AgentStatusBlock.tsx +313 -0
  10. package/data/blocks/agent-status/README.md +15 -0
  11. package/data/blocks/agent-status/agent-status-contract.ts +133 -0
  12. package/data/blocks/agent-status/agent-status-fixture.ts +79 -0
  13. package/data/blocks/agent-status/agent-status.css +216 -0
  14. package/data/blocks/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
  15. package/data/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  16. package/data/blocks/analytics-dashboard/README.md +17 -0
  17. package/data/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
  18. package/data/blocks/calendar-application/CalendarApplicationBlocks.tsx +519 -0
  19. package/data/blocks/calendar-application/README.md +20 -0
  20. package/data/blocks/calendar-application/calendar-application.css +216 -0
  21. package/data/blocks/calendar-application/calendar-localization.ts +90 -0
  22. package/data/blocks/calendar-application/calendar-panel-contract.ts +72 -0
  23. package/data/blocks/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
  24. package/data/blocks/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
  25. package/data/blocks/dashboard-composer/README.md +16 -0
  26. package/data/blocks/dashboard-composer/RegistryDataBlocks.tsx +238 -0
  27. package/data/blocks/dashboard-composer/dashboard-composer.css +328 -0
  28. package/data/blocks/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
  29. package/data/blocks/database-workspace/README.md +9 -0
  30. package/data/blocks/database-workspace/database-workspace.css +107 -0
  31. package/data/blocks/interaction-workbench/InteractionWorkbench.tsx +271 -0
  32. package/data/blocks/interaction-workbench/README.md +9 -0
  33. package/data/blocks/interaction-workbench/interaction-workbench.css +226 -0
  34. package/data/docs/calendar.md +164 -0
  35. package/data/docs/charts.md +95 -0
  36. package/data/docs/composition-registry.md +174 -0
  37. package/data/docs/dashboard-composer.md +94 -0
  38. package/data/docs/database.md +68 -0
  39. package/data/docs/examples/dashboard-layout.v1.json +65 -0
  40. package/data/docs/foundations.md +1 -1
  41. package/data/docs/interaction-foundation.md +109 -0
  42. package/data/docs/quick-start-ai.md +23 -1
  43. package/data/manifests/blocks.json +392 -0
  44. package/data/manifests/catalog.json +7 -4
  45. package/data/manifests/components.json +179 -0
  46. package/data/manifests/composition-registry.json +427 -0
  47. package/data/manifests/motion-profiles.json +1 -1
  48. package/data/manifests/patterns.json +113 -1
  49. package/data/manifests/templates.json +346 -0
  50. package/data/manifests/theme-renacore.json +142 -0
  51. package/data/manifests/themes.json +314 -28
  52. package/data/templates/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
  53. package/data/templates/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  54. package/data/templates/analytics-dashboard/README.md +6 -0
  55. package/data/templates/analytics-dashboard/analytics-dashboard.css +92 -0
  56. package/data/templates/analytics-dashboard/index.html +13 -0
  57. package/data/templates/analytics-dashboard/main.tsx +196 -0
  58. package/data/templates/analytics-dashboard/styles.css +120 -0
  59. package/data/templates/analytics-dashboard/template.manifest.json +17 -0
  60. package/data/templates/calendar-application/CalendarApplicationBlocks.tsx +519 -0
  61. package/data/templates/calendar-application/README.md +10 -0
  62. package/data/templates/calendar-application/calendar-application.css +216 -0
  63. package/data/templates/calendar-application/calendar-localization.ts +90 -0
  64. package/data/templates/calendar-application/index.html +14 -0
  65. package/data/templates/calendar-application/main.tsx +409 -0
  66. package/data/templates/calendar-application/styles.css +248 -0
  67. package/data/templates/calendar-application/template.manifest.json +27 -0
  68. package/data/templates/composition-registry-lab/README.md +9 -0
  69. package/data/templates/composition-registry-lab/index.html +14 -0
  70. package/data/templates/composition-registry-lab/main.tsx +217 -0
  71. package/data/templates/composition-registry-lab/sample-layout.json +63 -0
  72. package/data/templates/composition-registry-lab/styles.css +343 -0
  73. package/data/templates/composition-registry-lab/template.manifest.json +34 -0
  74. package/data/templates/dashboard-composer/ActivityFeedBlock.tsx +255 -0
  75. package/data/templates/dashboard-composer/AgentStatusBlock.tsx +313 -0
  76. package/data/templates/dashboard-composer/AnalyticsDashboardBlocks.tsx +107 -0
  77. package/data/templates/dashboard-composer/AnalyticsMetricBlocks.tsx +87 -0
  78. package/data/templates/dashboard-composer/CalendarApplicationBlocks.tsx +519 -0
  79. package/data/templates/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
  80. package/data/templates/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
  81. package/data/templates/dashboard-composer/DatabaseWorkspaceBlocks.tsx +240 -0
  82. package/data/templates/dashboard-composer/README.md +53 -0
  83. package/data/templates/dashboard-composer/RegistryDataBlocks.tsx +238 -0
  84. package/data/templates/dashboard-composer/activity-feed-contract.ts +96 -0
  85. package/data/templates/dashboard-composer/activity-feed-fixture.ts +22 -0
  86. package/data/templates/dashboard-composer/activity-feed.css +154 -0
  87. package/data/templates/dashboard-composer/agent-status-contract.ts +133 -0
  88. package/data/templates/dashboard-composer/agent-status-fixture.ts +79 -0
  89. package/data/templates/dashboard-composer/agent-status-lab/index.html +19 -0
  90. package/data/templates/dashboard-composer/agent-status-lab/main.tsx +151 -0
  91. package/data/templates/dashboard-composer/agent-status-lab/styles.css +139 -0
  92. package/data/templates/dashboard-composer/agent-status.css +216 -0
  93. package/data/templates/dashboard-composer/analytics-dashboard.css +92 -0
  94. package/data/templates/dashboard-composer/calendar-application.css +216 -0
  95. package/data/templates/dashboard-composer/calendar-localization.ts +90 -0
  96. package/data/templates/dashboard-composer/calendar-panel-contract.ts +72 -0
  97. package/data/templates/dashboard-composer/dashboard-composer.css +328 -0
  98. package/data/templates/dashboard-composer/database-workspace.css +107 -0
  99. package/data/templates/dashboard-composer/index.html +14 -0
  100. package/data/templates/dashboard-composer/main.tsx +550 -0
  101. package/data/templates/dashboard-composer/sample-layout.json +147 -0
  102. package/data/templates/dashboard-composer/styles.css +198 -0
  103. package/data/templates/dashboard-composer/template.manifest.json +34 -0
  104. package/data/templates/dashboard-composer/visual-editor/PuckEditorSurface.tsx +132 -0
  105. package/data/templates/dashboard-composer/visual-editor/VisualEditorApp.tsx +343 -0
  106. package/data/templates/dashboard-composer/visual-editor/index.html +17 -0
  107. package/data/templates/dashboard-composer/visual-editor/main.tsx +95 -0
  108. package/data/templates/dashboard-composer/visual-editor/styles.css +310 -0
  109. package/data/templates/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
  110. package/data/templates/database-workspace/README.md +5 -0
  111. package/data/templates/database-workspace/database-workspace.css +107 -0
  112. package/data/templates/database-workspace/index.html +14 -0
  113. package/data/templates/database-workspace/main.tsx +247 -0
  114. package/data/templates/database-workspace/styles.css +121 -0
  115. package/data/templates/database-workspace/template.manifest.json +19 -0
  116. package/data/templates/interaction-lab/InteractionWorkbench.tsx +271 -0
  117. package/data/templates/interaction-lab/README.md +7 -0
  118. package/data/templates/interaction-lab/index.html +14 -0
  119. package/data/templates/interaction-lab/interaction-workbench.css +226 -0
  120. package/data/templates/interaction-lab/main.tsx +104 -0
  121. package/data/templates/interaction-lab/styles.css +224 -0
  122. package/data/templates/interaction-lab/template.manifest.json +36 -0
  123. package/lib/api.mjs +72 -5
  124. package/lib/composition-runtime.mjs +382 -0
  125. package/package.json +4 -1
@@ -0,0 +1,380 @@
1
+ import * as React from 'react'
2
+ import type { Config, Data, Field } from '@puckeditor/core'
3
+ import {
4
+ getCeramicBlockDefinition,
5
+ listCeramicBlockDefinitions,
6
+ validateCeramicAgentMutation,
7
+ type CeramicBlockDefinition,
8
+ type CeramicLayoutBlock,
9
+ type CeramicLayoutDocument,
10
+ type CeramicPropRule,
11
+ } from '@utopia-studio-design/design-system/Composition'
12
+ import type { CeramicBlockRenderer } from './DashboardComposerBlocks'
13
+
14
+ type PuckBlockProps = Record<string, unknown> & { id: string }
15
+
16
+ export interface CeramicPuckConversionResult {
17
+ layout: CeramicLayoutDocument | null
18
+ issues: string[]
19
+ ok: boolean
20
+ }
21
+
22
+ export interface CeramicPuckAdapterOptions {
23
+ colorMode?: 'light' | 'dark'
24
+ direction?: 'ltr' | 'rtl'
25
+ layout: CeramicLayoutDocument
26
+ renderers: Record<string, CeramicBlockRenderer>
27
+ }
28
+
29
+ const fallbackDefaults: Record<string, Record<string, unknown>> = {
30
+ 'agent-status': {
31
+ density: 'default',
32
+ direction: 'inherit',
33
+ filter: 'all',
34
+ locale: 'en-US',
35
+ maxItems: 6,
36
+ showApprovals: true,
37
+ timeZone: 'UTC',
38
+ },
39
+ 'activity-feed': {
40
+ density: 'default',
41
+ direction: 'inherit',
42
+ groupBy: 'day',
43
+ locale: 'en-US',
44
+ pageSize: 4,
45
+ showSources: true,
46
+ timeZone: 'UTC',
47
+ },
48
+ 'calendar-panel': {
49
+ direction: 'inherit',
50
+ initialScrollHour: 8,
51
+ locale: 'en-US',
52
+ secondaryCalendar: 'none',
53
+ showAgendaAlternative: false,
54
+ timeZone: 'UTC',
55
+ view: 'week',
56
+ },
57
+ 'chart-panel': {
58
+ chartType: 'line',
59
+ comparison: 'previous-period',
60
+ showTable: false,
61
+ },
62
+ 'issue-table': {
63
+ columns: ['title', 'status', 'priority', 'owner', 'due'],
64
+ density: 'default',
65
+ view: 'active',
66
+ },
67
+ 'metric-grid': {
68
+ comparison: 'previous-period',
69
+ density: 'compact',
70
+ metrics: ['open', 'completed', 'focus'],
71
+ },
72
+ 'todo-list': {
73
+ density: 'default',
74
+ filter: 'all',
75
+ showCompleted: true,
76
+ title: 'New task list',
77
+ },
78
+ }
79
+
80
+ function makeArrayField(label: string, rule: Extract<CeramicPropRule, { type: 'array' }>): Field {
81
+ return {
82
+ type: 'custom',
83
+ label,
84
+ render: ({ id, onChange, value }) => (
85
+ <textarea
86
+ aria-describedby={`${id}-hint`}
87
+ aria-label={label}
88
+ id={id}
89
+ rows={4}
90
+ value={Array.isArray(value) ? value.join('\n') : ''}
91
+ onChange={(event) => {
92
+ const values = event.currentTarget.value
93
+ .split('\n')
94
+ .map((entry) => entry.trim())
95
+ .filter(Boolean)
96
+ if (rule.items.type === 'number') {
97
+ onChange(values.map(Number).filter(Number.isFinite))
98
+ return
99
+ }
100
+ if (rule.items.type === 'boolean') {
101
+ onChange(values.map((entry) => entry === 'true'))
102
+ return
103
+ }
104
+ onChange(values)
105
+ }}
106
+ />
107
+ ),
108
+ } as Field
109
+ }
110
+
111
+ function fieldFromRule(name: string, rule: CeramicPropRule): Field {
112
+ const label = name.replace(/([a-z])([A-Z])/g, '$1 $2')
113
+ if (rule.type === 'string' && rule.enum) {
114
+ return {
115
+ type: 'select',
116
+ label,
117
+ options: rule.enum.map((value) => ({ label: value, value })),
118
+ }
119
+ }
120
+ if (rule.type === 'string') return { type: 'text', label }
121
+ if (rule.type === 'number') {
122
+ return {
123
+ type: 'number',
124
+ label,
125
+ min: rule.minimum,
126
+ max: rule.maximum,
127
+ }
128
+ }
129
+ if (rule.type === 'boolean') {
130
+ return {
131
+ type: 'radio',
132
+ label,
133
+ options: [
134
+ { label: 'Show', value: true },
135
+ { label: 'Hide', value: false },
136
+ ],
137
+ }
138
+ }
139
+ return makeArrayField(label, rule)
140
+ }
141
+
142
+ function registeredProps(
143
+ definition: CeramicBlockDefinition,
144
+ props: Record<string, unknown>,
145
+ ) {
146
+ const allowed = new Set(Object.keys(definition.propsSchema.properties))
147
+ return Object.fromEntries(
148
+ Object.entries(props).filter(([key]) => allowed.has(key)),
149
+ )
150
+ }
151
+
152
+ function blockSpan(
153
+ definition: CeramicBlockDefinition,
154
+ block: CeramicLayoutBlock | undefined,
155
+ columns: number,
156
+ ) {
157
+ if (block && definition.allowedSpans.includes(block.placement.span)) {
158
+ return block.placement.span
159
+ }
160
+ return Math.max(...definition.allowedSpans.filter((span) => span <= columns))
161
+ }
162
+
163
+ function packBlocks(
164
+ blocks: CeramicLayoutBlock[],
165
+ columns: number,
166
+ lockedIds: Set<string>,
167
+ ) {
168
+ let cursor = 1
169
+ return [...blocks]
170
+ .sort((left, right) => left.placement.order - right.placement.order)
171
+ .map((block) => {
172
+ if (lockedIds.has(block.id)) {
173
+ cursor = 1
174
+ return block
175
+ }
176
+ if (cursor + block.placement.span - 1 > columns) cursor = 1
177
+ const column = cursor
178
+ cursor += block.placement.span
179
+ if (cursor > columns) cursor = 1
180
+ return {
181
+ ...block,
182
+ placement: {
183
+ ...block.placement,
184
+ column,
185
+ },
186
+ }
187
+ })
188
+ }
189
+
190
+ export function ceramicLayoutToPuckData(layout: CeramicLayoutDocument): Data {
191
+ return {
192
+ content: [...layout.blocks]
193
+ .sort((left, right) => left.placement.order - right.placement.order)
194
+ .map((block) => ({
195
+ type: block.type,
196
+ props: {
197
+ id: block.id,
198
+ ...block.props,
199
+ },
200
+ })),
201
+ root: {},
202
+ }
203
+ }
204
+
205
+ export function puckDataToCeramicLayout(
206
+ data: Data,
207
+ current: CeramicLayoutDocument,
208
+ ): CeramicPuckConversionResult {
209
+ const currentById = new Map(current.blocks.map((block) => [block.id, block]))
210
+ const lockedIds = new Set([
211
+ ...current.blocks.filter((block) => block.locked).map((block) => block.id),
212
+ ...(current.regions ?? []).flatMap((region) => region.locked ? region.blockIds : []),
213
+ ])
214
+ const lockedOrders = new Set(
215
+ current.blocks
216
+ .filter((block) => lockedIds.has(block.id))
217
+ .map((block) => block.placement.order),
218
+ )
219
+ const issues: string[] = []
220
+ let nextOrder = 1
221
+ const nextBlocks = data.content.flatMap((item) => {
222
+ const definition = getCeramicBlockDefinition(item.type)
223
+ if (!definition || definition.availability !== 'available') {
224
+ issues.push(`Puck returned an unregistered or unavailable block type: ${String(item.type)}.`)
225
+ return []
226
+ }
227
+ const itemProps = item.props as PuckBlockProps
228
+ const previous = currentById.get(itemProps.id)
229
+ const props = registeredProps(definition, itemProps)
230
+ const span = blockSpan(definition, previous, current.columns)
231
+ const locked = lockedIds.has(itemProps.id)
232
+ if (!locked) {
233
+ while (lockedOrders.has(nextOrder)) nextOrder += 1
234
+ }
235
+ const order = locked && previous ? previous.placement.order : nextOrder++
236
+ if (locked) nextOrder = Math.max(nextOrder, order + 1)
237
+ return [{
238
+ id: itemProps.id,
239
+ type: definition.id,
240
+ version: definition.version,
241
+ placement: {
242
+ column: locked && previous ? previous.placement.column : 1,
243
+ span,
244
+ order,
245
+ compactSpan: previous?.placement.compactSpan ?? current.columns,
246
+ },
247
+ props,
248
+ locked: previous?.locked ?? false,
249
+ } satisfies CeramicLayoutBlock]
250
+ })
251
+
252
+ const blockIds = new Set(nextBlocks.map((block) => block.id))
253
+ const next: CeramicLayoutDocument = {
254
+ ...current,
255
+ blocks: packBlocks(nextBlocks, current.columns, lockedIds),
256
+ regions: current.regions?.map((region) => ({
257
+ ...region,
258
+ blockIds: region.blockIds.filter((blockId) => blockIds.has(blockId)),
259
+ })),
260
+ }
261
+ const mutation = validateCeramicAgentMutation(current, next)
262
+ const mutationIssues = mutation.issues.map((issue) => issue.message)
263
+ return {
264
+ layout: issues.length || !mutation.ok ? null : next,
265
+ issues: [...issues, ...mutationIssues],
266
+ ok: issues.length === 0 && mutation.ok,
267
+ }
268
+ }
269
+
270
+ export function createCeramicPuckConfig({
271
+ colorMode = 'light',
272
+ direction = 'ltr',
273
+ layout,
274
+ renderers,
275
+ }: CeramicPuckAdapterOptions): Config {
276
+ const currentById = new Map(layout.blocks.map((block) => [block.id, block]))
277
+ const lockedIds = new Set([
278
+ ...layout.blocks.filter((block) => block.locked).map((block) => block.id),
279
+ ...(layout.regions ?? []).flatMap((region) => region.locked ? region.blockIds : []),
280
+ ])
281
+ const definitions = listCeramicBlockDefinitions({ renderableOnly: true })
282
+ .filter((definition) => renderers[definition.id])
283
+ const components = Object.fromEntries(definitions.map((definition) => {
284
+ const Renderer = renderers[definition.id]
285
+ const fields = Object.fromEntries(
286
+ definition.agentPermissions.editableProps
287
+ .filter((name) => definition.propsSchema.properties[name])
288
+ .map((name) => [name, fieldFromRule(name, definition.propsSchema.properties[name])]),
289
+ )
290
+ const defaults = {
291
+ ...(fallbackDefaults[definition.id] ?? {}),
292
+ ...(layout.blocks.find((block) => block.type === definition.id)?.props ?? {}),
293
+ }
294
+
295
+ return [definition.id, {
296
+ defaultProps: defaults,
297
+ fields,
298
+ inline: true,
299
+ label: definition.title,
300
+ permissions: {
301
+ delete: definition.agentPermissions.remove,
302
+ drag: definition.agentPermissions.move,
303
+ duplicate: definition.agentPermissions.create,
304
+ edit: definition.agentPermissions.editableProps.length > 0,
305
+ insert: definition.agentPermissions.create,
306
+ },
307
+ resolvePermissions: ({ props }: { props: PuckBlockProps }, { permissions }: { permissions: Record<string, boolean> }) => (
308
+ lockedIds.has(props.id)
309
+ ? { ...permissions, delete: false, drag: false, duplicate: false, edit: false }
310
+ : permissions
311
+ ),
312
+ render: (allProps: PuckBlockProps & {
313
+ editMode?: boolean
314
+ puck: {
315
+ dragRef?: React.Ref<HTMLElement>
316
+ }
317
+ }) => {
318
+ const { editMode: _editMode, id, puck, ...rawProps } = allProps
319
+ const previous = currentById.get(id)
320
+ const block: CeramicLayoutBlock = {
321
+ id,
322
+ type: definition.id,
323
+ version: definition.version,
324
+ placement: previous?.placement ?? {
325
+ column: 1,
326
+ span: blockSpan(definition, undefined, layout.columns),
327
+ order: 1,
328
+ compactSpan: layout.columns,
329
+ },
330
+ props: registeredProps(definition, rawProps),
331
+ locked: lockedIds.has(id),
332
+ }
333
+ return (
334
+ <article
335
+ className="ceramic-puck-block-preview"
336
+ data-block-type={definition.id}
337
+ data-locked={block.locked || undefined}
338
+ ref={puck.dragRef}
339
+ style={{
340
+ '--ceramic-puck-span': block.placement.span,
341
+ } as React.CSSProperties}
342
+ >
343
+ <Renderer block={block} direction={direction} />
344
+ </article>
345
+ )
346
+ },
347
+ }]
348
+ }))
349
+
350
+ const categories = Object.fromEntries(
351
+ [...new Set(definitions.map((definition) => definition.category))].map((category) => [
352
+ category,
353
+ {
354
+ components: definitions
355
+ .filter((definition) => definition.category === category)
356
+ .map((definition) => definition.id),
357
+ defaultExpanded: true,
358
+ title: category,
359
+ },
360
+ ]),
361
+ )
362
+
363
+ return {
364
+ categories,
365
+ components,
366
+ root: {
367
+ render: ({ children }: { children: React.ReactNode }) => (
368
+ <main
369
+ aria-label="Ceramic Puck canvas"
370
+ className="ceramic-puck-canvas"
371
+ data-color-mode={colorMode}
372
+ data-theme={layout.theme}
373
+ dir={direction}
374
+ >
375
+ {children}
376
+ </main>
377
+ ),
378
+ },
379
+ } as Config
380
+ }