@utopia-studio-design/design-system-cli 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/bin/utopia-ds-mcp.mjs +9 -2
- package/bin/utopia-ds.mjs +106 -8
- package/data/blocks/activity-feed/ActivityFeedBlock.tsx +255 -0
- package/data/blocks/activity-feed/README.md +16 -0
- package/data/blocks/activity-feed/activity-feed-contract.ts +96 -0
- package/data/blocks/activity-feed/activity-feed-fixture.ts +22 -0
- package/data/blocks/activity-feed/activity-feed.css +154 -0
- package/data/blocks/agent-status/AgentStatusBlock.tsx +313 -0
- package/data/blocks/agent-status/README.md +15 -0
- package/data/blocks/agent-status/agent-status-contract.ts +133 -0
- package/data/blocks/agent-status/agent-status-fixture.ts +79 -0
- package/data/blocks/agent-status/agent-status.css +216 -0
- package/data/blocks/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
- package/data/blocks/analytics-dashboard/README.md +17 -0
- package/data/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
- package/data/blocks/calendar-application/CalendarApplicationBlocks.tsx +519 -0
- package/data/blocks/calendar-application/README.md +20 -0
- package/data/blocks/calendar-application/calendar-application.css +216 -0
- package/data/blocks/calendar-application/calendar-localization.ts +90 -0
- package/data/blocks/calendar-application/calendar-panel-contract.ts +72 -0
- package/data/blocks/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
- package/data/blocks/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
- package/data/blocks/dashboard-composer/README.md +16 -0
- package/data/blocks/dashboard-composer/RegistryDataBlocks.tsx +238 -0
- package/data/blocks/dashboard-composer/dashboard-composer.css +328 -0
- package/data/blocks/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/blocks/database-workspace/README.md +9 -0
- package/data/blocks/database-workspace/database-workspace.css +107 -0
- package/data/blocks/interaction-workbench/InteractionWorkbench.tsx +271 -0
- package/data/blocks/interaction-workbench/README.md +9 -0
- package/data/blocks/interaction-workbench/interaction-workbench.css +226 -0
- package/data/docs/calendar.md +164 -0
- package/data/docs/charts.md +95 -0
- package/data/docs/composition-registry.md +174 -0
- package/data/docs/dashboard-composer.md +94 -0
- package/data/docs/database.md +68 -0
- package/data/docs/examples/dashboard-layout.v1.json +65 -0
- package/data/docs/foundations.md +1 -1
- package/data/docs/interaction-foundation.md +109 -0
- package/data/docs/quick-start-ai.md +23 -1
- package/data/manifests/blocks.json +392 -0
- package/data/manifests/catalog.json +7 -4
- package/data/manifests/components.json +179 -0
- package/data/manifests/composition-registry.json +427 -0
- package/data/manifests/motion-profiles.json +1 -1
- package/data/manifests/patterns.json +113 -1
- package/data/manifests/templates.json +346 -0
- package/data/manifests/theme-renacore.json +142 -0
- package/data/manifests/themes.json +314 -28
- package/data/templates/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/templates/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
- package/data/templates/analytics-dashboard/README.md +6 -0
- package/data/templates/analytics-dashboard/analytics-dashboard.css +92 -0
- package/data/templates/analytics-dashboard/index.html +13 -0
- package/data/templates/analytics-dashboard/main.tsx +196 -0
- package/data/templates/analytics-dashboard/styles.css +120 -0
- package/data/templates/analytics-dashboard/template.manifest.json +17 -0
- package/data/templates/calendar-application/CalendarApplicationBlocks.tsx +519 -0
- package/data/templates/calendar-application/README.md +10 -0
- package/data/templates/calendar-application/calendar-application.css +216 -0
- package/data/templates/calendar-application/calendar-localization.ts +90 -0
- package/data/templates/calendar-application/index.html +14 -0
- package/data/templates/calendar-application/main.tsx +409 -0
- package/data/templates/calendar-application/styles.css +248 -0
- package/data/templates/calendar-application/template.manifest.json +27 -0
- package/data/templates/composition-registry-lab/README.md +9 -0
- package/data/templates/composition-registry-lab/index.html +14 -0
- package/data/templates/composition-registry-lab/main.tsx +217 -0
- package/data/templates/composition-registry-lab/sample-layout.json +63 -0
- package/data/templates/composition-registry-lab/styles.css +343 -0
- package/data/templates/composition-registry-lab/template.manifest.json +34 -0
- package/data/templates/dashboard-composer/ActivityFeedBlock.tsx +255 -0
- package/data/templates/dashboard-composer/AgentStatusBlock.tsx +313 -0
- package/data/templates/dashboard-composer/AnalyticsDashboardBlocks.tsx +107 -0
- package/data/templates/dashboard-composer/AnalyticsMetricBlocks.tsx +87 -0
- package/data/templates/dashboard-composer/CalendarApplicationBlocks.tsx +519 -0
- package/data/templates/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
- package/data/templates/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
- package/data/templates/dashboard-composer/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/templates/dashboard-composer/README.md +53 -0
- package/data/templates/dashboard-composer/RegistryDataBlocks.tsx +238 -0
- package/data/templates/dashboard-composer/activity-feed-contract.ts +96 -0
- package/data/templates/dashboard-composer/activity-feed-fixture.ts +22 -0
- package/data/templates/dashboard-composer/activity-feed.css +154 -0
- package/data/templates/dashboard-composer/agent-status-contract.ts +133 -0
- package/data/templates/dashboard-composer/agent-status-fixture.ts +79 -0
- package/data/templates/dashboard-composer/agent-status-lab/index.html +19 -0
- package/data/templates/dashboard-composer/agent-status-lab/main.tsx +151 -0
- package/data/templates/dashboard-composer/agent-status-lab/styles.css +139 -0
- package/data/templates/dashboard-composer/agent-status.css +216 -0
- package/data/templates/dashboard-composer/analytics-dashboard.css +92 -0
- package/data/templates/dashboard-composer/calendar-application.css +216 -0
- package/data/templates/dashboard-composer/calendar-localization.ts +90 -0
- package/data/templates/dashboard-composer/calendar-panel-contract.ts +72 -0
- package/data/templates/dashboard-composer/dashboard-composer.css +328 -0
- package/data/templates/dashboard-composer/database-workspace.css +107 -0
- package/data/templates/dashboard-composer/index.html +14 -0
- package/data/templates/dashboard-composer/main.tsx +550 -0
- package/data/templates/dashboard-composer/sample-layout.json +147 -0
- package/data/templates/dashboard-composer/styles.css +198 -0
- package/data/templates/dashboard-composer/template.manifest.json +34 -0
- package/data/templates/dashboard-composer/visual-editor/PuckEditorSurface.tsx +132 -0
- package/data/templates/dashboard-composer/visual-editor/VisualEditorApp.tsx +343 -0
- package/data/templates/dashboard-composer/visual-editor/index.html +17 -0
- package/data/templates/dashboard-composer/visual-editor/main.tsx +95 -0
- package/data/templates/dashboard-composer/visual-editor/styles.css +310 -0
- package/data/templates/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
- package/data/templates/database-workspace/README.md +5 -0
- package/data/templates/database-workspace/database-workspace.css +107 -0
- package/data/templates/database-workspace/index.html +14 -0
- package/data/templates/database-workspace/main.tsx +247 -0
- package/data/templates/database-workspace/styles.css +121 -0
- package/data/templates/database-workspace/template.manifest.json +19 -0
- package/data/templates/interaction-lab/InteractionWorkbench.tsx +271 -0
- package/data/templates/interaction-lab/README.md +7 -0
- package/data/templates/interaction-lab/index.html +14 -0
- package/data/templates/interaction-lab/interaction-workbench.css +226 -0
- package/data/templates/interaction-lab/main.tsx +104 -0
- package/data/templates/interaction-lab/styles.css +224 -0
- package/data/templates/interaction-lab/template.manifest.json +36 -0
- package/lib/api.mjs +72 -5
- package/lib/composition-runtime.mjs +382 -0
- package/package.json +4 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
.ceramic-database-toolbar {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-columns: minmax(12rem, 1fr) auto auto;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: var(--space-3);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.ceramic-database-toolbar label,
|
|
9
|
+
.ceramic-database-toolbar input {
|
|
10
|
+
min-inline-size: 0;
|
|
11
|
+
inline-size: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.ceramic-database-toolbar input {
|
|
15
|
+
min-block-size: var(--button-height-sm);
|
|
16
|
+
border: var(--border-width-hairline) solid var(--input);
|
|
17
|
+
padding-inline: var(--space-3);
|
|
18
|
+
background: var(--background);
|
|
19
|
+
color: var(--foreground);
|
|
20
|
+
font: inherit;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.ceramic-database-toolbar > div,
|
|
24
|
+
.ceramic-saved-views {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-wrap: wrap;
|
|
27
|
+
align-items: center;
|
|
28
|
+
gap: var(--space-1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.ceramic-saved-views {
|
|
32
|
+
border-block-end: var(--border-width-hairline) solid var(--border);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ceramic-record-drawer-backdrop {
|
|
36
|
+
position: fixed;
|
|
37
|
+
z-index: var(--z-overlay);
|
|
38
|
+
inset: 0;
|
|
39
|
+
background: var(--overlay);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.ceramic-record-drawer {
|
|
43
|
+
position: fixed;
|
|
44
|
+
inset-block: 0;
|
|
45
|
+
inset-inline-end: 0;
|
|
46
|
+
z-index: var(--z-modal);
|
|
47
|
+
inline-size: min(100%, 28rem);
|
|
48
|
+
overflow: auto;
|
|
49
|
+
padding: var(--space-5);
|
|
50
|
+
border-inline-start: var(--border-width-strong) solid var(--border);
|
|
51
|
+
background: var(--popover-surface);
|
|
52
|
+
color: var(--foreground);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.ceramic-record-drawer header {
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: start;
|
|
58
|
+
justify-content: space-between;
|
|
59
|
+
gap: var(--space-4);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.ceramic-record-drawer header div,
|
|
63
|
+
.ceramic-record-drawer dl {
|
|
64
|
+
display: grid;
|
|
65
|
+
gap: var(--space-2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.ceramic-record-drawer :is(h2, dl, dd) {
|
|
69
|
+
margin: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.ceramic-record-drawer header span,
|
|
73
|
+
.ceramic-record-drawer dt {
|
|
74
|
+
color: var(--muted-foreground);
|
|
75
|
+
font-size: var(--font-size-sm);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ceramic-record-drawer dl > div {
|
|
79
|
+
display: grid;
|
|
80
|
+
gap: var(--space-1);
|
|
81
|
+
padding-block: var(--space-3);
|
|
82
|
+
border-block-end: var(--border-width-hairline) solid var(--border);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ceramic-record-drawer .uds-database-property-cell {
|
|
86
|
+
padding-inline: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ceramic-bulk-action-bar {
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: var(--space-2);
|
|
93
|
+
inline-size: fit-content;
|
|
94
|
+
padding: var(--space-2);
|
|
95
|
+
border: var(--border-width-strong) solid var(--border);
|
|
96
|
+
background: var(--surface-elevated);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@media (max-width: 44rem) {
|
|
100
|
+
.ceramic-database-toolbar {
|
|
101
|
+
grid-template-columns: 1fr;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.ceramic-database-toolbar > :last-child {
|
|
105
|
+
inline-size: 100%;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" dir="ltr" data-theme="utopia-default" data-color-mode="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="description" content="A live Ceramic dashboard composed from validated Layout JSON." />
|
|
7
|
+
<title>Dashboard Composer — Ceramic Design System</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<a class="skip-link" href="#main-content">Skip to dashboard composer</a>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="./main.tsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StrictMode,
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import { createRoot } from 'react-dom/client'
|
|
11
|
+
import { Badge } from '@utopia-studio-design/design-system/Badge'
|
|
12
|
+
import { Button } from '@utopia-studio-design/design-system/Button'
|
|
13
|
+
import {
|
|
14
|
+
getCeramicBlockDefinition,
|
|
15
|
+
validateCeramicAgentMutation,
|
|
16
|
+
validateCeramicLayout,
|
|
17
|
+
type CeramicLayoutBlock,
|
|
18
|
+
type CeramicLayoutDocument,
|
|
19
|
+
} from '@utopia-studio-design/design-system/Composition'
|
|
20
|
+
import type {
|
|
21
|
+
SchedulerEvent,
|
|
22
|
+
} from '@utopia-studio-design/design-system/Calendar'
|
|
23
|
+
import {
|
|
24
|
+
AgentStatusBlock,
|
|
25
|
+
type AgentStatusItem,
|
|
26
|
+
} from './AgentStatusBlock'
|
|
27
|
+
import { resolveAgentStatusConfiguration } from './agent-status-contract'
|
|
28
|
+
import { getAgentStatusFixture } from './agent-status-fixture'
|
|
29
|
+
import {
|
|
30
|
+
ActivityFeedBlock,
|
|
31
|
+
} from './ActivityFeedBlock'
|
|
32
|
+
import { resolveActivityFeedConfiguration } from './activity-feed-contract'
|
|
33
|
+
import { getActivityFeedFixture } from './activity-feed-fixture'
|
|
34
|
+
import { KPIGrid, type MetricDefinition } from './AnalyticsMetricBlocks'
|
|
35
|
+
import { SchedulerShell } from './CalendarApplicationBlocks'
|
|
36
|
+
import { resolveCalendarPanelConfiguration } from './calendar-panel-contract'
|
|
37
|
+
import {
|
|
38
|
+
CeramicDashboardRenderer,
|
|
39
|
+
TodoListBlock,
|
|
40
|
+
type CeramicBlockRenderer,
|
|
41
|
+
type TodoFilter,
|
|
42
|
+
type TodoItem,
|
|
43
|
+
} from './DashboardComposerBlocks'
|
|
44
|
+
import {
|
|
45
|
+
ChartPanelRegistryBlock,
|
|
46
|
+
IssueTableRegistryBlock,
|
|
47
|
+
} from './RegistryDataBlocks'
|
|
48
|
+
import '@utopia-studio-design/design-system/core.css'
|
|
49
|
+
import '@utopia-studio-design/design-system/themes/utopia-default.css'
|
|
50
|
+
import './agent-status.css'
|
|
51
|
+
import './activity-feed.css'
|
|
52
|
+
import './analytics-dashboard.css'
|
|
53
|
+
import './calendar-application.css'
|
|
54
|
+
import './dashboard-composer.css'
|
|
55
|
+
import initialLayoutDocument from './sample-layout.json'
|
|
56
|
+
import './styles.css'
|
|
57
|
+
|
|
58
|
+
const initialLayout = initialLayoutDocument as CeramicLayoutDocument
|
|
59
|
+
const anchorDate = new Date(2026, 2, 9, 12)
|
|
60
|
+
|
|
61
|
+
const initialTasks: TodoItem[] = [
|
|
62
|
+
{ id: 'todo-1', title: 'Review the Calendar block API', completed: true, dueLabel: 'Today', priority: 'high' },
|
|
63
|
+
{ id: 'todo-2', title: 'Approve the dashboard layout', completed: false, dueLabel: 'Today', priority: 'high' },
|
|
64
|
+
{ id: 'todo-3', title: 'Prepare the npm release notes', completed: false, dueLabel: 'Today', priority: 'medium' },
|
|
65
|
+
{ id: 'todo-4', title: 'Plan the Puck adapter spike', completed: false, dueLabel: 'Tomorrow', priority: 'low' },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
const initialEvents: SchedulerEvent[] = [
|
|
69
|
+
{
|
|
70
|
+
id: 'design-review',
|
|
71
|
+
title: 'Composer review',
|
|
72
|
+
start: '2026-03-09T01:00:00.000Z',
|
|
73
|
+
end: '2026-03-09T02:00:00.000Z',
|
|
74
|
+
tone: 'accent',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'release-check',
|
|
78
|
+
title: 'Release QA',
|
|
79
|
+
start: '2026-03-09T03:30:00.000Z',
|
|
80
|
+
end: '2026-03-09T04:15:00.000Z',
|
|
81
|
+
tone: 'positive',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'focus-block',
|
|
85
|
+
title: 'Focus block',
|
|
86
|
+
start: '2026-03-09T05:00:00.000Z',
|
|
87
|
+
end: '2026-03-09T06:00:00.000Z',
|
|
88
|
+
recurrence: { frequency: 'daily', count: 5 },
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'publish-window',
|
|
92
|
+
title: 'Publish window',
|
|
93
|
+
start: '2026-03-11T00:00:00.000Z',
|
|
94
|
+
end: '2026-03-12T00:00:00.000Z',
|
|
95
|
+
allDay: true,
|
|
96
|
+
tone: 'warning',
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
const arabicEventTitles: Record<string, string> = {
|
|
101
|
+
'design-review': 'مراجعة التخطيط',
|
|
102
|
+
'focus-block': 'وقت التركيز',
|
|
103
|
+
'publish-window': 'نافذة النشر',
|
|
104
|
+
'release-check': 'فحص الإصدار',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const metrics: MetricDefinition[] = [
|
|
108
|
+
{ id: 'open', label: 'Open tasks', value: 3, change: -25, changeLabel: 'vs yesterday' },
|
|
109
|
+
{ id: 'completed', label: 'Completed', value: 8, change: 14, changeLabel: 'this week' },
|
|
110
|
+
{ id: 'focus', label: 'Focus hours', value: 12.5, change: 6, changeLabel: 'this week' },
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
interface ComposerData {
|
|
114
|
+
agents: AgentStatusItem[]
|
|
115
|
+
compact: boolean
|
|
116
|
+
date: Date
|
|
117
|
+
direction: 'ltr' | 'rtl'
|
|
118
|
+
events: SchedulerEvent[]
|
|
119
|
+
layout: CeramicLayoutDocument
|
|
120
|
+
setDate: (date: Date) => void
|
|
121
|
+
setAgents: (items: AgentStatusItem[]) => void
|
|
122
|
+
setEvents: (events: SchedulerEvent[]) => void
|
|
123
|
+
setLayout: (layout: CeramicLayoutDocument) => void
|
|
124
|
+
setTasks: (items: TodoItem[]) => void
|
|
125
|
+
tasks: TodoItem[]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const ComposerDataContext = createContext<ComposerData | null>(null)
|
|
129
|
+
|
|
130
|
+
function useComposerData() {
|
|
131
|
+
const value = useContext(ComposerDataContext)
|
|
132
|
+
if (!value) throw new Error('Dashboard block renderer must be used inside ComposerDataContext.')
|
|
133
|
+
return value
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function updateBlockProps(
|
|
137
|
+
layout: CeramicLayoutDocument,
|
|
138
|
+
blockId: string,
|
|
139
|
+
props: Record<string, unknown>,
|
|
140
|
+
) {
|
|
141
|
+
return {
|
|
142
|
+
...layout,
|
|
143
|
+
blocks: layout.blocks.map((candidate) => (
|
|
144
|
+
candidate.id === blockId
|
|
145
|
+
? { ...candidate, props: { ...candidate.props, ...props } }
|
|
146
|
+
: candidate
|
|
147
|
+
)),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function TodoRenderer({ block }: { block: CeramicLayoutBlock }) {
|
|
152
|
+
const { tasks, setTasks } = useComposerData()
|
|
153
|
+
return (
|
|
154
|
+
<TodoListBlock
|
|
155
|
+
density={(block.props.density as 'compact' | 'default' | 'comfortable') ?? 'default'}
|
|
156
|
+
filter={(block.props.filter as TodoFilter) ?? 'all'}
|
|
157
|
+
items={tasks}
|
|
158
|
+
showCompleted={block.props.showCompleted !== false}
|
|
159
|
+
title={(block.props.title as string) ?? 'Today’s tasks'}
|
|
160
|
+
onItemsChange={setTasks}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function CalendarRenderer({ block, direction }: { block: CeramicLayoutBlock; direction: 'ltr' | 'rtl' }) {
|
|
166
|
+
const { compact, date, events, layout, setDate, setEvents, setLayout } = useComposerData()
|
|
167
|
+
const configuration = resolveCalendarPanelConfiguration(block.props, { compact, direction })
|
|
168
|
+
const displayEvents = configuration.locale.startsWith('ar')
|
|
169
|
+
? events.map((event) => ({
|
|
170
|
+
...event,
|
|
171
|
+
title: arabicEventTitles[event.id] ?? event.title,
|
|
172
|
+
}))
|
|
173
|
+
: events
|
|
174
|
+
return (
|
|
175
|
+
<SchedulerShell
|
|
176
|
+
{...configuration}
|
|
177
|
+
date={date}
|
|
178
|
+
events={displayEvents}
|
|
179
|
+
permissions={{ create: true, edit: true, move: true, resize: true, duplicate: true, delete: true }}
|
|
180
|
+
onDateChange={setDate}
|
|
181
|
+
onEventCreate={(selectedDate) => {
|
|
182
|
+
const start = new Date(selectedDate)
|
|
183
|
+
start.setHours(14, 0, 0, 0)
|
|
184
|
+
setEvents([...events, {
|
|
185
|
+
id: `event-${events.length + 1}`,
|
|
186
|
+
title: configuration.locale.startsWith('ar') ? 'حدث جديد' : 'New event',
|
|
187
|
+
start: start.toISOString(),
|
|
188
|
+
end: new Date(start.valueOf() + 30 * 60_000).toISOString(),
|
|
189
|
+
}])
|
|
190
|
+
}}
|
|
191
|
+
onEventsChange={setEvents}
|
|
192
|
+
onViewChange={(nextView) => setLayout(updateBlockProps(layout, block.id, { view: nextView }))}
|
|
193
|
+
/>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function MetricsRenderer({ block }: { block: CeramicLayoutBlock }) {
|
|
198
|
+
const metricIds = new Set((block.props.metrics as string[]) ?? metrics.map((metric) => metric.id))
|
|
199
|
+
return (
|
|
200
|
+
<KPIGrid
|
|
201
|
+
density={(block.props.density as 'compact' | 'default' | 'comfortable') ?? 'compact'}
|
|
202
|
+
metrics={metrics.filter((metric) => metricIds.has(metric.id))}
|
|
203
|
+
/>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function ActivityRenderer({ block, direction }: { block: CeramicLayoutBlock; direction: 'ltr' | 'rtl' }) {
|
|
208
|
+
const configuration = resolveActivityFeedConfiguration(block.props, { direction })
|
|
209
|
+
return (
|
|
210
|
+
<ActivityFeedBlock
|
|
211
|
+
{...configuration}
|
|
212
|
+
items={getActivityFeedFixture(configuration.locale)}
|
|
213
|
+
/>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function AgentStatusRenderer({ block, direction }: { block: CeramicLayoutBlock; direction: 'ltr' | 'rtl' }) {
|
|
218
|
+
const { agents, setAgents } = useComposerData()
|
|
219
|
+
const configuration = resolveAgentStatusConfiguration(block.props, { direction })
|
|
220
|
+
const stateById = new Map(agents.map((item) => [item.id, item]))
|
|
221
|
+
const localizedItems = getAgentStatusFixture(configuration.locale).map((item) => {
|
|
222
|
+
const runtimeItem = stateById.get(item.id)
|
|
223
|
+
return runtimeItem
|
|
224
|
+
? {
|
|
225
|
+
...item,
|
|
226
|
+
approval: runtimeItem.approval ? item.approval : undefined,
|
|
227
|
+
progress: runtimeItem.progress,
|
|
228
|
+
state: runtimeItem.state,
|
|
229
|
+
}
|
|
230
|
+
: item
|
|
231
|
+
})
|
|
232
|
+
const updateApproval = (item: AgentStatusItem, approved: boolean) => {
|
|
233
|
+
setAgents(agents.map((candidate) => (
|
|
234
|
+
candidate.id === item.id
|
|
235
|
+
? {
|
|
236
|
+
...candidate,
|
|
237
|
+
approval: undefined,
|
|
238
|
+
progress: approved ? Math.max(1, candidate.progress ?? 0) : candidate.progress,
|
|
239
|
+
state: approved ? 'running' : 'blocked',
|
|
240
|
+
}
|
|
241
|
+
: candidate
|
|
242
|
+
)))
|
|
243
|
+
}
|
|
244
|
+
return (
|
|
245
|
+
<AgentStatusBlock
|
|
246
|
+
{...configuration}
|
|
247
|
+
items={localizedItems}
|
|
248
|
+
onApprove={(item) => updateApproval(item, true)}
|
|
249
|
+
onReject={(item) => updateApproval(item, false)}
|
|
250
|
+
/>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const blockRenderers: Record<string, CeramicBlockRenderer> = {
|
|
255
|
+
'agent-status': AgentStatusRenderer,
|
|
256
|
+
'activity-feed': ActivityRenderer,
|
|
257
|
+
'chart-panel': ChartPanelRegistryBlock,
|
|
258
|
+
'todo-list': TodoRenderer,
|
|
259
|
+
'calendar-panel': CalendarRenderer,
|
|
260
|
+
'issue-table': IssueTableRegistryBlock,
|
|
261
|
+
'metric-grid': MetricsRenderer,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function App() {
|
|
265
|
+
const [layout, setLayout] = useState<CeramicLayoutDocument>(() => structuredClone(initialLayout))
|
|
266
|
+
const [agents, setAgents] = useState<AgentStatusItem[]>(() => getAgentStatusFixture('en-US'))
|
|
267
|
+
const [tasks, setTasks] = useState(initialTasks)
|
|
268
|
+
const [events, setEvents] = useState(initialEvents)
|
|
269
|
+
const [date, setDate] = useState(anchorDate)
|
|
270
|
+
const [direction, setDirection] = useState<'ltr' | 'rtl'>('ltr')
|
|
271
|
+
const [dark, setDark] = useState(false)
|
|
272
|
+
const [editable, setEditable] = useState(true)
|
|
273
|
+
const [compact, setCompact] = useState(() => window.matchMedia('(max-width: 48rem)').matches)
|
|
274
|
+
const [feedback, setFeedback] = useState('Layout JSON validated. Seven blocks are rendered.')
|
|
275
|
+
const nextBlockId = useRef(1)
|
|
276
|
+
const validation = validateCeramicLayout(layout)
|
|
277
|
+
const previewsArabic = layout.blocks.some((block) => (
|
|
278
|
+
(block.type === 'calendar-panel' || block.type === 'activity-feed' || block.type === 'agent-status')
|
|
279
|
+
&& block.props.locale === 'ar-SA'
|
|
280
|
+
))
|
|
281
|
+
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
const media = window.matchMedia('(max-width: 48rem)')
|
|
284
|
+
const handleChange = () => setCompact(media.matches)
|
|
285
|
+
media.addEventListener('change', handleChange)
|
|
286
|
+
return () => media.removeEventListener('change', handleChange)
|
|
287
|
+
}, [])
|
|
288
|
+
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
document.documentElement.dir = direction
|
|
291
|
+
return () => {
|
|
292
|
+
document.documentElement.dir = 'ltr'
|
|
293
|
+
}
|
|
294
|
+
}, [direction])
|
|
295
|
+
|
|
296
|
+
const commitAgentChange = (next: CeramicLayoutDocument, successMessage: string) => {
|
|
297
|
+
const result = validateCeramicAgentMutation(layout, next)
|
|
298
|
+
if (!result.ok) {
|
|
299
|
+
setFeedback(`Registry rejected the proposal: ${result.issues[0]?.message}`)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
setLayout(next)
|
|
303
|
+
setFeedback(successMessage)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const addBlock = (
|
|
307
|
+
type:
|
|
308
|
+
| 'todo-list'
|
|
309
|
+
| 'calendar-panel'
|
|
310
|
+
| 'activity-feed'
|
|
311
|
+
| 'agent-status'
|
|
312
|
+
| 'issue-table'
|
|
313
|
+
| 'chart-panel',
|
|
314
|
+
) => {
|
|
315
|
+
const definition = getCeramicBlockDefinition(type)
|
|
316
|
+
if (!definition) return
|
|
317
|
+
const nextOrder = Math.max(0, ...layout.blocks.map((block) => block.placement.order)) + 1
|
|
318
|
+
const block: CeramicLayoutBlock = {
|
|
319
|
+
id: `${type}-${nextBlockId.current++}`,
|
|
320
|
+
type,
|
|
321
|
+
version: definition.version,
|
|
322
|
+
placement: { column: 1, span: 12, order: nextOrder, compactSpan: 12 },
|
|
323
|
+
props: type === 'todo-list'
|
|
324
|
+
? { title: 'New task list', filter: 'all', density: 'default', showCompleted: true }
|
|
325
|
+
: type === 'issue-table'
|
|
326
|
+
? {
|
|
327
|
+
columns: ['title', 'status', 'priority', 'owner', 'due'],
|
|
328
|
+
density: 'default',
|
|
329
|
+
view: 'active',
|
|
330
|
+
}
|
|
331
|
+
: type === 'chart-panel'
|
|
332
|
+
? {
|
|
333
|
+
chartType: 'line',
|
|
334
|
+
comparison: 'previous-period',
|
|
335
|
+
showTable: false,
|
|
336
|
+
}
|
|
337
|
+
: type === 'agent-status'
|
|
338
|
+
? {
|
|
339
|
+
density: 'default',
|
|
340
|
+
direction: 'inherit',
|
|
341
|
+
filter: 'all',
|
|
342
|
+
locale: 'en-US',
|
|
343
|
+
maxItems: 6,
|
|
344
|
+
showApprovals: true,
|
|
345
|
+
timeZone: 'Asia/Seoul',
|
|
346
|
+
}
|
|
347
|
+
: type === 'activity-feed'
|
|
348
|
+
? {
|
|
349
|
+
density: 'default',
|
|
350
|
+
direction: 'inherit',
|
|
351
|
+
groupBy: 'day',
|
|
352
|
+
locale: 'en-US',
|
|
353
|
+
pageSize: 4,
|
|
354
|
+
showSources: true,
|
|
355
|
+
timeZone: 'Asia/Seoul',
|
|
356
|
+
}
|
|
357
|
+
: {
|
|
358
|
+
direction: 'inherit',
|
|
359
|
+
initialScrollHour: 8,
|
|
360
|
+
locale: 'en-US',
|
|
361
|
+
secondaryCalendar: 'none',
|
|
362
|
+
showAgendaAlternative: false,
|
|
363
|
+
timeZone: 'Asia/Seoul',
|
|
364
|
+
view: 'week',
|
|
365
|
+
},
|
|
366
|
+
locked: false,
|
|
367
|
+
}
|
|
368
|
+
commitAgentChange({ ...layout, blocks: [...layout.blocks, block] }, `${definition.title} added by the agent.`)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const toggleLocalizedBlocks = () => {
|
|
372
|
+
const locale = previewsArabic ? 'en-US' : 'ar-SA'
|
|
373
|
+
const directionMode = previewsArabic ? 'inherit' : 'auto'
|
|
374
|
+
const next = {
|
|
375
|
+
...layout,
|
|
376
|
+
blocks: layout.blocks.map((block) => (
|
|
377
|
+
block.type === 'calendar-panel' || block.type === 'activity-feed' || block.type === 'agent-status'
|
|
378
|
+
? {
|
|
379
|
+
...block,
|
|
380
|
+
props: {
|
|
381
|
+
...block.props,
|
|
382
|
+
direction: directionMode,
|
|
383
|
+
locale,
|
|
384
|
+
},
|
|
385
|
+
}
|
|
386
|
+
: block
|
|
387
|
+
)),
|
|
388
|
+
}
|
|
389
|
+
commitAgentChange(
|
|
390
|
+
next,
|
|
391
|
+
previewsArabic
|
|
392
|
+
? 'Localized blocks restored to English.'
|
|
393
|
+
: 'Calendar, Activity Feed, and Agent Status now preview Arabic with automatic RTL.',
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const proposeSwap = () => {
|
|
398
|
+
const todo = layout.blocks.find((block) => block.type === 'todo-list')
|
|
399
|
+
const calendar = layout.blocks.find((block) => block.type === 'calendar-panel')
|
|
400
|
+
if (!todo || !calendar) {
|
|
401
|
+
setFeedback('Add both a To-do List and Calendar before asking the agent to rearrange them.')
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
const next = {
|
|
405
|
+
...layout,
|
|
406
|
+
blocks: layout.blocks.map((block) => {
|
|
407
|
+
if (block.id === todo.id) {
|
|
408
|
+
return {
|
|
409
|
+
...block,
|
|
410
|
+
placement: {
|
|
411
|
+
...block.placement,
|
|
412
|
+
column: Math.min(todo.placement.column, calendar.placement.column) + calendar.placement.span,
|
|
413
|
+
order: Math.max(todo.placement.order, calendar.placement.order),
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (block.id === calendar.id) {
|
|
418
|
+
return {
|
|
419
|
+
...block,
|
|
420
|
+
placement: {
|
|
421
|
+
...block.placement,
|
|
422
|
+
column: Math.min(todo.placement.column, calendar.placement.column),
|
|
423
|
+
order: Math.min(todo.placement.order, calendar.placement.order),
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return block
|
|
428
|
+
}),
|
|
429
|
+
}
|
|
430
|
+
commitAgentChange(next, 'Agent proposal applied: Calendar and To-do positions were exchanged.')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const data = useMemo<ComposerData>(() => ({
|
|
434
|
+
agents,
|
|
435
|
+
compact,
|
|
436
|
+
date,
|
|
437
|
+
direction,
|
|
438
|
+
events,
|
|
439
|
+
layout,
|
|
440
|
+
setDate,
|
|
441
|
+
setAgents,
|
|
442
|
+
setEvents,
|
|
443
|
+
setLayout,
|
|
444
|
+
setTasks,
|
|
445
|
+
tasks,
|
|
446
|
+
}), [agents, compact, date, direction, events, layout, tasks])
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<ComposerDataContext.Provider value={data}>
|
|
450
|
+
<main
|
|
451
|
+
className="dashboard-composer-template"
|
|
452
|
+
data-color-mode={dark ? 'dark' : 'light'}
|
|
453
|
+
data-theme="utopia-default"
|
|
454
|
+
dir={direction}
|
|
455
|
+
id="main-content"
|
|
456
|
+
>
|
|
457
|
+
<header className="composer-header">
|
|
458
|
+
<div>
|
|
459
|
+
<span>Live composition</span>
|
|
460
|
+
<h1>Arrange real Ceramic blocks, not placeholders</h1>
|
|
461
|
+
<p>To-do, Calendar, Issue Table, Chart, Activity, Agent Status, and Metrics are rendered from the same validated Layout JSON an agent can edit.</p>
|
|
462
|
+
</div>
|
|
463
|
+
<div aria-label="Preview controls">
|
|
464
|
+
<Button asChild>
|
|
465
|
+
<a href="/templates/dashboard-composer/visual-editor/">Open visual editor</a>
|
|
466
|
+
</Button>
|
|
467
|
+
<Button asChild variant="outline">
|
|
468
|
+
<a href="/templates/dashboard-composer/agent-status-lab/">Open Agent Status State Lab</a>
|
|
469
|
+
</Button>
|
|
470
|
+
<Button variant="outline" onClick={toggleLocalizedBlocks}>
|
|
471
|
+
{previewsArabic ? 'Preview English blocks' : 'Preview Arabic blocks'}
|
|
472
|
+
</Button>
|
|
473
|
+
<Button variant="outline" onClick={() => setDirection((value) => value === 'ltr' ? 'rtl' : 'ltr')}>
|
|
474
|
+
{direction === 'ltr' ? 'Switch to RTL' : 'Switch to LTR'}
|
|
475
|
+
</Button>
|
|
476
|
+
<Button variant="outline" onClick={() => setDark((value) => !value)}>
|
|
477
|
+
{dark ? 'Switch to light' : 'Switch to dark'}
|
|
478
|
+
</Button>
|
|
479
|
+
</div>
|
|
480
|
+
</header>
|
|
481
|
+
|
|
482
|
+
<section aria-labelledby="composer-controls-title" className="composer-controls">
|
|
483
|
+
<div>
|
|
484
|
+
<span>Dashboard controls</span>
|
|
485
|
+
<h2 id="composer-controls-title">Agent and human use the same contract</h2>
|
|
486
|
+
</div>
|
|
487
|
+
<div>
|
|
488
|
+
<Button aria-pressed={editable} variant={editable ? 'default' : 'outline'} onClick={() => setEditable((value) => !value)}>
|
|
489
|
+
{editable ? 'Finish arranging' : 'Arrange blocks'}
|
|
490
|
+
</Button>
|
|
491
|
+
<Button variant="outline" onClick={proposeSwap}>Ask agent to swap To-do and Calendar</Button>
|
|
492
|
+
<Button
|
|
493
|
+
variant="outline"
|
|
494
|
+
onClick={() => {
|
|
495
|
+
setLayout(structuredClone(initialLayout))
|
|
496
|
+
setFeedback('Original dashboard layout restored.')
|
|
497
|
+
}}
|
|
498
|
+
>
|
|
499
|
+
Reset layout
|
|
500
|
+
</Button>
|
|
501
|
+
</div>
|
|
502
|
+
<output aria-live="polite" data-valid={validation.ok}>
|
|
503
|
+
<Badge variant={validation.ok ? 'secondary' : 'destructive'}>
|
|
504
|
+
{validation.ok ? 'Schema valid' : 'Schema failed'}
|
|
505
|
+
</Badge>
|
|
506
|
+
<span>{feedback}</span>
|
|
507
|
+
</output>
|
|
508
|
+
</section>
|
|
509
|
+
|
|
510
|
+
<section aria-labelledby="palette-heading" className="composer-palette">
|
|
511
|
+
<div>
|
|
512
|
+
<span>Registry palette</span>
|
|
513
|
+
<h2 id="palette-heading">Add another real block</h2>
|
|
514
|
+
</div>
|
|
515
|
+
<div>
|
|
516
|
+
<Button size="sm" variant="outline" onClick={() => addBlock('todo-list')}>Add To-do List</Button>
|
|
517
|
+
<Button size="sm" variant="outline" onClick={() => addBlock('calendar-panel')}>Add Calendar</Button>
|
|
518
|
+
<Button size="sm" variant="outline" onClick={() => addBlock('activity-feed')}>Add Activity Feed</Button>
|
|
519
|
+
<Button size="sm" variant="outline" onClick={() => addBlock('agent-status')}>Add Agent Status</Button>
|
|
520
|
+
<Button size="sm" variant="outline" onClick={() => addBlock('issue-table')}>Add Issue Table</Button>
|
|
521
|
+
<Button size="sm" variant="outline" onClick={() => addBlock('chart-panel')}>Add Chart Panel</Button>
|
|
522
|
+
</div>
|
|
523
|
+
</section>
|
|
524
|
+
|
|
525
|
+
<CeramicDashboardRenderer
|
|
526
|
+
direction={direction}
|
|
527
|
+
editable={editable}
|
|
528
|
+
layout={layout}
|
|
529
|
+
renderers={blockRenderers}
|
|
530
|
+
onLayoutChange={(next) => {
|
|
531
|
+
setLayout(next)
|
|
532
|
+
setFeedback('Layout updated through registered block controls.')
|
|
533
|
+
}}
|
|
534
|
+
/>
|
|
535
|
+
|
|
536
|
+
<details className="composer-json">
|
|
537
|
+
<summary>Inspect the live Layout JSON</summary>
|
|
538
|
+
<p>This document stores semantic block choices and placement. Product tasks and events remain in application data.</p>
|
|
539
|
+
<pre>{JSON.stringify(layout, null, 2)}</pre>
|
|
540
|
+
</details>
|
|
541
|
+
</main>
|
|
542
|
+
</ComposerDataContext.Provider>
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
createRoot(document.getElementById('root')!).render(
|
|
547
|
+
<StrictMode>
|
|
548
|
+
<App />
|
|
549
|
+
</StrictMode>,
|
|
550
|
+
)
|