@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.
Files changed (126) hide show
  1. package/README.md +23 -0
  2. package/bin/utopia-ds-mcp.mjs +9 -2
  3. package/bin/utopia-ds.mjs +106 -8
  4. package/data/blocks/activity-feed/ActivityFeedBlock.tsx +255 -0
  5. package/data/blocks/activity-feed/README.md +16 -0
  6. package/data/blocks/activity-feed/activity-feed-contract.ts +96 -0
  7. package/data/blocks/activity-feed/activity-feed-fixture.ts +22 -0
  8. package/data/blocks/activity-feed/activity-feed.css +154 -0
  9. package/data/blocks/agent-status/AgentStatusBlock.tsx +313 -0
  10. package/data/blocks/agent-status/README.md +15 -0
  11. package/data/blocks/agent-status/agent-status-contract.ts +133 -0
  12. package/data/blocks/agent-status/agent-status-fixture.ts +79 -0
  13. package/data/blocks/agent-status/agent-status.css +216 -0
  14. package/data/blocks/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
  15. package/data/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  16. package/data/blocks/analytics-dashboard/README.md +17 -0
  17. package/data/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
  18. package/data/blocks/calendar-application/CalendarApplicationBlocks.tsx +519 -0
  19. package/data/blocks/calendar-application/README.md +20 -0
  20. package/data/blocks/calendar-application/calendar-application.css +216 -0
  21. package/data/blocks/calendar-application/calendar-localization.ts +90 -0
  22. package/data/blocks/calendar-application/calendar-panel-contract.ts +72 -0
  23. package/data/blocks/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
  24. package/data/blocks/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
  25. package/data/blocks/dashboard-composer/README.md +16 -0
  26. package/data/blocks/dashboard-composer/RegistryDataBlocks.tsx +238 -0
  27. package/data/blocks/dashboard-composer/dashboard-composer.css +328 -0
  28. package/data/blocks/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
  29. package/data/blocks/database-workspace/README.md +9 -0
  30. package/data/blocks/database-workspace/database-workspace.css +107 -0
  31. package/data/blocks/interaction-workbench/InteractionWorkbench.tsx +271 -0
  32. package/data/blocks/interaction-workbench/README.md +9 -0
  33. package/data/blocks/interaction-workbench/interaction-workbench.css +226 -0
  34. package/data/docs/calendar.md +164 -0
  35. package/data/docs/charts.md +95 -0
  36. package/data/docs/composition-registry.md +174 -0
  37. package/data/docs/dashboard-composer.md +94 -0
  38. package/data/docs/database.md +68 -0
  39. package/data/docs/examples/dashboard-layout.v1.json +65 -0
  40. package/data/docs/foundations.md +1 -1
  41. package/data/docs/interaction-foundation.md +109 -0
  42. package/data/docs/quick-start-ai.md +23 -1
  43. package/data/manifests/blocks.json +392 -0
  44. package/data/manifests/catalog.json +7 -4
  45. package/data/manifests/components.json +179 -0
  46. package/data/manifests/composition-registry.json +427 -0
  47. package/data/manifests/motion-profiles.json +1 -1
  48. package/data/manifests/patterns.json +113 -1
  49. package/data/manifests/templates.json +346 -0
  50. package/data/manifests/theme-renacore.json +142 -0
  51. package/data/manifests/theme-vyapti.json +125 -0
  52. package/data/manifests/themes.json +400 -28
  53. package/data/templates/analytics-dashboard/AnalyticsDashboardBlocks.tsx +107 -0
  54. package/data/templates/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  55. package/data/templates/analytics-dashboard/README.md +6 -0
  56. package/data/templates/analytics-dashboard/analytics-dashboard.css +92 -0
  57. package/data/templates/analytics-dashboard/index.html +13 -0
  58. package/data/templates/analytics-dashboard/main.tsx +196 -0
  59. package/data/templates/analytics-dashboard/styles.css +120 -0
  60. package/data/templates/analytics-dashboard/template.manifest.json +17 -0
  61. package/data/templates/calendar-application/CalendarApplicationBlocks.tsx +519 -0
  62. package/data/templates/calendar-application/README.md +10 -0
  63. package/data/templates/calendar-application/calendar-application.css +216 -0
  64. package/data/templates/calendar-application/calendar-localization.ts +90 -0
  65. package/data/templates/calendar-application/index.html +14 -0
  66. package/data/templates/calendar-application/main.tsx +409 -0
  67. package/data/templates/calendar-application/styles.css +248 -0
  68. package/data/templates/calendar-application/template.manifest.json +27 -0
  69. package/data/templates/composition-registry-lab/README.md +9 -0
  70. package/data/templates/composition-registry-lab/index.html +14 -0
  71. package/data/templates/composition-registry-lab/main.tsx +217 -0
  72. package/data/templates/composition-registry-lab/sample-layout.json +63 -0
  73. package/data/templates/composition-registry-lab/styles.css +343 -0
  74. package/data/templates/composition-registry-lab/template.manifest.json +34 -0
  75. package/data/templates/dashboard-composer/ActivityFeedBlock.tsx +255 -0
  76. package/data/templates/dashboard-composer/AgentStatusBlock.tsx +313 -0
  77. package/data/templates/dashboard-composer/AnalyticsDashboardBlocks.tsx +107 -0
  78. package/data/templates/dashboard-composer/AnalyticsMetricBlocks.tsx +87 -0
  79. package/data/templates/dashboard-composer/CalendarApplicationBlocks.tsx +519 -0
  80. package/data/templates/dashboard-composer/CeramicPuckAdapter.tsx +380 -0
  81. package/data/templates/dashboard-composer/DashboardComposerBlocks.tsx +337 -0
  82. package/data/templates/dashboard-composer/DatabaseWorkspaceBlocks.tsx +240 -0
  83. package/data/templates/dashboard-composer/README.md +53 -0
  84. package/data/templates/dashboard-composer/RegistryDataBlocks.tsx +238 -0
  85. package/data/templates/dashboard-composer/activity-feed-contract.ts +96 -0
  86. package/data/templates/dashboard-composer/activity-feed-fixture.ts +22 -0
  87. package/data/templates/dashboard-composer/activity-feed.css +154 -0
  88. package/data/templates/dashboard-composer/agent-status-contract.ts +133 -0
  89. package/data/templates/dashboard-composer/agent-status-fixture.ts +79 -0
  90. package/data/templates/dashboard-composer/agent-status-lab/index.html +19 -0
  91. package/data/templates/dashboard-composer/agent-status-lab/main.tsx +151 -0
  92. package/data/templates/dashboard-composer/agent-status-lab/styles.css +139 -0
  93. package/data/templates/dashboard-composer/agent-status.css +216 -0
  94. package/data/templates/dashboard-composer/analytics-dashboard.css +92 -0
  95. package/data/templates/dashboard-composer/calendar-application.css +216 -0
  96. package/data/templates/dashboard-composer/calendar-localization.ts +90 -0
  97. package/data/templates/dashboard-composer/calendar-panel-contract.ts +72 -0
  98. package/data/templates/dashboard-composer/dashboard-composer.css +328 -0
  99. package/data/templates/dashboard-composer/database-workspace.css +107 -0
  100. package/data/templates/dashboard-composer/index.html +14 -0
  101. package/data/templates/dashboard-composer/main.tsx +550 -0
  102. package/data/templates/dashboard-composer/sample-layout.json +147 -0
  103. package/data/templates/dashboard-composer/styles.css +198 -0
  104. package/data/templates/dashboard-composer/template.manifest.json +34 -0
  105. package/data/templates/dashboard-composer/visual-editor/PuckEditorSurface.tsx +132 -0
  106. package/data/templates/dashboard-composer/visual-editor/VisualEditorApp.tsx +343 -0
  107. package/data/templates/dashboard-composer/visual-editor/index.html +17 -0
  108. package/data/templates/dashboard-composer/visual-editor/main.tsx +95 -0
  109. package/data/templates/dashboard-composer/visual-editor/styles.css +310 -0
  110. package/data/templates/database-workspace/DatabaseWorkspaceBlocks.tsx +240 -0
  111. package/data/templates/database-workspace/README.md +5 -0
  112. package/data/templates/database-workspace/database-workspace.css +107 -0
  113. package/data/templates/database-workspace/index.html +14 -0
  114. package/data/templates/database-workspace/main.tsx +247 -0
  115. package/data/templates/database-workspace/styles.css +121 -0
  116. package/data/templates/database-workspace/template.manifest.json +19 -0
  117. package/data/templates/interaction-lab/InteractionWorkbench.tsx +271 -0
  118. package/data/templates/interaction-lab/README.md +7 -0
  119. package/data/templates/interaction-lab/index.html +14 -0
  120. package/data/templates/interaction-lab/interaction-workbench.css +226 -0
  121. package/data/templates/interaction-lab/main.tsx +104 -0
  122. package/data/templates/interaction-lab/styles.css +224 -0
  123. package/data/templates/interaction-lab/template.manifest.json +36 -0
  124. package/lib/api.mjs +72 -5
  125. package/lib/composition-runtime.mjs +382 -0
  126. package/package.json +4 -1
