@utopia-studio-design/design-system-cli 0.8.0 → 0.8.1

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 (240) hide show
  1. package/bin/utopia-ds-mcp.mjs +5 -15
  2. package/bin/utopia-ds.mjs +3 -16
  3. package/data/docs/create-a-brandbook.md +11 -1
  4. package/data/manifests/blocks.json +21 -230
  5. package/data/manifests/patterns.json +0 -330
  6. package/lib/api.mjs +6 -193
  7. package/lib/brandbook-validation.mjs +112 -0
  8. package/package.json +2 -2
  9. package/data/blocks/semantic-pattern-parts/AuthParts.tsx +0 -48
  10. package/data/blocks/semantic-pattern-parts/ChoiceCards.tsx +0 -7
  11. package/data/blocks/semantic-pattern-parts/ListFilters.tsx +0 -9
  12. package/data/blocks/semantic-pattern-parts/OutcomeNotice.tsx +0 -7
  13. package/data/blocks/semantic-pattern-parts/PatternParts.tsx +0 -7
  14. package/data/blocks/semantic-pattern-parts/ProductParts.tsx +0 -38
  15. package/data/blocks/semantic-pattern-parts/ProgressSteps.tsx +0 -6
  16. package/data/blocks/semantic-pattern-parts/ReviewList.tsx +0 -7
  17. package/data/blocks/semantic-pattern-parts/ValidatedField.tsx +0 -8
  18. package/data/blocks/semantic-pattern-parts/WorkflowParts.tsx +0 -339
  19. package/data/blocks/semantic-pattern-parts/auth-parts.css +0 -27
  20. package/data/blocks/semantic-pattern-parts/pattern-parts.css +0 -25
  21. package/data/blocks/semantic-pattern-parts/product-parts.css +0 -25
  22. package/data/blocks/semantic-pattern-parts/types.ts +0 -3
  23. package/data/blocks/semantic-pattern-parts/workflow-parts.css +0 -118
  24. package/data/blocks/ux-pattern-examples/AccessExamples.tsx +0 -46
  25. package/data/blocks/ux-pattern-examples/AccountExamples.tsx +0 -133
  26. package/data/blocks/ux-pattern-examples/OnboardingExample.tsx +0 -15
  27. package/data/blocks/ux-pattern-examples/ProductExamples.tsx +0 -79
  28. package/data/blocks/ux-pattern-examples/ReviewExample.tsx +0 -14
  29. package/data/blocks/ux-pattern-examples/SignupExample.tsx +0 -22
  30. package/data/blocks/ux-pattern-examples/WorkflowExample.tsx +0 -1761
  31. package/data/blocks/ux-pattern-examples/WorkflowExamples.tsx +0 -190
  32. package/data/blocks/ux-pattern-examples/copy.ts +0 -6
  33. package/data/blocks/ux-pattern-examples/ux-examples.css +0 -18
  34. package/data/blocks/ux-pattern-examples/workflow-copy.ts +0 -76
  35. package/data/blocks/ux-pattern-examples/workflow-definitions.ts +0 -1291
  36. package/data/docs/ui-parts-qa.md +0 -172
  37. package/data/docs/ui-parts.md +0 -105
  38. package/data/docs/ux-pattern-adaptation-qa.md +0 -38
  39. package/data/docs/ux-pattern-adaptation.md +0 -41
  40. package/data/docs/ux-pattern-migration.md +0 -111
  41. package/data/docs/ux-pattern-release-2026-09-13.md +0 -26
  42. package/data/docs/ux-source-evidence/account-auth.json +0 -41
  43. package/data/docs/ux-source-evidence/account-forgot-password.json +0 -36
  44. package/data/docs/ux-source-evidence/account-invite.json +0 -63
  45. package/data/docs/ux-source-evidence/account-locked.json +0 -63
  46. package/data/docs/ux-source-evidence/account-login.json +0 -28
  47. package/data/docs/ux-source-evidence/account-mfa.json +0 -62
  48. package/data/docs/ux-source-evidence/account-onboarding-credentials.json +0 -52
  49. package/data/docs/ux-source-evidence/account-onboarding-legal.json +0 -56
  50. package/data/docs/ux-source-evidence/account-onboarding-orientation.json +0 -66
  51. package/data/docs/ux-source-evidence/account-onboarding-profile.json +0 -50
  52. package/data/docs/ux-source-evidence/account-onboarding.json +0 -69
  53. package/data/docs/ux-source-evidence/account-reset-password.json +0 -27
  54. package/data/docs/ux-source-evidence/account-session-expired.json +0 -62
  55. package/data/docs/ux-source-evidence/account-signup.json +0 -33
  56. package/data/docs/ux-source-evidence/account-verify-email.json +0 -34
  57. package/data/docs/ux-source-evidence/account-welcome.json +0 -41
  58. package/data/docs/ux-source-evidence/admin-analytics.json +0 -54
  59. package/data/docs/ux-source-evidence/admin-audit.json +0 -31
  60. package/data/docs/ux-source-evidence/admin-broadcasts-new.json +0 -36
  61. package/data/docs/ux-source-evidence/admin-broadcasts.json +0 -41
  62. package/data/docs/ux-source-evidence/admin-closures.json +0 -52
  63. package/data/docs/ux-source-evidence/admin-devices.json +0 -51
  64. package/data/docs/ux-source-evidence/admin-escalations.json +0 -35
  65. package/data/docs/ux-source-evidence/admin-incidents.json +0 -54
  66. package/data/docs/ux-source-evidence/admin-loading.json +0 -17
  67. package/data/docs/ux-source-evidence/admin-onboarding-detail.json +0 -80
  68. package/data/docs/ux-source-evidence/admin-onboarding.json +0 -62
  69. package/data/docs/ux-source-evidence/admin-payments.json +0 -45
  70. package/data/docs/ux-source-evidence/admin-providers.json +0 -38
  71. package/data/docs/ux-source-evidence/admin-settings.json +0 -18
  72. package/data/docs/ux-source-evidence/admin-subscriptions-detail-allocate.json +0 -42
  73. package/data/docs/ux-source-evidence/admin-subscriptions-detail.json +0 -48
  74. package/data/docs/ux-source-evidence/admin-subscriptions-new.json +0 -43
  75. package/data/docs/ux-source-evidence/admin-subscriptions.json +0 -41
  76. package/data/docs/ux-source-evidence/admin-transactions.json +0 -41
  77. package/data/docs/ux-source-evidence/error.json +0 -30
  78. package/data/docs/ux-source-evidence/loading.json +0 -19
  79. package/data/docs/ux-source-evidence/marketing-blog.json +0 -48
  80. package/data/docs/ux-source-evidence/marketing-contact.json +0 -36
  81. package/data/docs/ux-source-evidence/marketing.json +0 -64
  82. package/data/docs/ux-source-evidence/not-found.json +0 -28
  83. package/data/docs/ux-source-evidence/portal-loading.json +0 -17
  84. package/data/docs/ux-source-evidence/portal-mentor-messages.json +0 -31
  85. package/data/docs/ux-source-evidence/portal-mentor-profile.json +0 -23
  86. package/data/docs/ux-source-evidence/portal-mentor-requests-detail.json +0 -54
  87. package/data/docs/ux-source-evidence/portal-mentor-sessions-detail-assess.json +0 -42
  88. package/data/docs/ux-source-evidence/portal-mentor-settings.json +0 -18
  89. package/data/docs/ux-source-evidence/portal-mentor.json +0 -38
  90. package/data/docs/ux-source-evidence/portal-sessions-detail-live.json +0 -48
  91. package/data/docs/ux-source-evidence/portal-sponsor-billing.json +0 -28
  92. package/data/docs/ux-source-evidence/portal-sponsor-profile.json +0 -22
  93. package/data/docs/ux-source-evidence/portal-sponsor-sessions.json +0 -28
  94. package/data/docs/ux-source-evidence/portal-sponsor-settings.json +0 -18
  95. package/data/docs/ux-source-evidence/portal-sponsor-trainees.json +0 -36
  96. package/data/docs/ux-source-evidence/portal-sponsor.json +0 -52
  97. package/data/docs/ux-source-evidence/portal-trainee-competency.json +0 -42
  98. package/data/docs/ux-source-evidence/portal-trainee-credits.json +0 -40
  99. package/data/docs/ux-source-evidence/portal-trainee-mentors.json +0 -48
  100. package/data/docs/ux-source-evidence/portal-trainee-messages.json +0 -32
  101. package/data/docs/ux-source-evidence/portal-trainee-notifications.json +0 -35
  102. package/data/docs/ux-source-evidence/portal-trainee-profile.json +0 -26
  103. package/data/docs/ux-source-evidence/portal-trainee-request.json +0 -54
  104. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +0 -36
  105. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +0 -42
  106. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail.json +0 -55
  107. package/data/docs/ux-source-evidence/portal-trainee-sessions.json +0 -36
  108. package/data/docs/ux-source-evidence/portal-trainee-settings-notifications.json +0 -38
  109. package/data/docs/ux-source-evidence/portal-trainee-settings.json +0 -18
  110. package/data/docs/ux-source-evidence/portal-trainee.json +0 -44
  111. package/data/docs/ux-source-evidence/portal-tv-detail.json +0 -39
  112. package/data/docs/ux-source-evidence/portal-tv.json +0 -44
  113. package/data/docs/ux-source-evidence/shared-admin-admin-shell.json +0 -35
  114. package/data/docs/ux-source-evidence/shared-auth-auth-primitives.json +0 -105
  115. package/data/docs/ux-source-evidence/shared-auth-auth-shell.json +0 -25
  116. package/data/docs/ux-source-evidence/shared-auth-onboarding-primitives.json +0 -52
  117. package/data/docs/ux-source-evidence/shared-auth-onboarding-shell.json +0 -28
  118. package/data/docs/ux-source-evidence/shared-auth-status-indicator.json +0 -41
  119. package/data/docs/ux-source-evidence/shared-portal-empty-state.json +0 -24
  120. package/data/docs/ux-source-evidence/shared-portal-loading-skeleton.json +0 -27
  121. package/data/docs/ux-source-evidence/shared-portal-notifications-dropdown.json +0 -39
  122. package/data/docs/ux-source-evidence/shared-portal-portal-shell.json +0 -36
  123. package/data/docs/ux-source-evidence/shared-portal-profile-form.json +0 -32
  124. package/data/docs/ux-source-evidence/shared-portal-settings-view.json +0 -81
  125. package/data/docs/ux-source-evidence/shared-portal-usage-sparkline.json +0 -18
  126. package/data/docs/ux-source-evidence/shared-portal-user-menu.json +0 -34
  127. package/data/manifests/ui-patterns.json +0 -1029
  128. package/data/manifests/ux-examples.json +0 -2177
  129. package/data/manifests/ux-patterns.json +0 -1135
  130. package/data/manifests/ux-source-inventory.json +0 -4669
  131. package/data/ui-parts/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +0 -87
  132. package/data/ui-parts/blocks/analytics-dashboard/analytics-dashboard.css +0 -92
  133. package/data/ui-parts/blocks/semantic-pattern-parts/AuthParts.tsx +0 -48
  134. package/data/ui-parts/blocks/semantic-pattern-parts/ChoiceCards.tsx +0 -7
  135. package/data/ui-parts/blocks/semantic-pattern-parts/ListFilters.tsx +0 -9
  136. package/data/ui-parts/blocks/semantic-pattern-parts/OutcomeNotice.tsx +0 -7
  137. package/data/ui-parts/blocks/semantic-pattern-parts/ProductParts.tsx +0 -38
  138. package/data/ui-parts/blocks/semantic-pattern-parts/ProgressSteps.tsx +0 -6
  139. package/data/ui-parts/blocks/semantic-pattern-parts/ReviewList.tsx +0 -7
  140. package/data/ui-parts/blocks/semantic-pattern-parts/ValidatedField.tsx +0 -8
  141. package/data/ui-parts/blocks/semantic-pattern-parts/WorkflowParts.tsx +0 -339
  142. package/data/ui-parts/blocks/semantic-pattern-parts/auth-parts.css +0 -27
  143. package/data/ui-parts/blocks/semantic-pattern-parts/pattern-parts.css +0 -25
  144. package/data/ui-parts/blocks/semantic-pattern-parts/product-parts.css +0 -25
  145. package/data/ui-parts/blocks/semantic-pattern-parts/types.ts +0 -3
  146. package/data/ui-parts/blocks/semantic-pattern-parts/workflow-parts.css +0 -118
  147. package/data/ui-parts/blocks/ux-pattern-examples/AccessExamples.tsx +0 -46
  148. package/data/ui-parts/blocks/ux-pattern-examples/AccountExamples.tsx +0 -133
  149. package/data/ui-parts/blocks/ux-pattern-examples/ProductExamples.tsx +0 -79
  150. package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExample.tsx +0 -1761
  151. package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExamples.tsx +0 -190
  152. package/data/ui-parts/blocks/ux-pattern-examples/copy.ts +0 -6
  153. package/data/ui-parts/blocks/ux-pattern-examples/ux-examples.css +0 -18
  154. package/data/ui-parts/blocks/ux-pattern-examples/workflow-copy.ts +0 -76
  155. package/data/ui-parts/blocks/ux-pattern-examples/workflow-definitions.ts +0 -1291
  156. package/data/ux-source-evidence/account-auth.json +0 -41
  157. package/data/ux-source-evidence/account-forgot-password.json +0 -36
  158. package/data/ux-source-evidence/account-invite.json +0 -63
  159. package/data/ux-source-evidence/account-locked.json +0 -63
  160. package/data/ux-source-evidence/account-login.json +0 -28
  161. package/data/ux-source-evidence/account-mfa.json +0 -62
  162. package/data/ux-source-evidence/account-onboarding-credentials.json +0 -52
  163. package/data/ux-source-evidence/account-onboarding-legal.json +0 -56
  164. package/data/ux-source-evidence/account-onboarding-orientation.json +0 -66
  165. package/data/ux-source-evidence/account-onboarding-profile.json +0 -50
  166. package/data/ux-source-evidence/account-onboarding.json +0 -69
  167. package/data/ux-source-evidence/account-reset-password.json +0 -27
  168. package/data/ux-source-evidence/account-session-expired.json +0 -62
  169. package/data/ux-source-evidence/account-signup.json +0 -33
  170. package/data/ux-source-evidence/account-verify-email.json +0 -34
  171. package/data/ux-source-evidence/account-welcome.json +0 -41
  172. package/data/ux-source-evidence/admin-analytics.json +0 -54
  173. package/data/ux-source-evidence/admin-audit.json +0 -31
  174. package/data/ux-source-evidence/admin-broadcasts-new.json +0 -36
  175. package/data/ux-source-evidence/admin-broadcasts.json +0 -41
  176. package/data/ux-source-evidence/admin-closures.json +0 -52
  177. package/data/ux-source-evidence/admin-devices.json +0 -51
  178. package/data/ux-source-evidence/admin-escalations.json +0 -35
  179. package/data/ux-source-evidence/admin-incidents.json +0 -54
  180. package/data/ux-source-evidence/admin-loading.json +0 -17
  181. package/data/ux-source-evidence/admin-onboarding-detail.json +0 -80
  182. package/data/ux-source-evidence/admin-onboarding.json +0 -62
  183. package/data/ux-source-evidence/admin-payments.json +0 -45
  184. package/data/ux-source-evidence/admin-providers.json +0 -38
  185. package/data/ux-source-evidence/admin-settings.json +0 -18
  186. package/data/ux-source-evidence/admin-subscriptions-detail-allocate.json +0 -42
  187. package/data/ux-source-evidence/admin-subscriptions-detail.json +0 -48
  188. package/data/ux-source-evidence/admin-subscriptions-new.json +0 -43
  189. package/data/ux-source-evidence/admin-subscriptions.json +0 -41
  190. package/data/ux-source-evidence/admin-transactions.json +0 -41
  191. package/data/ux-source-evidence/error.json +0 -30
  192. package/data/ux-source-evidence/loading.json +0 -19
  193. package/data/ux-source-evidence/marketing-blog.json +0 -48
  194. package/data/ux-source-evidence/marketing-contact.json +0 -36
  195. package/data/ux-source-evidence/marketing.json +0 -64
  196. package/data/ux-source-evidence/not-found.json +0 -28
  197. package/data/ux-source-evidence/portal-loading.json +0 -17
  198. package/data/ux-source-evidence/portal-mentor-messages.json +0 -31
  199. package/data/ux-source-evidence/portal-mentor-profile.json +0 -23
  200. package/data/ux-source-evidence/portal-mentor-requests-detail.json +0 -54
  201. package/data/ux-source-evidence/portal-mentor-sessions-detail-assess.json +0 -42
  202. package/data/ux-source-evidence/portal-mentor-settings.json +0 -18
  203. package/data/ux-source-evidence/portal-mentor.json +0 -38
  204. package/data/ux-source-evidence/portal-sessions-detail-live.json +0 -48
  205. package/data/ux-source-evidence/portal-sponsor-billing.json +0 -28
  206. package/data/ux-source-evidence/portal-sponsor-profile.json +0 -22
  207. package/data/ux-source-evidence/portal-sponsor-sessions.json +0 -28
  208. package/data/ux-source-evidence/portal-sponsor-settings.json +0 -18
  209. package/data/ux-source-evidence/portal-sponsor-trainees.json +0 -36
  210. package/data/ux-source-evidence/portal-sponsor.json +0 -52
  211. package/data/ux-source-evidence/portal-trainee-competency.json +0 -42
  212. package/data/ux-source-evidence/portal-trainee-credits.json +0 -40
  213. package/data/ux-source-evidence/portal-trainee-mentors.json +0 -48
  214. package/data/ux-source-evidence/portal-trainee-messages.json +0 -32
  215. package/data/ux-source-evidence/portal-trainee-notifications.json +0 -35
  216. package/data/ux-source-evidence/portal-trainee-profile.json +0 -26
  217. package/data/ux-source-evidence/portal-trainee-request.json +0 -54
  218. package/data/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +0 -36
  219. package/data/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +0 -42
  220. package/data/ux-source-evidence/portal-trainee-sessions-detail.json +0 -55
  221. package/data/ux-source-evidence/portal-trainee-sessions.json +0 -36
  222. package/data/ux-source-evidence/portal-trainee-settings-notifications.json +0 -38
  223. package/data/ux-source-evidence/portal-trainee-settings.json +0 -18
  224. package/data/ux-source-evidence/portal-trainee.json +0 -44
  225. package/data/ux-source-evidence/portal-tv-detail.json +0 -39
  226. package/data/ux-source-evidence/portal-tv.json +0 -44
  227. package/data/ux-source-evidence/shared-admin-admin-shell.json +0 -35
  228. package/data/ux-source-evidence/shared-auth-auth-primitives.json +0 -105
  229. package/data/ux-source-evidence/shared-auth-auth-shell.json +0 -25
  230. package/data/ux-source-evidence/shared-auth-onboarding-primitives.json +0 -52
  231. package/data/ux-source-evidence/shared-auth-onboarding-shell.json +0 -28
  232. package/data/ux-source-evidence/shared-auth-status-indicator.json +0 -41
  233. package/data/ux-source-evidence/shared-portal-empty-state.json +0 -24
  234. package/data/ux-source-evidence/shared-portal-loading-skeleton.json +0 -27
  235. package/data/ux-source-evidence/shared-portal-notifications-dropdown.json +0 -39
  236. package/data/ux-source-evidence/shared-portal-portal-shell.json +0 -36
  237. package/data/ux-source-evidence/shared-portal-profile-form.json +0 -32
  238. package/data/ux-source-evidence/shared-portal-settings-view.json +0 -81
  239. package/data/ux-source-evidence/shared-portal-usage-sparkline.json +0 -18
  240. package/data/ux-source-evidence/shared-portal-user-menu.json +0 -34
