@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
package/README.md CHANGED
@@ -7,7 +7,13 @@ npm install -D @utopia-studio-design/design-system-cli
7
7
  npx utopia-ds init . --theme utopia-default --yes
8
8
  npx utopia-ds search "Arabic data table" --json
9
9
  npx utopia-ds component DataTable --json
10
+ npx utopia-ds registry --list --json
11
+ npx utopia-ds layout validate ./dashboard-layout.json --json
12
+ npx utopia-ds layout serialize ./dashboard-layout.json
13
+ npx utopia-ds block interaction-workbench --copy ./src/interaction-workbench
10
14
  npx utopia-ds template template-saas-solution-homepage --copy ./saas-website
15
+ npx utopia-ds template template-interaction-lab --copy ./interaction-lab
16
+ npx utopia-ds template template-dashboard-composer --copy ./dashboard-composer
11
17
  npx utopia-ds template template-saas-solution-homepage --theme dextrum --copy ./dextrum-website
12
18
  npx utopia-ds template validate ./my-community-template
13
19
  npx utopia-ds template submit ./my-community-template
@@ -17,3 +23,20 @@ npx utopia-ds mcp
17
23
  ```
18
24
 
19
25
  Programmatic consumers can import `@utopia-studio-design/design-system-cli/api`.
26
+ The Ceramic Composition Registry and versioned Layout JSON are the source of
27
+ truth for agent and Studio dashboard composition. The Dashboard Composer's
28
+ optional Puck adapter consumes this API, but it does not define Ceramic blocks
29
+ or styling.
30
+
31
+ To run the generated Dashboard Composer as a standalone project:
32
+
33
+ ```sh
34
+ cd dashboard-composer
35
+ npm install
36
+ npm run dev
37
+ ```
38
+
39
+ Use `npm run build` before deployment. The generated project pins a compatible
40
+ Ceramic package range and splits the optional Puck editor dependencies into
41
+ separate production chunks; the validated Layout JSON and registry renderer
42
+ remain usable without opening the visual editor.
@@ -1,13 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- capabilityManifest, getComponent, getDoc, getMotionProfile, getTemplate, getTheme,
4
- listComponents, listDocs, listMotionProfiles, listTemplates, listThemes, repositoryDoctor, search,
3
+ capabilityManifest, getBlock, getBlockDefinition, getComponent, getDoc, getMotionProfile, getTemplate, getTheme,
4
+ listBlockDefinitions, listBlocks, listComponents, listDocs, listMotionProfiles, listTemplates, listThemes,
5
+ repositoryDoctor, search, validateAgentMutation, validateLayout,
5
6
  } from '../lib/api.mjs'
6
7
 
7
8
  const tools = [
8
9
  ['search', 'Search components, docs, templates, themes, and motion profiles.', { query: { type: 'string' } }, ({ query }) => search(query)],
9
10
  ['list_components', 'List every Ceramic component contract.', {}, () => listComponents()],
10
11
  ['get_component', 'Get one component including imports, tokens, usage, and Arabic rules.', { name: { type: 'string' } }, ({ name }) => getComponent(name)],
12
+ ['list_block_definitions', 'List Ceramic registry definitions for agent and editor composition.', {}, () => listBlockDefinitions()],
13
+ ['get_block_definition', 'Get one Ceramic block definition including props, layout, states, and agent permissions.', { id: { type: 'string' } }, ({ id }) => getBlockDefinition(id)],
14
+ ['validate_layout', 'Validate a versioned Ceramic Layout JSON document without coercion.', { document: { type: 'object' } }, ({ document }) => validateLayout(document)],
15
+ ['validate_agent_layout_mutation', 'Validate an agent-authored layout change against locks and permissions.', { current: { type: 'object' }, next: { type: 'object' } }, ({ current, next }) => validateAgentMutation(current, next)],
16
+ ['list_blocks', 'List copyable Ceramic interaction blocks.', {}, () => listBlocks()],
17
+ ['get_block', 'Get a block contract and copyable bundle metadata.', { id: { type: 'string' } }, ({ id }) => getBlock(id)],
11
18
  ['list_templates', 'List Ceramic starter templates.', {}, () => listTemplates()],
12
19
  ['get_template', 'Get a template contract and sections.', { id: { type: 'string' } }, ({ id }) => getTemplate(id)],
13
20
  ['list_themes', 'List themes that implement the semantic contract.', {}, () => listThemes()],
package/bin/utopia-ds.mjs CHANGED
@@ -3,10 +3,12 @@ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, wri
3
3
  import { dirname, join, resolve } from 'node:path'
4
4
  import { spawn } from 'node:child_process'
5
5
  import {
6
- capabilityManifest, envelope, getComponent, getDoc, getMotionProfile, getTemplate, getTheme,
7
- listComponents, listDocs, listMotionProfiles, listTemplates, listThemes, mcpLaunch, repositoryDoctor, search,
6
+ capabilityManifest, envelope, getBlock, getBlockDefinition, getComponent, getDoc, getMotionProfile, getTemplate, getTheme,
7
+ listBlockDefinitions, listBlocks, listComponents, listDocs, listMotionProfiles, listTemplates, listThemes, mcpLaunch,
8
+ repositoryDoctor, search, serializeLayout, validateLayout,
8
9
  } from '../lib/api.mjs'
9
10
  import { createTemplateSubmissionUrl, validateTemplateSubmission } from '../lib/template-submission.mjs'
11
+ import { installCliTelemetry } from '../lib/telemetry.mjs'
10
12
 
11
13
  const args = process.argv.slice(2)
12
14
  const command = args.find((arg) => !arg.startsWith('--')) ?? 'help'
@@ -15,6 +17,16 @@ const optionsWithValues = new Set(['--copy', '--theme'])
15
17
  const values = commandIndex >= 0 ? positionalValues(args.slice(commandIndex + 1)) : []
16
18
  const json = args.includes('--json')
17
19
  const dense = args.includes('--dense')
20
+ const cliPackageMetadata = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
21
+ const cliVersion = cliPackageMetadata.version
22
+ const designSystemVersion = cliPackageMetadata.ceramic.designSystem
23
+ let telemetryErrorCode
24
+
25
+ installCliTelemetry({
26
+ command,
27
+ packageVersion: cliVersion,
28
+ getErrorCode: () => telemetryErrorCode,
29
+ })
18
30
 
19
31
  function positionalValues(input) {
20
32
  const positionals = []
@@ -36,6 +48,7 @@ function output(type, data, format = null) {
36
48
  }
37
49
 
38
50
  function fail(message, code = 'ERR_INPUT', suggestions = []) {
51
+ telemetryErrorCode = code
39
52
  if (json) console.error(JSON.stringify({ apiVersion: 1, error: message, code, suggestions }, null, 2))
40
53
  else console.error(message)
41
54
  process.exitCode = 1
@@ -61,6 +74,10 @@ Commands:
61
74
  init [directory] [--theme <id>] [--yes] Add Ceramic and agent instructions
62
75
  search <query> Search every design-system domain
63
76
  component <Name>|--list Inspect component contracts
77
+ registry <block-id>|--list Inspect agent-editable block definitions
78
+ layout validate <file> Validate versioned Ceramic Layout JSON
79
+ layout serialize <file> Print canonical Ceramic Layout JSON
80
+ block <id>|--list [--copy <directory>] Inspect or copy a composed interaction block
64
81
  template <id>|--list [--skeleton] Inspect starter structures
65
82
  template <id> --copy <directory> [--theme <id>]
66
83
  Generate a themed runnable project
@@ -237,10 +254,10 @@ function copyTemplateProject(entry) {
237
254
  const workspaceCli = resolve(process.cwd(), 'packages/design-system-cli')
238
255
  const designSystemDependency = existsSync(join(workspacePackage, 'package.json'))
239
256
  ? `file:${workspacePackage}`
240
- : '^0.3.0'
257
+ : designSystemVersion
241
258
  const cliDependency = existsSync(join(workspaceCli, 'package.json'))
242
259
  ? `file:${workspaceCli}`
243
- : '^0.3.0'
260
+ : `^${cliVersion}`
244
261
  write(join(target, 'package.json'), `${JSON.stringify({
245
262
  name: packageName,
246
263
  private: true,
@@ -253,16 +270,62 @@ function copyTemplateProject(entry) {
253
270
  'lucide-react': '^1.23.0',
254
271
  react: '^19.0.0',
255
272
  'react-dom': '^19.0.0',
273
+ ...(entry.packageDependencies ?? {}),
256
274
  },
257
275
  devDependencies: { '@utopia-studio-design/design-system-cli': cliDependency, '@vitejs/plugin-react': '^4.3.4', typescript: '^5.7.2', vite: '^6.0.7' },
258
276
  }, null, 2)}\n`)
259
277
  write(join(target, '.ceramic/config.json'), `${JSON.stringify({ apiVersion: 1, theme, motionProfile: getTheme(theme)?.motionProfile, motionAdapter: 'framer-motion', arabicFriendly: true, source: '@utopia-studio-design/design-system' }, null, 2)}\n`)
260
- write(join(target, 'vite.config.ts'), `import { resolve } from 'node:path'\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n plugins: [react()],\n build: {\n rollupOptions: {\n input: {\n home: resolve('index.html'),\n product: resolve('product/index.html'),\n agents: resolve('agents/index.html'),\n integrations: resolve('integrations/index.html'),\n integrationDetail: resolve('integrations/slack/index.html'),\n customers: resolve('customers/index.html'),\n customerStory: resolve('customers/aster-labs/index.html'),\n pricing: resolve('pricing/index.html'),\n changelog: resolve('changelog/index.html'),\n contactSales: resolve('contact-sales/index.html'),\n },\n },\n },\n})\n`)
278
+ const htmlEntries = []
279
+ const collectHtml = (directory, prefix = '') => {
280
+ for (const name of readdirSync(directory)) {
281
+ const path = join(directory, name)
282
+ const relative = prefix ? `${prefix}/${name}` : name
283
+ if (statSync(path).isDirectory()) collectHtml(path, relative)
284
+ else if (name.endsWith('.html')) htmlEntries.push(relative)
285
+ }
286
+ }
287
+ collectHtml(target)
288
+ const viteInputs = htmlEntries.map((path, index) => ` page${index + 1}: resolve('${path}'),`).join('\n')
289
+ write(join(target, 'vite.config.ts'), `import { resolve } from 'node:path'\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n plugins: [react()],\n build: {\n rollupOptions: {\n input: {\n${viteInputs}\n },\n output: {\n manualChunks(id) {\n if (id.includes('node_modules/@dnd-kit/')) return 'puck-dnd'\n if (id.includes('node_modules/@tanstack/react-virtual')) return 'puck-virtual'\n if (id.includes('node_modules/@radix-ui/')) return 'radix'\n if (\n id.includes('node_modules/clsx')\n || id.includes('node_modules/class-variance-authority')\n || id.includes('node_modules/tailwind-merge')\n || id.includes('node_modules/tiny-invariant')\n ) return 'ui-utils'\n if (id.includes('node_modules/react-dom') || id.includes('node_modules/react/')) return 'react'\n return undefined\n },\n },\n },\n },\n})\n`)
261
290
  const existingReadme = readFileSync(join(target, 'README.md'), 'utf8')
262
- write(join(target, 'README.md'), `# Generated Ceramic SaaS website\n\nActive theme: \`${theme}\`\n\n\`\`\`sh\nnpm install\nnpm run dev\n\`\`\`\n\nOpen \`http://localhost:5173/?seed=1974341818\`. All ten page entries share the same seed, theme, and locale state.\n\n${existingReadme}`)
291
+ write(join(target, 'README.md'), `# Generated ${entry.title}\n\nActive theme: \`${theme}\`\n\n\`\`\`sh\nnpm install\nnpm run dev\n\`\`\`\n\nOpen \`http://localhost:5173/\`.\n\n${existingReadme}`)
263
292
  output('template-copy-result', { ok: true, id: entry.id, target, theme, pages: entry.pages ?? [] }, (data) => `Generated ${data.id} with theme ${data.theme} in ${data.target}.\nNext: cd ${data.target} && npm install && npm run dev`)
