@utopia-studio-design/design-system-cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/bin/utopia-ds-mcp.mjs +9 -2
- package/bin/utopia-ds.mjs +115 -7
- package/data/blocks/activity-feed/ActivityFeedBlock.tsx +255 -0
- package/data/blocks/activity-feed/README.md +16 -0
- package/data/blocks/activity-feed/activity-feed-contract.ts +96 -0
- package/data/blocks/activity-feed/activity-feed-fixture.ts +22 -0
- package/data/blocks/activity-feed/activity-feed.css +154 -0
- package/data/blocks/agent-status/AgentStatusBlock.tsx +313 -0
- package/data/blocks/agent-status/README.md +15 -0
- package/data/blocks/agent-status/agent-status-contract.ts +133 -0
- package/data/blocks/agent-status/agent-status-fixture.ts +79 -0
- package/data/blocks/agent-status/agent-status.css +216 -0
- package/data/blocks/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
- package/data/blocks/analytics-dashboard/README.md +17 -0
- package/data/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
- package/data/blocks/calendar-application/CalendarApplicationBlocks.tsx +519 -0
- package/data/blocks/calendar-application/README.md +20 -0
- package/data/blocks/calendar-application/calendar-application.css +216 -0
- package/data/blocks/calendar-application/calendar-localization.ts +90 -0
- package/data/blocks/calendar-application/calendar-panel-contract.ts +72 -0
- package/data/blocks/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
- package/data/blocks/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
- package/data/blocks/dashboard-composer/README.md +16 -0
- package/data/blocks/dashboard-composer/RegistryDataBlocks.tsx +238 -0
- package/data/blocks/dashboard-composer/dashboard-composer.css +328 -0
- package/data/blocks/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/blocks/database-workspace/README.md +9 -0
- package/data/blocks/database-workspace/database-workspace.css +107 -0
- package/data/blocks/interaction-workbench/InteractionWorkbench.tsx +271 -0
- package/data/blocks/interaction-workbench/README.md +9 -0
- package/data/blocks/interaction-workbench/interaction-workbench.css +226 -0
- package/data/docs/calendar.md +164 -0
- package/data/docs/charts.md +95 -0
- package/data/docs/composition-registry.md +174 -0
- package/data/docs/dashboard-composer.md +94 -0
- package/data/docs/database.md +68 -0
- package/data/docs/examples/dashboard-layout.v1.json +65 -0
- package/data/docs/foundations.md +2 -1
- package/data/docs/interaction-foundation.md +109 -0
- package/data/docs/observability.md +165 -0
- package/data/docs/quick-start-ai.md +55 -1
- package/data/manifests/blocks.json +392 -0
- package/data/manifests/catalog.json +7 -4
- package/data/manifests/components.json +191 -3
- package/data/manifests/composition-registry.json +427 -0
- package/data/manifests/motion-profiles.json +1 -1
- package/data/manifests/patterns.json +113 -1
- package/data/manifests/templates.json +346 -0
- package/data/manifests/theme-renacore.json +142 -0
- package/data/manifests/themes.json +314 -28
- package/data/templates/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/templates/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
- package/data/templates/analytics-dashboard/README.md +6 -0
- package/data/templates/analytics-dashboard/analytics-dashboard.css +92 -0
- package/data/templates/analytics-dashboard/index.html +13 -0
- package/data/templates/analytics-dashboard/main.tsx +196 -0
- package/data/templates/analytics-dashboard/styles.css +120 -0
- package/data/templates/analytics-dashboard/template.manifest.json +17 -0
- package/data/templates/calendar-application/CalendarApplicationBlocks.tsx +519 -0
- package/data/templates/calendar-application/README.md +10 -0
- package/data/templates/calendar-application/calendar-application.css +216 -0
- package/data/templates/calendar-application/calendar-localization.ts +90 -0
- package/data/templates/calendar-application/index.html +14 -0
- package/data/templates/calendar-application/main.tsx +409 -0
- package/data/templates/calendar-application/styles.css +248 -0
- package/data/templates/calendar-application/template.manifest.json +27 -0
- package/data/templates/composition-registry-lab/README.md +9 -0
- package/data/templates/composition-registry-lab/index.html +14 -0
- package/data/templates/composition-registry-lab/main.tsx +217 -0
- package/data/templates/composition-registry-lab/sample-layout.json +63 -0
- package/data/templates/composition-registry-lab/styles.css +343 -0
- package/data/templates/composition-registry-lab/template.manifest.json +34 -0
- package/data/templates/dashboard-composer/ActivityFeedBlock.tsx +255 -0
- package/data/templates/dashboard-composer/AgentStatusBlock.tsx +313 -0
- package/data/templates/dashboard-composer/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/templates/dashboard-composer/AnalyticsMetricBlocks.tsx +87 -0
- package/data/templates/dashboard-composer/CalendarApplicationBlocks.tsx +519 -0
- package/data/templates/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
- package/data/templates/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
- package/data/templates/dashboard-composer/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/templates/dashboard-composer/README.md +53 -0
- package/data/templates/dashboard-composer/RegistryDataBlocks.tsx +238 -0
- package/data/templates/dashboard-composer/activity-feed-contract.ts +96 -0
- package/data/templates/dashboard-composer/activity-feed-fixture.ts +22 -0
- package/data/templates/dashboard-composer/activity-feed.css +154 -0
- package/data/templates/dashboard-composer/agent-status-contract.ts +133 -0
- package/data/templates/dashboard-composer/agent-status-fixture.ts +79 -0
- package/data/templates/dashboard-composer/agent-status-lab/index.html +19 -0
- package/data/templates/dashboard-composer/agent-status-lab/main.tsx +151 -0
- package/data/templates/dashboard-composer/agent-status-lab/styles.css +139 -0
- package/data/templates/dashboard-composer/agent-status.css +216 -0
- package/data/templates/dashboard-composer/analytics-dashboard.css +92 -0
- package/data/templates/dashboard-composer/calendar-application.css +216 -0
- package/data/templates/dashboard-composer/calendar-localization.ts +90 -0
- package/data/templates/dashboard-composer/calendar-panel-contract.ts +72 -0
- package/data/templates/dashboard-composer/dashboard-composer.css +328 -0
- package/data/templates/dashboard-composer/database-workspace.css +107 -0
- package/data/templates/dashboard-composer/index.html +14 -0
- package/data/templates/dashboard-composer/main.tsx +550 -0
- package/data/templates/dashboard-composer/sample-layout.json +147 -0
- package/data/templates/dashboard-composer/styles.css +198 -0
- package/data/templates/dashboard-composer/template.manifest.json +34 -0
- package/data/templates/dashboard-composer/visual-editor/PuckEditorSurface.tsx +132 -0
- package/data/templates/dashboard-composer/visual-editor/VisualEditorApp.tsx +343 -0
- package/data/templates/dashboard-composer/visual-editor/index.html +17 -0
- package/data/templates/dashboard-composer/visual-editor/main.tsx +95 -0
- package/data/templates/dashboard-composer/visual-editor/styles.css +310 -0
- package/data/templates/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/templates/database-workspace/README.md +5 -0
- package/data/templates/database-workspace/database-workspace.css +107 -0
- package/data/templates/database-workspace/index.html +14 -0
- package/data/templates/database-workspace/main.tsx +247 -0
- package/data/templates/database-workspace/styles.css +121 -0
- package/data/templates/database-workspace/template.manifest.json +19 -0
- package/data/templates/interaction-lab/InteractionWorkbench.tsx +271 -0
- package/data/templates/interaction-lab/README.md +7 -0
- package/data/templates/interaction-lab/index.html +14 -0
- package/data/templates/interaction-lab/interaction-workbench.css +226 -0
- package/data/templates/interaction-lab/main.tsx +104 -0
- package/data/templates/interaction-lab/styles.css +224 -0
- package/data/templates/interaction-lab/template.manifest.json +36 -0
- package/lib/api.mjs +72 -5
- package/lib/composition-runtime.mjs +382 -0
- package/lib/telemetry.mjs +74 -0
- package/package.json +4 -1
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,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,
|
|
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
|
-
:
|
|
257
|
+
: designSystemVersion
|
|
241
258
|
const cliDependency = existsSync(join(workspaceCli, 'package.json'))
|
|
242
259
|
? `file:${workspaceCli}`
|
|
243
|
-
:
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|