@@ -1,87 +0,0 @@
1
- import * as React from 'react'
2
- import { Badge } from '@utopia-studio-design/design-system/Badge'
3
- import type { ChartState } from '@utopia-studio-design/design-system/Charts'
4
- import './analytics-dashboard.css'
5
-
6
- export interface MetricDefinition {
7
- change?: number
8
- changeLabel?: string
9
- format?: 'number' | 'percent' | 'currency'
10
- id: string
11
- label: string
12
- value: number
13
- }
14
-
15
- export interface KPIGridProps extends React.HTMLAttributes<HTMLDivElement> {
16
- currency?: string
17
- density?: 'compact' | 'default' | 'comfortable'
18
- locale?: string
19
- metrics: MetricDefinition[]
20
- state?: ChartState
21
- }
22
-
23
- export function KPIGrid({
24
- className,
25
- currency = 'USD',
26
- density = 'default',
27
- locale,
28
- metrics,
29
- state = metrics.length === 0 ? 'empty' : 'ready',
30
- ...props
31
- }: KPIGridProps) {
32
- const formatMetric = (metric: MetricDefinition) => {
33
- if (metric.format === 'percent') {
34
- return new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 1 }).format(metric.value)
35
- }
36
- if (metric.format === 'currency') {
37
- return new Intl.NumberFormat(locale, {
38
- style: 'currency',
39
- currency,
40
- notation: 'compact',
41
- maximumFractionDigits: 1,
42
- }).format(metric.value)
43
- }
44
- return new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: 1 }).format(metric.value)
45
- }
46
-
47
- return (
48
- <section
49
- aria-busy={state === 'loading'}
50
- aria-label="Key performance indicators"
51
- className={['ceramic-kpi-grid', className].filter(Boolean).join(' ')}
52
- data-density={density}
53
- data-state={state}
54
- {...props}
55
- >
56
- {state === 'loading' ? (
57
- <>
58
- {[0, 1, 2].map((item) => <span aria-hidden="true" className="ceramic-kpi-skeleton" key={item} />)}
59
- <span className="uds-visually-hidden" role="status">Loading metrics…</span>
60
- </>
61
- ) : null}
62
- {state === 'empty' ? (
63
- <div className="ceramic-kpi-state" role="status">
64
- <strong>No metrics available</strong>
65
- <span>Choose another period or data source.</span>
66
- </div>
67
- ) : null}
68
- {state === 'error' ? (
69
- <div className="ceramic-kpi-state" role="alert">
70
- <strong>Metrics unavailable</strong>
71
- <span>Reconnect the data source and try again.</span>
72
- </div>
73
- ) : null}
74
- {state === 'ready' || state === 'partial' ? metrics.map((metric) => (
75
- <article className="ceramic-kpi" key={metric.id}>
76
- <span>{metric.label}</span>
77
- <strong>{formatMetric(metric)}</strong>
78
- {metric.change !== undefined ? (
79
- <Badge variant="outline">
80
- {metric.change > 0 ? '+' : ''}{metric.change.toFixed(1)}% {metric.changeLabel ?? 'vs previous'}
81
- </Badge>
82
- ) : <span className="ceramic-kpi-unavailable">No comparison</span>}
83
- </article>
84
- )) : null}
85
- </section>
86
- )
87
- }
@@ -1,92 +0,0 @@
1
- .ceramic-kpi-grid {
2
- display: grid;
3
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
4
- gap: var(--interaction-density-default);
5
- min-inline-size: 0;
6
- }
7
-
8
- .ceramic-kpi-grid[data-density='compact'] {
9
- gap: var(--interaction-density-compact);
10
- }
11
-
12
- .ceramic-kpi-grid[data-density='comfortable'] {
13
- gap: var(--interaction-density-comfortable);
14
- }
15
-
16
- .ceramic-kpi {
17
- display: grid;
18
- align-content: start;
19
- gap: var(--space-2);
20
- min-inline-size: 0;
21
- padding: var(--space-4);
22
- border: var(--border-width-hairline) solid var(--border);
23
- background: var(--surface);
24
- }
25
-
26
- .ceramic-kpi > span:first-child,
27
- .ceramic-kpi-unavailable {
28
- color: var(--muted-foreground);
29
- font-size: var(--font-size-small);
30
- }
31
-
32
- .ceramic-kpi > strong {
33
- font-size: var(--font-size-marketing-h3);
34
- font-weight: var(--font-weight-medium);
35
- line-height: var(--line-height-tight);
36
- }
37
-
38
- .ceramic-kpi .uds-badge {
39
- inline-size: fit-content;
40
- }
41
-
42
- .ceramic-kpi-state {
43
- display: grid;
44
- grid-column: 1 / -1;
45
- justify-items: center;
46
- gap: var(--space-2);
47
- min-block-size: 10rem;
48
- padding: var(--space-6);
49
- border: var(--border-width-hairline) dashed var(--border);
50
- color: var(--muted-foreground);
51
- text-align: center;
52
- }
53
-
54
- .ceramic-kpi-state strong {
55
- color: var(--foreground);
56
- }
57
-
58
- .ceramic-kpi-skeleton {
59
- min-block-size: 9rem;
60
- background: var(--surface-strong);
61
- }
62
-
63
- .ceramic-analytics-header {
64
- display: flex;
65
- align-items: end;
66
- justify-content: space-between;
67
- gap: var(--space-6);
68
- padding-block: var(--space-6);
69
- border-block-end: var(--border-width-hairline) solid var(--border);
70
- }
71
-
72
- .ceramic-analytics-header > div {
73
- display: grid;
74
- gap: var(--space-1);
75
- }
76
-
77
- .ceramic-analytics-header :is(h1, p) {
78
- margin: 0;
79
- }
80
-
81
- .ceramic-analytics-header > div > span,
82
- .ceramic-analytics-header p {
83
- color: var(--muted-foreground);
84
- font-size: var(--font-size-small);
85
- }
86
-
87
- @media (max-width: 44rem) {
88
- .ceramic-analytics-header {
89
- align-items: stretch;
90
- flex-direction: column;
91
- }
92
- }
@@ -1,48 +0,0 @@
1
- import { useId, useRef, useState, type ReactNode } from 'react'
2
- import { Button, TextInput } from '@utopia-studio-design/design-system'
3
- import { Eye, EyeOff } from 'lucide-react'
4
- import './auth-parts.css'
5
-
6
- /** Host-owned policy and labels: this meter is not a server password policy. */
7
- export function PasswordField({ label, value, onChange, showLabel, hideLabel, rules = [], error, hint }: {
8
- label: string; value: string; onChange: (value: string) => void; showLabel: string; hideLabel: string;
9
- rules?: { id: string; label: string; passed: boolean }[]; error?: string; hint?: string;
10
- }) {
11
- const id = useId(), [shown, setShown] = useState(false)
12
- return <div className="auth-part-field" data-slot="password-field">
13
- <label htmlFor={id}>{label}</label>
14
- <div className="auth-part-password"><TextInput id={id} type={shown ? 'text' : 'password'} value={value} onChange={e => onChange(e.target.value)} autoComplete="off" aria-invalid={Boolean(error)} aria-describedby={error || hint ? id + '-help' : undefined} /><Button type="button" variant="ghost" size="icon" onClick={() => setShown(v => !v)} aria-label={shown ? hideLabel : showLabel} aria-pressed={shown}>{shown ? <EyeOff aria-hidden="true" /> : <Eye aria-hidden="true" />}</Button></div>
15
- {error || hint ? <p id={id + '-help'} data-error={Boolean(error)}>{error || hint}</p> : null}
16
- {value && rules.length ? <ul className="auth-part-rules">{rules.map(rule => <li key={rule.id} data-passed={rule.passed}><span aria-hidden="true">{rule.passed ? '✓' : '○'}</span> {rule.label}</li>)}</ul> : null}
17
- </div>
18
- }
19
-
20
- /** Six slots, paste, backspace and arrow movement; verification stays with host. */
21
- export function VerificationCode({ label, digitLabel, value, onChange, error, disabled = false }: {
22
- label: string; digitLabel: (index: number) => string; value: string; onChange: (value: string) => void; error?: string; disabled?: boolean;
23
- }) {
24
- const inputs = useRef<(HTMLInputElement | null)[]>([]), id = useId()
25
- const update = (index: number, incoming: string) => {
26
- const digits = incoming.replace(/\D/g, '').slice(0, 6 - index)
27
- const slots = value.padEnd(6, ' ').split('')
28
- slots.splice(index, Math.max(1, digits.length), ...(digits || ' ').split(''))
29
- onChange(slots.slice(0, 6).join('').trimEnd())
30
- if (digits) inputs.current[Math.min(index + digits.length, 5)]?.focus()
31
- }
32
- return <fieldset className="auth-part-code" data-slot="verification-code" aria-describedby={error ? id : undefined}>
33
- <legend>{label}</legend><div dir="ltr">{Array.from({ length: 6 }, (_, index) => <TextInput key={index} ref={el => { inputs.current[index] = el }} aria-label={digitLabel(index + 1)} aria-invalid={Boolean(error)} aria-describedby={error ? id : undefined} inputMode="numeric" autoComplete={index === 0 ? 'one-time-code' : 'off'} value={value[index] ?? ''} disabled={disabled}
34
- onChange={e => update(index, e.target.value)} onFocus={e => e.currentTarget.select()} onPaste={e => { e.preventDefault(); update(index, e.clipboardData.getData('text')) }}
35
- onKeyDown={e => { if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { e.preventDefault(); inputs.current[Math.max(0, Math.min(5, index + (e.key === 'ArrowLeft' ? -1 : 1)))]?.focus() } else if (e.key === 'Backspace' && !value[index] && index) { e.preventDefault(); onChange(value.slice(0, index - 1)); inputs.current[index - 1]?.focus() } }} />)}</div>
36
- {error ? <p id={id} role="alert">{error}</p> : null}
37
- </fieldset>
38
- }
39
-
40
- export function ProviderButtons({ providers, onSelect, pending, label }: {
41
- providers: { id: string; label: string }[]; onSelect: (id: string) => void; pending?: string; label: string;
42
- }) {
43
- return <div className="auth-part-providers" data-slot="provider-sign-in" role="group" aria-label={label}>{providers.map(provider => <Button key={provider.id} type="button" variant="outline" disabled={Boolean(pending)} aria-busy={pending === provider.id} onClick={() => onSelect(provider.id)}>{provider.label}</Button>)}</div>
44
- }
45
-
46
- export function AuthFrame({ title, subtitle, footer, children, locale = 'en' }: { title: string; subtitle?: string; footer: string; children: ReactNode; locale?: string }) {
47
- return <section className="ux-example ux-example--account" lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}><div className="ux-example__center"><div className="auth-part-frame"><header><h3>{title}</h3>{subtitle ? <p>{subtitle}</p> : null}</header>{children}</div></div><footer>{footer}</footer></section>
48
- }
@@ -1,7 +0,0 @@
1
- import { Button } from '@utopia-studio-design/design-system'
2
- import type { PatternOption } from './types'
3
- import './pattern-parts.css'
4
-
5
- export function ChoiceCards({ label, options, value, onChange }: { label: string; options: PatternOption[]; value: string; onChange: (id: string) => void }) {
6
- return <fieldset className="pattern-parts pattern-choices" data-slot="pattern-choices"><legend>{label}</legend><div>{options.map(option => <Button key={option.id} type="button" variant="outline" aria-pressed={value === option.id} onClick={() => onChange(option.id)}><strong>{option.label}</strong>{option.description ? <span>{option.description}</span> : null}</Button>)}</div></fieldset>
7
- }
@@ -1,9 +0,0 @@
1
- import { useId } from 'react'
2
- import { Button, TextInput, NativeSelect } from '@utopia-studio-design/design-system'
3
- import type { PatternOption } from './types'
4
- import './pattern-parts.css'
5
-
6
- export function ListFilters({ label, query, onQueryChange, queryLabel, status, onStatusChange, statusLabel, options, resetLabel }: { label: string; query: string; onQueryChange: (query: string) => void; queryLabel: string; status: string; onStatusChange: (status: string) => void; statusLabel: string; options: PatternOption[]; resetLabel: string }) {
7
- const id = useId()
8
- return <section className="pattern-parts pattern-filters" data-slot="pattern-filters" aria-label={label}><div><label htmlFor={`${id}-query`}>{queryLabel}</label><TextInput id={`${id}-query`} type="search" value={query} onChange={event => onQueryChange(event.target.value)} /></div><div><label htmlFor={`${id}-status`}>{statusLabel}</label><NativeSelect id={`${id}-status`} value={status} onChange={event => onStatusChange(event.target.value)}>{options.map(option => <option value={option.id} key={option.id}>{option.label}</option>)}</NativeSelect></div><Button type="button" variant="outline" onClick={() => { onQueryChange(''); onStatusChange(options[0]?.id ?? '') }}>{resetLabel}</Button></section>
9
- }
@@ -1,7 +0,0 @@
1
- import type { ReactNode } from 'react'
2
- import { Button } from '@utopia-studio-design/design-system'
3
- import './pattern-parts.css'
4
-
5
- export function OutcomeNotice({ title, children, actionLabel, onAction, tone = 'info' }: { title: string; children: ReactNode; actionLabel: string; onAction: () => void; tone?: 'info' | 'success' | 'warning' }) {
6
- return <section className="pattern-parts pattern-result" data-slot="pattern-result" data-tone={tone}><div role="status"><strong>{title}</strong><p>{children}</p></div><Button type="button" variant="outline" onClick={onAction}>{actionLabel}</Button></section>
7
- }
@@ -1,38 +0,0 @@
1
- import { Button, NativeSelect } from '@utopia-studio-design/design-system'
2
- import './product-parts.css'
3
-
4
- /** Rows represent destinations; the host supplies navigation, not this part. */
5
- export function ActivityList({ items, emptyLabel, unreadLabel, onOpen }: {
6
- items: { id: string; title: string; body: string; time: string; unread?: boolean }[];
7
- emptyLabel: string; unreadLabel: string; onOpen: (id: string) => void;
8
- }) {
9
- if (!items.length) return <p className="product-card" role="status">{emptyLabel}</p>
10
- return <ul className="product-list" data-slot="activity-list">{items.map(item => <li key={item.id}>
11
- <button className="product-row" data-slot="activity-row" data-unread={Boolean(item.unread)} onClick={() => onOpen(item.id)} type="button">
12
- <span className="product-row__content"><strong>{item.title}</strong><span>{item.body}</span></span>
13
- <span className="product-row__meta"><span>{item.time}</span>{item.unread ? <strong>{unreadLabel}</strong> : null}</span>
14
- </button>
15
- </li>)}</ul>
16
- }
17
-
18
- /** Selected channels and mandatory-delivery policy belong to the host. */
19
- export function ChannelPreferences({ items, options, requiredLabel, onChange }: {
20
- items: { id: string; title: string; description: string; channel: string; disallowed?: string[] }[];
21
- options: { id: string; label: string }[]; requiredLabel: string; onChange: (id: string, channel: string) => void;
22
- }) {
23
- return <div className="product-list" data-slot="channel-preferences">{items.map(item => <div className="product-setting" key={item.id}>
24
- <div><strong>{item.title}</strong><p>{item.description}</p>{item.disallowed?.length ? <small>{requiredLabel}</small> : null}</div>
25
- <NativeSelect aria-label={item.title} value={item.channel} onChange={e => onChange(item.id, e.target.value)}>{options.map(option => <option key={option.id} value={option.id} disabled={item.disallowed?.includes(option.id)}>{option.label}</option>)}</NativeSelect>
26
- </div>)}</div>
27
- }
28
-
29
- export function DirectoryCards({ items, emptyLabel, profileLabel, actionLabel, onProfile, onAction }: {
30
- items: { id: string; name: string; role: string; organization: string; languages: string; expertise: string; availability: string; stats: string }[];
31
- emptyLabel: string; profileLabel: string; actionLabel: string; onProfile: (id: string) => void; onAction: (id: string) => void;
32
- }) {
33
- if (!items.length) return <p className="product-card" role="status">{emptyLabel}</p>
34
- return <ul className="product-grid" data-slot="directory-cards">{items.map(item => <li className="product-card" data-slot="directory-card" key={item.id}>
35
- <header><strong>{item.name}</strong><p>{item.role}</p></header><span>{item.availability}</span><p>{item.stats}</p><p>{item.organization} · {item.languages}</p><p>{item.expertise}</p>
36
- <div className="product-actions"><Button onClick={() => onAction(item.id)}>{actionLabel}</Button><Button variant="outline" onClick={() => onProfile(item.id)}>{profileLabel}</Button></div>
37
- </li>)}</ul>
38
- }
@@ -1,6 +0,0 @@
1
- import { Button } from '@utopia-studio-design/design-system'
2
- import './pattern-parts.css'
3
-
4
- export function ProgressSteps({ label, steps, onSelect }: { label: string; steps: { id: string; label: string; status: 'done' | 'current' | 'locked'; statusLabel: string }[]; onSelect: (id: string) => void }) {
5
- return <nav className="pattern-parts pattern-steps" data-slot="pattern-steps" aria-label={label}><ol>{steps.map((step, index) => <li key={step.id}><Button variant={step.status === 'current' ? 'default' : 'outline'} disabled={step.status === 'locked'} aria-current={step.status === 'current' ? 'step' : undefined} onClick={() => onSelect(step.id)}><span aria-hidden="true">{step.status === 'done' ? '✓' : index + 1}</span><strong>{step.label}</strong><span>{step.statusLabel}</span></Button></li>)}</ol></nav>
6
- }
@@ -1,7 +0,0 @@
1
- import { Button, Badge, DataTable, DataTableShell, DataTableHeader, DataTableBody, DataTableRow, DataTableHead, DataTableCell } from '@utopia-studio-design/design-system'
2
- import type { ReviewItem } from './types'
3
- import './pattern-parts.css'
4
-
5
- export function ReviewList({ label, rows, columns, state = 'ready', message, retryLabel, onRetry, onReview, reviewLabel }: { label: string; rows: ReviewItem[]; columns: [string, string, string, string]; state?: 'loading' | 'error' | 'ready'; message: string; retryLabel: string; onRetry: () => void; onReview: (id: string) => void; reviewLabel: (row: ReviewItem) => string }) {
6
- return <section className="pattern-parts pattern-review" data-slot="pattern-review" aria-label={label} aria-busy={state === 'loading'}>{state !== 'ready' ? <div className="pattern-result"><p role={state === 'error' ? 'alert' : 'status'}>{message}</p>{state === 'error' ? <Button variant="outline" onClick={onRetry}>{retryLabel}</Button> : null}</div> : <DataTableShell><DataTable aria-label={label}><DataTableHeader><DataTableRow>{columns.map(column => <DataTableHead key={column}>{column}</DataTableHead>)}</DataTableRow></DataTableHeader><DataTableBody>{rows.map(row => <DataTableRow key={row.id}><DataTableCell><strong>{row.title}</strong>{row.description ? <p>{row.description}</p> : null}</DataTableCell><DataTableCell><Badge variant="secondary">{row.status}</Badge></DataTableCell><DataTableCell>{row.assignee}</DataTableCell><DataTableCell><Button variant="outline" size="sm" onClick={() => onReview(row.id)}>{reviewLabel(row)}</Button></DataTableCell></DataTableRow>)}{!rows.length ? <DataTableRow><DataTableCell colSpan={4}><p role="status">{message}</p></DataTableCell></DataTableRow> : null}</DataTableBody></DataTable></DataTableShell>}</section>
7
- }
@@ -1,8 +0,0 @@
1
- import { useId } from 'react'
2
- import { TextInput } from '@utopia-studio-design/design-system'
3
- import './pattern-parts.css'
4
-
5
- export function ValidatedField({ label, value, onChange, hint, error, type = 'text', required = false }: { label: string; value: string; onChange: (value: string) => void; hint?: string; error?: string; type?: 'text' | 'email'; required?: boolean }) {
6
- const id = useId()
7
- return <div className="pattern-parts pattern-field" data-slot="pattern-field"><label htmlFor={id}>{label}</label><TextInput id={id} type={type} value={value} onChange={event => onChange(event.target.value)} required={required} aria-invalid={Boolean(error)} aria-describedby={error || hint ? `${id}-help` : undefined} />{error || hint ? <p id={`${id}-help`} data-error={Boolean(error)}>{error || hint}</p> : null}</div>
8
- }
@@ -1,339 +0,0 @@
1
- import { useId } from "react";
2
- import {
3
- Button,
4
- TextInput,
5
- TextArea,
6
- NativeSelect,
7
- } from "@utopia-studio-design/design-system";
8
- import "./workflow-parts.css";
9
- import "./product-parts.css";
10
-
11
- export type WorkflowField = {
12
- id: string;
13
- label: string;
14
- type?:
15
- | "text"
16
- | "email"
17
- | "date"
18
- | "time"
19
- | "number"
20
- | "url"
21
- | "textarea"
22
- | "select"
23
- | "multi-select"
24
- | "check"
25
- | "file";
26
- options?: string[];
27
- required?: boolean;
28
- minLength?: number;
29
- min?: number;
30
- max?: number;
31
- accept?: string;
32
- hint?: string;
33
- };
34
-
35
- /** Pure field rendering. Validation, uploaded file handling and persistence belong to the host. */
36
- export function WorkflowFields({
37
- fields,
38
- values,
39
- errors = {},
40
- onChange,
41
- onFile,
42
- }: {
43
- fields: WorkflowField[];
44
- values: Record<string, string>;
45
- errors?: Record<string, string>;
46
- onChange: (id: string, value: string) => void;
47
- onFile?: (id: string, file: File | undefined) => void;
48
- }) {
49
- const prefix = useId();
50
- return (
51
- <div className="workflow-fields" data-slot="workflow-fields">
52
- {fields.map((field) => {
53
- const id = `${prefix}-${field.id}`,
54
- error = errors[field.id];
55
- const shared = {
56
- id,
57
- required: field.required,
58
- "aria-invalid": Boolean(error),
59
- "aria-describedby": `${id}-help`,
60
- value: values[field.id] ?? "",
61
- onChange: (
62
- e: React.ChangeEvent<
63
- HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
64
- >
65
- ) => onChange(field.id, e.target.value),
66
- };
67
- if (field.type === 'multi-select') {
68
- const selected = (values[field.id] ?? '').split('\n').filter(Boolean)
69
- return <fieldset className="product-card" data-slot="workflow-multi-select" key={field.id} aria-describedby={`${id}-help`} aria-invalid={Boolean(error)}>
70
- <legend>{field.label}{field.required ? ' *' : ''}</legend>
71
- <div className="workflow-fields">{field.options?.map(option => <label className="ux-example__check" key={option}><input type="checkbox" checked={selected.includes(option)} onChange={e => onChange(field.id, (e.target.checked ? [...selected, option] : selected.filter(v=>v!==option)).join('\n'))} />{option}</label>)}</div>
72
- <small id={`${id}-help`} data-error={Boolean(error)}>{error || field.hint}</small>
73
- </fieldset>
74
- }
75
- return (
76
- <div className="workflow-field" key={field.id}>
77
- <label htmlFor={id}>
78
- {field.label}
79
- {field.required ? " *" : ""}
80
- </label>
81
- {field.type === "check" ? (
82
- <input
83
- id={id}
84
- type="checkbox"
85
- checked={values[field.id] === "true"}
86
- required={field.required}
87
- aria-invalid={Boolean(error)}
88
- aria-describedby={`${id}-help`}
89
- onChange={(e) => onChange(field.id, String(e.target.checked))}
90
- />
91
- ) : field.type === "file" ? (
92
- <input
93
- id={id}
94
- type="file"
95
- accept={field.accept}
96
- aria-invalid={Boolean(error)}
97
- aria-describedby={`${id}-help`}
98
- onChange={(e) => onFile?.(field.id, e.target.files?.[0])}
99
- />
100
- ) : field.type === "textarea" ? (
101
- <TextArea {...shared} minLength={field.minLength} rows={4} />
102
- ) : field.type === "select" ? (
103
- <NativeSelect {...shared}>
104
- <option value="">—</option>
105
- {field.options?.map((option) => (
106
- <option key={option}>{option}</option>
107
- ))}
108
- </NativeSelect>
109
- ) : (
110
- <TextInput
111
- {...shared}
112
- type={field.type ?? "text"}
113
- minLength={field.minLength}
114
- min={field.min}
115
- max={field.max}
116
- />
117
- )}
118
- <small id={`${id}-help`} data-error={Boolean(error)}>
119
- {error ||
120
- field.hint ||
121
- (field.type === "file" ? values[field.id] : "")}
122
- </small>
123
- </div>
124
- );
125
- })}
126
- </div>
127
- );
128
- }
129
-
130
- /** Table is independently scroll-owned. The host supplies filtered rows and exact actions. */
131
- export function RecordTable({
132
- caption,
133
- columns,
134
- rows,
135
- emptyLabel,
136
- actionLabel,
137
- onOpen,
138
- }: {
139
- caption: string;
140
- columns: string[];
141
- rows: { id: string; cells: string[] }[];
142
- emptyLabel: string;
143
- actionLabel: string;
144
- onOpen?: (id: string) => void;
145
- }) {
146
- return (
147
- <div
148
- className="workflow-table-scroll"
149
- data-slot="record-table"
150
- tabIndex={0}
151
- role="region"
152
- aria-label={caption}
153
- >
154
- <table>
155
- <caption>{caption}</caption>
156
- <thead>
157
- <tr>
158
- {columns.map((column, i) => (
159
- <th scope="col" key={i}>
160
- {column}
161
- </th>
162
- ))}
163
- {onOpen ? <th scope="col">{actionLabel}</th> : null}
164
- </tr>
165
- </thead>
166
- <tbody>
167
- {rows.length ? (
168
- rows.map((row) => (
169
- <tr key={row.id}>
170
- {row.cells.map((cell, i) => (
171
- <td key={i}>{cell}</td>
172
- ))}
173
- {onOpen ? (
174
- <td>
175
- <Button
176
- size="sm"
177
- variant="outline"
178
- onClick={() => onOpen(row.id)}
179
- aria-label={`${actionLabel}: ${row.cells[0]}`}
180
- >
181
- {actionLabel}
182
- </Button>
183
- </td>
184
- ) : null}
185
- </tr>
186
- ))
187
- ) : (
188
- <tr>
189
- <td colSpan={columns.length + (onOpen ? 1 : 0)}>{emptyLabel}</td>
190
- </tr>
191
- )}
192
- </tbody>
193
- </table>
194
- </div>
195
- );
196
- }
197
-
198
- /** Gate states are supplied by the host; a preview cannot verify identity or hardware. */
199
- export function StepChecklist({
200
- items,
201
- passLabel,
202
- failLabel,
203
- onChange,
204
- }: {
205
- items: {
206
- id: string;
207
- label: string;
208
- description: string;
209
- state: string;
210
- disabled?: boolean;
211
- }[];
212
- passLabel: string;
213
- failLabel: string;
214
- onChange: (id: string, state: "pass" | "fail") => void;
215
- }) {
216
- return (
217
- <ol className="workflow-checklist" data-slot="step-checklist">
218
- {items.map((item) => (
219
- <li className="product-card" key={item.id} data-state={item.state}>
220
- <strong>{item.label}</strong>
221
- <p>{item.description}</p>
222
- <div className="product-actions">
223
- <Button
224
- disabled={item.disabled}
225
- aria-pressed={item.state === "pass"}
226
- onClick={() => onChange(item.id, "pass")}
227
- >
228
- {passLabel}
229
- </Button>
230
- <Button
231
- variant="outline"
232
- disabled={item.disabled}
233
- aria-pressed={item.state === "fail"}
234
- onClick={() => onChange(item.id, "fail")}
235
- >
236
- {failLabel}
237
- </Button>
238
- </div>
239
- </li>
240
- ))}
241
- </ol>
242
- );
243
- }
244
-
245
- export function StatStrip({
246
- items,
247
- }: {
248
- items: { label: string; value: string; detail?: string }[];
249
- }) {
250
- return (
251
- <dl className="workflow-stats" data-slot="stat-strip">
252
- {items.map((item) => (
253
- <div className="product-card" key={item.label}>
254
- <dt>{item.label}</dt>
255
- <dd>{item.value}</dd>
256
- {item.detail ? <small>{item.detail}</small> : null}
257
- </div>
258
- ))}
259
- </dl>
260
- );
261
- }
262
-
263
- export function RatingFields({
264
- labels,
265
- values,
266
- onChange,
267
- }: {
268
- labels: string[];
269
- values: number[];
270
- onChange: (index: number, value: number) => void;
271
- }) {
272
- return (
273
- <div className="workflow-fields" data-slot="rating-fields">
274
- {labels.map((label, index) => (
275
- <fieldset className="product-card" key={label}>
276
- <legend>{label}</legend>
277
- <div className="product-actions">
278
- {[1, 2, 3, 4, 5].map((value) => (
279
- <Button
280
- key={value}
281
- variant={values[index] === value ? "default" : "outline"}
282
- aria-pressed={values[index] === value}
283
- onClick={() => onChange(index, value)}
284
- aria-label={`${label}: ${value} / 5`}
285
- >
286
- {value}
287
- </Button>
288
- ))}
289
- </div>
290
- </fieldset>
291
- ))}
292
- </div>
293
- );
294
- }
295
-
296
- export function TrendSummary({
297
- title,
298
- items,
299
- }: {
300
- title: string;
301
- items: { label: string; value: number; max: number }[];
302
- }) {
303
- return (
304
- <section className="product-card" data-slot="trend-summary">
305
- <h4>{title}</h4>
306
- {items.map((item) => (
307
- <label className="workflow-field" key={item.label}>
308
- <span>
309
- {item.label} · {item.value} / {item.max}
310
- </span>
311
- <meter min={0} max={item.max} value={item.value} />
312
- </label>
313
- ))}
314
- </section>
315
- );
316
- }
317
-
318
- /** No camera, microphone, network or recording is started by this visual stage. */
319
- export function MediaStage({
320
- label,
321
- status,
322
- children,
323
- }: {
324
- label: string;
325
- status: string;
326
- children?: React.ReactNode;
327
- }) {
328
- return (
329
- <section
330
- className="workflow-media"
331
- data-slot="media-stage"
332
- aria-label={label}
333
- >
334
- <h4>{label}</h4>
335
- <p role="status">{status}</p>
336
- {children}
337
- </section>
338
- );
339
- }