@@ -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,313 @@
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 {
5
+ ProgressBar,
6
+ StatusDot,
7
+ } from '@utopia-studio-design/design-system/DataDisplay'
8
+ import './agent-status.css'
9
+
10
+ export type AgentExecutionState =
11
+ | 'queued'
12
+ | 'running'
13
+ | 'waiting-approval'
14
+ | 'blocked'
15
+ | 'failed'
16
+ | 'completed'
17
+
18
+ export type AgentRisk = 'none' | 'low' | 'medium' | 'high'
19
+ export type AgentStatusDensity = 'compact' | 'default' | 'comfortable'
20
+ export type AgentStatusFilter = 'all' | 'active' | 'attention' | 'completed'
21
+ export type AgentStatusState = 'loading' | 'empty' | 'error' | 'ready'
22
+
23
+ export interface AgentApprovalRequest {
24
+ id: string
25
+ description?: string
26
+ label: string
27
+ }
28
+
29
+ export interface AgentStatusItem {
30
+ id: string
31
+ approval?: AgentApprovalRequest
32
+ name: string
33
+ progress?: number
34
+ risk?: AgentRisk
35
+ state: AgentExecutionState
36
+ summary?: string
37
+ taskLabel: string
38
+ updatedAt?: string
39
+ }
40
+
41
+ export interface AgentStatusLabels {
42
+ approve: string
43
+ approvalRequested: string
44
+ count: (visible: string, total: string) => string
45
+ emptyDescription: string
46
+ emptyTitle: string
47
+ errorDescription: string
48
+ errorTitle: string
49
+ eyebrow: string
50
+ loading: string
51
+ noMatchingDescription: string
52
+ noMatchingTitle: string
53
+ progress: string
54
+ reject: string
55
+ retry: string
56
+ risk: Record<Exclude<AgentRisk, 'none'>, string>
57
+ state: Record<AgentExecutionState, string>
58
+ timeUnavailable: string
59
+ title: string
60
+ updated: string
61
+ }
62
+
63
+ export interface AgentStatusBlockProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
64
+ density?: AgentStatusDensity
65
+ direction?: 'ltr' | 'rtl'
66
+ filter?: AgentStatusFilter
67
+ items?: AgentStatusItem[]
68
+ labels: AgentStatusLabels
69
+ locale?: string
70
+ maxItems?: number
71
+ onApprove?: (item: AgentStatusItem, request: AgentApprovalRequest) => void
72
+ onReject?: (item: AgentStatusItem, request: AgentApprovalRequest) => void
73
+ onRetry?: () => void
74
+ showApprovals?: boolean
75
+ state?: AgentStatusState
76
+ timeZone?: string
77
+ title?: string
78
+ }
79
+
80
+ const attentionStates = new Set<AgentExecutionState>([
81
+ 'waiting-approval',
82
+ 'blocked',
83
+ 'failed',
84
+ ])
85
+
86
+ function filterItems(items: AgentStatusItem[], filter: AgentStatusFilter) {
87
+ if (filter === 'active') {
88
+ return items.filter((item) => item.state === 'queued' || item.state === 'running')
89
+ }
90
+ if (filter === 'attention') {
91
+ return items.filter((item) => attentionStates.has(item.state))
92
+ }
93
+ if (filter === 'completed') {
94
+ return items.filter((item) => item.state === 'completed')
95
+ }
96
+ return items
97
+ }
98
+
99
+ function formatNumber(locale: string, value: number) {
100
+ try {
101
+ return new Intl.NumberFormat(locale).format(value)
102
+ } catch {
103
+ return String(value)
104
+ }
105
+ }
106
+
107
+ function formatPercent(locale: string, value: number) {
108
+ try {
109
+ return new Intl.NumberFormat(locale, {
110
+ maximumFractionDigits: 0,
111
+ style: 'percent',
112
+ }).format(value / 100)
113
+ } catch {
114
+ return `${Math.round(value)}%`
115
+ }
116
+ }
117
+
118
+ function formatUpdatedAt(locale: string, timeZone: string, value?: string) {
119
+ if (!value) return null
120
+ const date = new Date(value)
121
+ if (Number.isNaN(date.valueOf())) return null
122
+ try {
123
+ return {
124
+ date,
125
+ label: new Intl.DateTimeFormat(locale, {
126
+ dateStyle: 'medium',
127
+ timeStyle: 'short',
128
+ timeZone,
129
+ }).format(date),
130
+ }
131
+ } catch {
132
+ return {
133
+ date,
134
+ label: new Intl.DateTimeFormat(locale, {
135
+ dateStyle: 'medium',
136
+ timeStyle: 'short',
137
+ timeZone: 'UTC',
138
+ }).format(date),
139
+ }
140
+ }
141
+ }
142
+
143
+ function statusVariant(state: AgentExecutionState) {
144
+ if (state === 'failed') return 'destructive' as const
145
+ if (state === 'completed') return 'secondary' as const
146
+ return 'outline' as const
147
+ }
148
+
149
+ function progressStatus(state: AgentExecutionState) {
150
+ if (state === 'failed') return 'destructive' as const
151
+ if (state === 'blocked' || state === 'waiting-approval') return 'warning' as const
152
+ if (state === 'completed') return 'success' as const
153
+ return 'default' as const
154
+ }
155
+
156
+ export function AgentStatusBlock({
157
+ density = 'default',
158
+ direction,
159
+ filter = 'all',
160
+ items = [],
161
+ labels,
162
+ locale = 'en-US',
163
+ maxItems = 6,
164
+ onApprove,
165
+ onReject,
166
+ onRetry,
167
+ showApprovals = true,
168
+ state: stateProp,
169
+ timeZone = 'UTC',
170
+ title,
171
+ ...props
172
+ }: AgentStatusBlockProps) {
173
+ const titleId = React.useId()
174
+ const filteredItems = filterItems(items, filter)
175
+ const visibleItems = filteredItems.slice(0, maxItems)
176
+ const state = stateProp ?? (items.length ? 'ready' : 'empty')
177
+ const visibleCount = formatNumber(locale, visibleItems.length)
178
+ const totalCount = formatNumber(locale, filteredItems.length)
179
+
180
+ return (
181
+ <section
182
+ aria-busy={state === 'loading'}
183
+ aria-labelledby={titleId}
184
+ className="ceramic-agent-status"
185
+ data-density={density}
186
+ data-filter={filter}
187
+ data-state={state}
188
+ dir={direction}
189
+ lang={locale}
190
+ {...props}
191
+ >
192
+ <header>
193
+ <div>
194
+ <span>{labels.eyebrow}</span>
195
+ <h2 id={titleId}>{title ?? labels.title}</h2>
196
+ </div>
197
+ {state === 'ready' ? (
198
+ <Badge variant="outline">{labels.count(visibleCount, totalCount)}</Badge>
199
+ ) : null}
200
+ </header>
201
+
202
+ {state === 'loading' ? (
203
+ <div className="ceramic-agent-status-state" role="status">{labels.loading}</div>
204
+ ) : null}
205
+ {state === 'error' ? (
206
+ <div className="ceramic-agent-status-state" role="alert">
207
+ <strong>{labels.errorTitle}</strong>
208
+ <span>{labels.errorDescription}</span>
209
+ {onRetry ? <Button size="sm" variant="outline" onClick={onRetry}>{labels.retry}</Button> : null}
210
+ </div>
211
+ ) : null}
212
+ {state === 'empty' ? (
213
+ <div className="ceramic-agent-status-state" role="status">
214
+ <strong>{labels.emptyTitle}</strong>
215
+ <span>{labels.emptyDescription}</span>
216
+ </div>
217
+ ) : null}
218
+ {state === 'ready' && visibleItems.length === 0 ? (
219
+ <div className="ceramic-agent-status-state" role="status">
220
+ <strong>{labels.noMatchingTitle}</strong>
221
+ <span>{labels.noMatchingDescription}</span>
222
+ </div>
223
+ ) : null}
224
+ {state === 'ready' && visibleItems.length > 0 ? (
225
+ <ul className="ceramic-agent-status-list">
226
+ {visibleItems.map((item) => {
227
+ const progress = Math.max(0, Math.min(100, item.progress ?? (
228
+ item.state === 'completed' ? 100 : 0
229
+ )))
230
+ const updatedAt = formatUpdatedAt(locale, timeZone, item.updatedAt)
231
+ const approvalTitleId = `${titleId}-${item.id}-approval`
232
+ return (
233
+ <li data-execution-state={item.state} key={item.id}>
234
+ <div className="ceramic-agent-status-heading">
235
+ <StatusDot aria-hidden="true" data-execution-state={item.state} />
236
+ <div>
237
+ <strong><bdi>{item.name}</bdi></strong>
238
+ <span><bdi>{item.taskLabel}</bdi></span>
239
+ </div>
240
+ <Badge variant={statusVariant(item.state)}>
241
+ {labels.state[item.state]}
242
+ </Badge>
243
+ </div>
244
+
245
+ {item.summary ? <p><bdi>{item.summary}</bdi></p> : null}
246
+
247
+ {(item.state === 'running' || item.state === 'completed') ? (
248
+ <ProgressBar
249
+ aria-label={`${item.name}: ${labels.progress}`}
250
+ label={labels.progress}
251
+ showValue
252
+ size={density === 'compact' ? 'sm' : 'md'}
253
+ status={progressStatus(item.state)}
254
+ value={progress}
255
+ valueText={formatPercent(locale, progress)}
256
+ />
257
+ ) : null}
258
+
259
+ <div className="ceramic-agent-status-meta">
260
+ {item.risk && item.risk !== 'none' ? (
261
+ <Badge data-risk={item.risk} variant="outline">
262
+ {labels.risk[item.risk]}
263
+ </Badge>
264
+ ) : null}
265
+ {updatedAt ? (
266
+ <span>
267
+ {labels.updated}{' '}
268
+ <time dateTime={updatedAt.date.toISOString()}>{updatedAt.label}</time>
269
+ </span>
270
+ ) : item.updatedAt ? <span>{labels.timeUnavailable}</span> : null}
271
+ </div>
272
+
273
+ {showApprovals && item.state === 'waiting-approval' && item.approval ? (
274
+ <section
275
+ aria-labelledby={approvalTitleId}
276
+ className="ceramic-agent-approval"
277
+ >
278
+ <div>
279
+ <strong id={approvalTitleId}>{labels.approvalRequested}</strong>
280
+ <span>{item.approval.label}</span>
281
+ {item.approval.description ? <p>{item.approval.description}</p> : null}
282
+ </div>
283
+ {onApprove || onReject ? (
284
+ <div>
285
+ {onReject ? (
286
+ <Button
287
+ size="sm"
288
+ variant="outline"
289
+ onClick={() => onReject(item, item.approval!)}
290
+ >
291
+ {labels.reject}
292
+ </Button>
293
+ ) : null}
294
+ {onApprove ? (
295
+ <Button
296
+ size="sm"
297
+ onClick={() => onApprove(item, item.approval!)}
298
+ >
299
+ {labels.approve}
300
+ </Button>
301
+ ) : null}
302
+ </div>
303
+ ) : null}
304
+ </section>
305
+ ) : null}
306
+ </li>
307
+ )
308
+ })}
309
+ </ul>
310
+ ) : null}
311
+ </section>
312
+ )
313
+ }
@@ -0,0 +1,107 @@
1
+ import * as React from 'react'
2
+ import { Button } from '@utopia-studio-design/design-system/Button'
3
+ import {
4
+ ChartRoot,
5
+ type ChartAnnotationDefinition,
6
+ type ChartDatum,
7
+ type ChartSeriesDefinition,
8
+ type ChartState,
9
+ type ChartType,
10
+ } from '@utopia-studio-design/design-system/Charts'
11
+ import './analytics-dashboard.css'
12
+
13
+ export {
14
+ KPIGrid,
15
+ type KPIGridProps,
16
+ type MetricDefinition,
17
+ } from './AnalyticsMetricBlocks'
18
+
19
+ export interface AnalyticsChartCardProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
20
+ annotations?: ChartAnnotationDefinition[]
21
+ data: ChartDatum[]
22
+ description?: React.ReactNode
23
+ direction?: 'ltr' | 'rtl'
24
+ series: ChartSeriesDefinition[]
25
+ showDataTable?: boolean
26
+ state?: ChartState
27
+ title: React.ReactNode
28
+ type?: ChartType
29
+ }
30
+
31
+ export function AnalyticsChartCard({
32
+ annotations = [],
33
+ data,
34
+ description,
35
+ direction,
36
+ series,
37
+ showDataTable,
38
+ state,
39
+ title,
40
+ type,
41
+ ...props
42
+ }: AnalyticsChartCardProps) {
43
+ return (
44
+ <ChartRoot
45
+ accessibleSummary={`${String(title)} across ${data.length} periods. Use the chart explorer or data table for exact values.`}
46
+ annotations={annotations}
47
+ axis={{ showGrid: true }}
48
+ data={data}
49
+ description={description}
50
+ direction={direction}
51
+ series={series}
52
+ showDataTable={showDataTable}
53
+ state={state}
54
+ title={title}
55
+ type={type}
56
+ {...props}
57
+ />
58
+ )
59
+ }
60
+
61
+ export interface ComparisonChartPanelProps extends AnalyticsChartCardProps {
62
+ benchmarkLabel?: string
63
+ benchmarkValue?: number
64
+ }
65
+
66
+ export function ComparisonChartPanel({
67
+ benchmarkLabel = 'Target',
68
+ benchmarkValue,
69
+ annotations = [],
70
+ ...props
71
+ }: ComparisonChartPanelProps) {
72
+ const nextAnnotations = benchmarkValue === undefined
73
+ ? annotations
74
+ : [
75
+ ...annotations,
76
+ { id: 'benchmark', label: benchmarkLabel, value: benchmarkValue, tone: 'neutral' as const },
77
+ ]
78
+ return <AnalyticsChartCard annotations={nextAnnotations} {...props} />
79
+ }
80
+
81
+ export function ChartWithDataTable(props: AnalyticsChartCardProps) {
82
+ return <AnalyticsChartCard {...props} showDataTable />
83
+ }
84
+
85
+ export interface AnalyticsDashboardHeaderProps extends React.HTMLAttributes<HTMLElement> {
86
+ onRefresh?: () => void
87
+ periodLabel: string
88
+ title: string
89
+ }
90
+
91
+ export function AnalyticsDashboardHeader({
92
+ onRefresh,
93
+ periodLabel,
94
+ title,
95
+ ...props
96
+ }: AnalyticsDashboardHeaderProps) {
97
+ return (
98
+ <header className="ceramic-analytics-header" {...props}>
99
+ <div>
100
+ <span>Analytics dashboard</span>
101
+ <h1>{title}</h1>
102
+ <p>{periodLabel}</p>
103
+ </div>
104
+ <Button variant="outline" onClick={onRefresh}>Refresh data</Button>
105
+ </header>
106
+ )
107
+ }