@utopia-studio-design/design-system-cli 0.4.1 → 0.6.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/theme-vyapti.json +125 -0
- package/data/manifests/themes.json +400 -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
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.
|
package/bin/utopia-ds-mcp.mjs
CHANGED
|
@@ -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,
|
|
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,8 +3,9 @@ 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,
|
|
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'
|
|
10
11
|
import { installCliTelemetry } from '../lib/telemetry.mjs'
|
|
@@ -16,7 +17,9 @@ const optionsWithValues = new Set(['--copy', '--theme'])
|
|
|
16
17
|
const values = commandIndex >= 0 ? positionalValues(args.slice(commandIndex + 1)) : []
|
|
17
18
|
const json = args.includes('--json')
|
|
18
19
|
const dense = args.includes('--dense')
|
|
19
|
-
const
|
|
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
|
|
20
23
|
let telemetryErrorCode
|
|
21
24
|
|
|
22
25
|
installCliTelemetry({
|
|
@@ -71,6 +74,10 @@ Commands:
|
|
|
71
74
|
init [directory] [--theme <id>] [--yes] Add Ceramic and agent instructions
|
|
72
75
|
search <query> Search every design-system domain
|
|
73
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
|
|
74
81
|
template <id>|--list [--skeleton] Inspect starter structures
|
|
75
82
|
template <id> --copy <directory> [--theme <id>]
|
|
76
83
|
Generate a themed runnable project
|
|
@@ -247,10 +254,10 @@ function copyTemplateProject(entry) {
|
|
|
247
254
|
const workspaceCli = resolve(process.cwd(), 'packages/design-system-cli')
|
|
248
255
|
const designSystemDependency = existsSync(join(workspacePackage, 'package.json'))
|
|
249
256
|
? `file:${workspacePackage}`
|
|
250
|
-
:
|
|
257
|
+
: designSystemVersion
|
|
251
258
|
const cliDependency = existsSync(join(workspaceCli, 'package.json'))
|
|
252
259
|
? `file:${workspaceCli}`
|
|
253
|
-
:
|
|
260
|
+
: `^${cliVersion}`
|
|
254
261
|
write(join(target, 'package.json'), `${JSON.stringify({
|
|
255
262
|
name: packageName,
|
|
256
263
|
private: true,
|
|
@@ -263,16 +270,62 @@ function copyTemplateProject(entry) {
|
|
|
263
270
|
'lucide-react': '^1.23.0',
|
|
264
271
|
react: '^19.0.0',
|
|
265
272
|
'react-dom': '^19.0.0',
|
|
273
|
+
...(entry.packageDependencies ?? {}),
|
|
266
274
|
},
|
|
267
275
|
devDependencies: { '@utopia-studio-design/design-system-cli': cliDependency, '@vitejs/plugin-react': '^4.3.4', typescript: '^5.7.2', vite: '^6.0.7' },
|
|
268
276
|
}, null, 2)}\n`)
|
|
269
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`)
|
|
270
|
-
|
|
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`)
|
|
271
290
|
const existingReadme = readFileSync(join(target, 'README.md'), 'utf8')
|
|
272
|
-
write(join(target, 'README.md'), `# Generated
|
|
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}`)
|
|
273
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`)
|
|
274
293
|
}
|
|
275
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
|
+
|
|
276
329
|
function validateCommunityTemplate({ submit = false } = {}) {
|
|
277
330
|
const target = resolve(values[1] ?? process.cwd())
|
|
278
331
|
const result = validateTemplateSubmission(target)
|
|
@@ -311,6 +364,51 @@ else if (command === 'search') {
|
|
|
311
364
|
const name = values[0]
|
|
312
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- '))
|
|
313
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
|
+
}
|
|
314
412
|
} else if (command === 'template') {
|
|
315
413
|
const name = values[0]
|
|
316
414
|
if (name === 'validate') validateCommunityTemplate()
|
|
@@ -336,6 +434,6 @@ else if (command === 'search') {
|
|
|
336
434
|
if (!topic || args.includes('--list')) output('docs-list', listDocs(), (rows) => rows.join('\n'))
|
|
337
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()) }
|
|
338
436
|
} else if (command === 'doctor') {
|
|
339
|
-
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
|
|
340
438
|
} else if (command === 'mcp') runMcp()
|
|
341
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
|
+
}
|