264
293
  }
265
294
 
295
+ function copyBlock(entry) {
296
+ const requestedTarget = argAfter('--copy', entry.id)
297
+ if (!requestedTarget) return
298
+ const target = resolve(requestedTarget)
299
+ if (existsSync(target) && !args.includes('--force')) return fail(`Target already exists: ${target}. Pass --force to overwrite it.`, 'ERR_TARGET_EXISTS')
300
+ const source = resolve(dirname(new URL(import.meta.url).pathname), '..', 'data', entry.bundlePath)
301
+ if (!existsSync(source)) return fail(`Block bundle is missing for "${entry.id}".`, 'ERR_BLOCK_BUNDLE')
302
+ cpSync(source, target, { recursive: true, force: args.includes('--force') })
303
+ output('block-copy-result', { ok: true, id: entry.id, target }, (data) => `Copied ${data.id} to ${data.target}.`)
304
+ }
305
+
306
+ function readLayoutInput() {
307
+ const file = values[1]
308
+ if (!file) {
309
+ fail('Layout command requires a JSON file path.', 'ERR_LAYOUT_FILE')
310
+ return null
311
+ }
312
+ const path = resolve(file)
313
+ if (!existsSync(path)) {
314
+ fail(`Layout file not found: ${path}.`, 'ERR_LAYOUT_FILE')
315
+ return null
316
+ }
317
+ try {
318
+ return { path, document: JSON.parse(readFileSync(path, 'utf8')) }
319
+ } catch (cause) {
320
+ fail(
321
+ `Layout file is not valid JSON: ${path}.`,
322
+ 'ERR_LAYOUT_JSON',
323
+ [cause instanceof Error ? cause.message : 'Unknown JSON parse error'],
324
+ )
325
+ return null
326
+ }
327
+ }
328
+
266
329
  function validateCommunityTemplate({ submit = false } = {}) {
267
330
  const target = resolve(values[1] ?? process.cwd())
268
331
  const result = validateTemplateSubmission(target)
@@ -301,6 +364,51 @@ else if (command === 'search') {
301
364
  const name = values[0]
302
365
  if (!name || args.includes('--list')) output('component-list', listComponents(), (rows) => rows.map((item) => `${item.name}|${item.status}|${item.category}|${item.packageImport}`).join(dense ? '\n' : '\n- '))
303
366
  else { const item = getComponent(name); item ? output('component', item, formatComponent) : fail(`Unknown component "${name}".`, 'ERR_COMPONENT', search(name).slice(0, 5).map((result) => result.id)) }
367
+ } else if (command === 'registry') {
368
+ const name = values[0]
369
+ if (!name || args.includes('--list')) {
370
+ const definitions = listBlockDefinitions({ renderableOnly: args.includes('--renderable') })
371
+ output('block-definition-list', definitions, (rows) =>
372
+ rows.map((item) => `${item.id}|${item.availability}|${item.category}|${item.title}`).join('\n'))
373
+ } else {
374
+ const item = getBlockDefinition(name)
375
+ item ? output('block-definition', item) : fail(`Unknown block definition "${name}".`, 'ERR_BLOCK_DEFINITION')
376
+ }
377
+ } else if (command === 'layout') {
378
+ const action = values[0]
379
+ if (!['validate', 'serialize'].includes(action)) {
380
+ fail('Layout requires "validate" or "serialize".', 'ERR_LAYOUT_ACTION')
381
+ } else {
382
+ const input = readLayoutInput()
383
+ if (input && action === 'validate') {
384
+ const result = validateLayout(input.document)
385
+ output('layout-validation-result', { ...result, path: input.path })
386
+ if (!result.ok) process.exitCode = 1
387
+ } else if (input) {
388
+ try {
389
+ const serialized = serializeLayout(input.document)
390
+ if (json) output('layout-serialization-result', { ok: true, path: input.path, serialized })
391
+ else process.stdout.write(serialized)
392
+ } catch (cause) {
393
+ fail(
394
+ 'Ceramic layout validation failed.',
395
+ 'ERR_LAYOUT_INVALID',
396
+ cause instanceof Error && Array.isArray(cause.issues)
397
+ ? cause.issues.map((issue) => `${issue.code}: ${issue.path} ${issue.message}`)
398
+ : [],
399
+ )
400
+ }
401
+ }
402
+ }
403
+ } else if (command === 'block') {
404
+ const name = values[0]
405
+ if (!name || args.includes('--list')) output('block-list', listBlocks(), (rows) => rows.map((item) => `${item.id}|${item.category}|${item.title}|${item.purpose}`).join('\n'))
406
+ else {
407
+ const item = getBlock(name)
408
+ if (!item) fail(`Unknown block "${name}".`, 'ERR_BLOCK')
409
+ else if (args.includes('--copy')) copyBlock(item)
410
+ else output('block', item)
411
+ }
304
412
  } else if (command === 'template') {
305
413
  const name = values[0]
306
414
  if (name === 'validate') validateCommunityTemplate()
@@ -326,6 +434,6 @@ else if (command === 'search') {
326
434
  if (!topic || args.includes('--list')) output('docs-list', listDocs(), (rows) => rows.join('\n'))
327
435
  else { const item = getDoc(topic); item ? output('docs', item, (doc) => dense ? doc.content.replace(/\n{2,}/g, '\n').trim() : doc.content) : fail(`Unknown docs topic "${topic}".`, 'ERR_DOCS', listDocs()) }
328
436
  } else if (command === 'doctor') {
329
- const result = repositoryDoctor(); output('doctor-result', result, (data) => data.ok ? `Ceramic doctor passed. ${data.checks.components} components, ${data.checks.templates} templates, ${data.checks.themes} themes, ${data.checks.motionProfiles} motion profiles, MCP ready.` : `Ceramic doctor failed: ${data.missing.join(', ')}`); if (!result.ok) process.exitCode = 1
437
+ const result = repositoryDoctor(); output('doctor-result', result, (data) => data.ok ? `Ceramic doctor passed. ${data.checks.components} components, ${data.checks.blocks} blocks, ${data.checks.templates} templates, ${data.checks.themes} themes, ${data.checks.motionProfiles} motion profiles, MCP ready.` : `Ceramic doctor failed: ${data.missing.join(', ')}`); if (!result.ok) process.exitCode = 1
330
438
  } else if (command === 'mcp') runMcp()
331
439
  else fail(`Unknown command "${command}".`, 'ERR_COMMAND', capabilityManifest().commands.map((item) => item.name))
@@ -0,0 +1,255 @@
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 { StatusDot } from '@utopia-studio-design/design-system/DataDisplay'
5
+ import './activity-feed.css'
6
+
7
+ export type ActivityFeedDensity = 'compact' | 'default' | 'comfortable'
8
+ export type ActivityFeedGrouping = 'none' | 'day' | 'source'
9
+ export type ActivityFeedState = 'loading' | 'empty' | 'error' | 'ready'
10
+ export type ActivityFeedTone = 'neutral' | 'info' | 'positive' | 'warning' | 'critical'
11
+
12
+ export interface ActivityFeedItem {
13
+ id: string
14
+ label: string
15
+ source?: string
16
+ timestamp: string
17
+ tone?: ActivityFeedTone
18
+ }
19
+
20
+ export interface ActivityFeedLabels {
21
+ end: string
22
+ emptyDescription: string
23
+ emptyTitle: string
24
+ errorDescription: string
25
+ errorTitle: string
26
+ eyebrow: string
27
+ invalidTime: string
28
+ loadMore: string
29
+ loading: string
30
+ otherSource: string
31
+ retry: string
32
+ title: string
33
+ }
34
+
35
+ export interface ActivityFeedBlockProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
36
+ density?: ActivityFeedDensity
37
+ direction?: 'ltr' | 'rtl'
38
+ groupBy?: ActivityFeedGrouping
39
+ items?: ActivityFeedItem[]
40
+ labels: ActivityFeedLabels
41
+ locale?: string
42
+ onRetry?: () => void
43
+ pageSize?: number
44
+ showSources?: boolean
45
+ state?: ActivityFeedState
46
+ timeZone?: string
47
+ title?: string
48
+ }
49
+
50
+ interface ActivityGroup {
51
+ id: string
52
+ label?: string
53
+ items: ActivityFeedItem[]
54
+ }
55
+
56
+ function safeDate(timestamp: string) {
57
+ const date = new Date(timestamp)
58
+ return Number.isNaN(date.valueOf()) ? null : date
59
+ }
60
+
61
+ function makeFormatter(locale: string, timeZone: string, options: Intl.DateTimeFormatOptions) {
62
+ try {
63
+ return new Intl.DateTimeFormat(locale, { ...options, timeZone })
64
+ } catch {
65
+ return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' })
66
+ }
67
+ }
68
+
69
+ function formatNumber(locale: string, value: number) {
70
+ try {
71
+ return new Intl.NumberFormat(locale).format(value)
72
+ } catch {
73
+ return String(value)
74
+ }
75
+ }
76
+
77
+ function groupItems(
78
+ items: ActivityFeedItem[],
79
+ groupBy: ActivityFeedGrouping,
80
+ locale: string,
81
+ timeZone: string,
82
+ otherSource: string,
83
+ ): ActivityGroup[] {
84
+ if (groupBy === 'none') return [{ id: 'all', items }]
85
+
86
+ const dayFormatter = makeFormatter(locale, timeZone, {
87
+ dateStyle: 'full',
88
+ })
89
+ const groups = new Map<string, ActivityGroup>()
90
+ items.forEach((item) => {
91
+ const date = safeDate(item.timestamp)
92
+ const label = groupBy === 'source'
93
+ ? item.source || otherSource
94
+ : date ? dayFormatter.format(date) : otherSource
95
+ const id = `${groupBy}:${label}`
96
+ const group = groups.get(id) ?? { id, label, items: [] }
97
+ group.items.push(item)
98
+ groups.set(id, group)
99
+ })
100
+ return [...groups.values()]
101
+ }
102
+
103
+ function ActivityItems({
104
+ items,
105
+ labels,
106
+ locale,
107
+ showSources,
108
+ timeZone,
109
+ }: {
110
+ items: ActivityFeedItem[]
111
+ labels: ActivityFeedLabels
112
+ locale: string
113
+ showSources: boolean
114
+ timeZone: string
115
+ }) {
116
+ const dateTimeFormatter = makeFormatter(locale, timeZone, {
117
+ dateStyle: 'medium',
118
+ timeStyle: 'short',
119
+ })
120
+ const timeFormatter = makeFormatter(locale, timeZone, {
121
+ hour: 'numeric',
122
+ minute: '2-digit',
123
+ })
124
+
125
+ return (
126
+ <ol className="ceramic-activity-items">
127
+ {items.map((item) => {
128
+ const date = safeDate(item.timestamp)
129
+ const accessibleTime = date ? dateTimeFormatter.format(date) : labels.invalidTime
130
+ const visibleTime = date ? timeFormatter.format(date) : labels.invalidTime
131
+ return (
132
+ <li key={item.id}>
133
+ <StatusDot
134
+ aria-hidden="true"
135
+ className="ceramic-activity-marker"
136
+ data-tone={item.tone ?? 'neutral'}
137
+ />
138
+ <div className="ceramic-activity-copy">
139
+ <p>{item.label}</p>
140
+ <div>
141
+ <time dateTime={date ? date.toISOString() : undefined} title={accessibleTime}>
142
+ {visibleTime}
143
+ </time>
144
+ {showSources && item.source ? <Badge variant="outline">{item.source}</Badge> : null}
145
+ </div>
146
+ </div>
147
+ </li>
148
+ )
149
+ })}
150
+ </ol>
151
+ )
152
+ }
153
+
154
+ export function ActivityFeedBlock({
155
+ density = 'default',
156
+ direction,
157
+ groupBy = 'day',
158
+ items = [],
159
+ labels,
160
+ locale = 'en-US',
161
+ onRetry,
162
+ pageSize = 4,
163
+ showSources = true,
164
+ state: stateProp,
165
+ timeZone = 'UTC',
166
+ title,
167
+ ...props
168
+ }: ActivityFeedBlockProps) {
169
+ const titleId = React.useId()
170
+ const [visibleCount, setVisibleCount] = React.useState(pageSize)
171
+ const state = stateProp ?? (items.length ? 'ready' : 'empty')
172
+ const visibleItems = items.slice(0, visibleCount)
173
+ const groups = groupItems(visibleItems, groupBy, locale, timeZone, labels.otherSource)
174
+ const hasMore = visibleCount < items.length
175
+ const visibleCountLabel = formatNumber(locale, visibleItems.length)
176
+ const totalCountLabel = formatNumber(locale, items.length)
177
+
178
+ React.useEffect(() => {
179
+ setVisibleCount(pageSize)
180
+ }, [items.length, pageSize])
181
+
182
+ return (
183
+ <section
184
+ aria-busy={state === 'loading'}
185
+ aria-labelledby={titleId}
186
+ className="ceramic-activity-feed"
187
+ data-density={density}
188
+ data-state={state}
189
+ dir={direction}
190
+ lang={locale}
191
+ {...props}
192
+ >
193
+ <header>
194
+ <div>
195
+ <span>{labels.eyebrow}</span>
196
+ <h2 id={titleId}>{title ?? labels.title}</h2>
197
+ </div>
198
+ {state === 'ready' ? <Badge variant="outline">{visibleCountLabel}/{totalCountLabel}</Badge> : null}
199
+ </header>
200
+
201
+ {state === 'loading' ? (
202
+ <div className="ceramic-activity-state" role="status">{labels.loading}</div>
203
+ ) : null}
204
+ {state === 'error' ? (
205
+ <div className="ceramic-activity-state" role="alert">
206
+ <strong>{labels.errorTitle}</strong>
207
+ <span>{labels.errorDescription}</span>
208
+ {onRetry ? <Button size="sm" variant="outline" onClick={onRetry}>{labels.retry}</Button> : null}
209
+ </div>
210
+ ) : null}
211
+ {state === 'empty' ? (
212
+ <div className="ceramic-activity-state" role="status">
213
+ <strong>{labels.emptyTitle}</strong>
214
+ <span>{labels.emptyDescription}</span>
215
+ </div>
216
+ ) : null}
217
+ {state === 'ready' ? (
218
+ <div className="ceramic-activity-groups">
219
+ {groups.map((group, groupIndex) => {
220
+ const groupTitleId = `${titleId}-group-${groupIndex + 1}`
221
+ return (
222
+ <section aria-labelledby={group.label ? groupTitleId : undefined} key={group.id}>
223
+ {group.label ? <h3 id={groupTitleId}>{group.label}</h3> : null}
224
+ <ActivityItems
225
+ items={group.items}
226
+ labels={labels}
227
+ locale={locale}
228
+ showSources={showSources}
229
+ timeZone={timeZone}
230
+ />
231
+ </section>
232
+ )
233
+ })}
234
+ </div>
235
+ ) : null}
236
+
237
+ {state === 'ready' ? (
238
+ <footer>
239
+ {hasMore ? (
240
+ <Button
241
+ size="sm"
242
+ variant="outline"
243
+ onClick={() => setVisibleCount((count) => Math.min(items.length, count + pageSize))}
244
+ >
245
+ {labels.loadMore}
246
+ </Button>
247
+ ) : <span>{labels.end}</span>}
248
+ </footer>
249
+ ) : null}
250
+ <output aria-live="polite" className="uds-visually-hidden">
251
+ {state === 'ready' ? `${visibleCountLabel} / ${totalCountLabel}` : ''}
252
+ </output>
253
+ </section>
254
+ )
255
+ }
@@ -0,0 +1,16 @@
1
+ # Activity Feed block
2
+
3
+ `ActivityFeedBlock` is a Ceramic composition block for chronological,
4
+ actor-independent product activity. The product owns activity data and retry
5
+ behavior; the Composition Registry owns density, grouping, locale, direction,
6
+ timezone, source visibility, and page size.
7
+
8
+ Use `resolveActivityFeedConfiguration` in every renderer. `direction="auto"`
9
+ derives text direction from the locale while `direction="inherit"` follows the
10
+ dashboard preview. Activity timestamps are formatted with the registered locale
11
+ and IANA timezone. Raw colors, spacing, classes, and tokens are not part of the
12
+ Layout JSON contract.
13
+
14
+ The block supports `loading`, `empty`, `error`, and `ready` states. Local fixture
15
+ pagination reveals `pageSize` items at a time; production adapters may replace
16
+ the supplied `items` when a backend page is fetched.
@@ -0,0 +1,96 @@
1
+ import { getLocaleTextDirection } from '@utopia-studio-design/design-system/Utilities'
2
+ import type {
3
+ ActivityFeedBlockProps,
4
+ ActivityFeedLabels,
5
+ } from './ActivityFeedBlock'
6
+
7
+ export const activityFeedLocaleOptions = ['en-US', 'ar-SA', 'ar-EG', 'ar-AE'] as const
8
+
9
+ export type ActivityFeedLocale = typeof activityFeedLocaleOptions[number]
10
+ export type ActivityFeedDirection = 'inherit' | 'auto' | 'ltr' | 'rtl'
11
+
12
+ export interface ActivityFeedConfiguration {
13
+ density: NonNullable<ActivityFeedBlockProps['density']>
14
+ direction?: 'ltr' | 'rtl'
15
+ groupBy: NonNullable<ActivityFeedBlockProps['groupBy']>
16
+ labels: ActivityFeedLabels
17
+ locale: ActivityFeedLocale
18
+ pageSize: number
19
+ showSources: boolean
20
+ timeZone: string
21
+ }
22
+
23
+ export interface ActivityFeedContext {
24
+ direction: 'ltr' | 'rtl'
25
+ }
26
+
27
+ const directionModes: ActivityFeedDirection[] = ['inherit', 'auto', 'ltr', 'rtl']
28
+ const densities: ActivityFeedConfiguration['density'][] = ['compact', 'default', 'comfortable']
29
+ const groupings: ActivityFeedConfiguration['groupBy'][] = ['none', 'day', 'source']
30
+
31
+ const englishLabels: ActivityFeedLabels = {
32
+ end: 'You have reached the end of the activity.',
33
+ emptyDescription: 'New updates will appear here when work changes.',
34
+ emptyTitle: 'No activity yet',
35
+ errorDescription: 'Reconnect the activity source and try again.',
36
+ errorTitle: 'Activity unavailable',
37
+ eyebrow: 'Activity',
38
+ invalidTime: 'Time unavailable',
39
+ loadMore: 'Load more',
40
+ loading: 'Loading activity…',
41
+ otherSource: 'Other',
42
+ retry: 'Try again',
43
+ title: 'Recent activity',
44
+ }
45
+
46
+ const arabicLabels: ActivityFeedLabels = {
47
+ end: 'وصلت إلى نهاية سجل النشاط.',
48
+ emptyDescription: 'ستظهر التحديثات الجديدة هنا عند تغيّر العمل.',
49
+ emptyTitle: 'لا يوجد نشاط بعد',
50
+ errorDescription: 'أعد الاتصال بمصدر النشاط ثم حاول مرة أخرى.',
51
+ errorTitle: 'النشاط غير متاح',
52
+ eyebrow: 'النشاط',
53
+ invalidTime: 'الوقت غير متاح',
54
+ loadMore: 'تحميل المزيد',
55
+ loading: 'جارٍ تحميل النشاط…',
56
+ otherSource: 'أخرى',
57
+ retry: 'حاول مرة أخرى',
58
+ title: 'النشاط الأخير',
59
+ }
60
+
61
+ export function getActivityFeedLabels(locale: string): ActivityFeedLabels {
62
+ return getLocaleTextDirection(locale) === 'rtl' ? arabicLabels : englishLabels
63
+ }
64
+
65
+ export function resolveActivityFeedConfiguration(
66
+ props: Record<string, unknown>,
67
+ context: ActivityFeedContext,
68
+ ): ActivityFeedConfiguration {
69
+ const locale = activityFeedLocaleOptions.includes(props.locale as ActivityFeedLocale)
70
+ ? props.locale as ActivityFeedLocale
71
+ : 'en-US'
72
+ const directionMode = directionModes.includes(props.direction as ActivityFeedDirection)
73
+ ? props.direction as ActivityFeedDirection
74
+ : 'inherit'
75
+ const direction = directionMode === 'inherit'
76
+ ? context.direction
77
+ : directionMode === 'auto' ? getLocaleTextDirection(locale) : directionMode
78
+ const pageSize = Number.isInteger(props.pageSize)
79
+ ? Math.min(20, Math.max(2, Number(props.pageSize)))
80
+ : 4
81
+
82
+ return {
83
+ density: densities.includes(props.density as ActivityFeedConfiguration['density'])
84
+ ? props.density as ActivityFeedConfiguration['density']
85
+ : 'default',
86
+ direction,
87
+ groupBy: groupings.includes(props.groupBy as ActivityFeedConfiguration['groupBy'])
88
+ ? props.groupBy as ActivityFeedConfiguration['groupBy']
89
+ : 'day',
90
+ labels: getActivityFeedLabels(locale),
91
+ locale,
92
+ pageSize,
93
+ showSources: props.showSources !== false,
94
+ timeZone: typeof props.timeZone === 'string' && props.timeZone ? props.timeZone : 'UTC',
95
+ }
96
+ }
@@ -0,0 +1,22 @@
1
+ import type { ActivityFeedItem } from './ActivityFeedBlock'
2
+
3
+ const activityItems: Record<'en' | 'ar', ActivityFeedItem[]> = {
4
+ en: [
5
+ { id: 'activity-1', label: 'Calendar block moved after the task list', source: 'Studio', timestamp: '2026-03-09T00:20:00.000Z', tone: 'info' },
6
+ { id: 'activity-2', label: 'Release QA completed without blockers', source: 'Mina', timestamp: '2026-03-09T01:10:00.000Z', tone: 'positive' },
7
+ { id: 'activity-3', label: 'Layout JSON published to this workspace', source: 'Agent', timestamp: '2026-03-09T02:30:00.000Z', tone: 'positive' },
8
+ { id: 'activity-4', label: 'Design-system package data synchronized', source: 'CLI', timestamp: '2026-03-08T06:40:00.000Z', tone: 'neutral' },
9
+ { id: 'activity-5', label: 'Arabic calendar review requested', source: 'Studio', timestamp: '2026-03-08T08:15:00.000Z', tone: 'warning' },
10
+ ],
11
+ ar: [
12
+ { id: 'activity-1', label: 'نُقلت كتلة التقويم بعد قائمة المهام', source: 'الاستوديو', timestamp: '2026-03-09T00:20:00.000Z', tone: 'info' },
13
+ { id: 'activity-2', label: 'اكتمل فحص الإصدار من دون عوائق', source: 'مينا', timestamp: '2026-03-09T01:10:00.000Z', tone: 'positive' },
14
+ { id: 'activity-3', label: 'نُشر ملف تخطيط JSON في مساحة العمل', source: 'الوكيل', timestamp: '2026-03-09T02:30:00.000Z', tone: 'positive' },
15
+ { id: 'activity-4', label: 'تمت مزامنة بيانات حزمة نظام التصميم', source: 'CLI', timestamp: '2026-03-08T06:40:00.000Z', tone: 'neutral' },
16
+ { id: 'activity-5', label: 'طُلبت مراجعة التقويم العربي', source: 'الاستوديو', timestamp: '2026-03-08T08:15:00.000Z', tone: 'warning' },
17
+ ],
18
+ }
19
+
20
+ export function getActivityFeedFixture(locale: string) {
21
+ return activityItems[locale.startsWith('ar') ? 'ar' : 'en']
22
+ }