@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
@@ -0,0 +1,112 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, extname, isAbsolute, join, resolve } from 'node:path'
3
+
4
+ const chapters = ['brand', 'core', 'photography', 'motion', 'marketing', 'solution']
5
+ const directionFields = ['name', 'thesis', 'typography', 'color', 'imagery', 'motion', 'marketing', 'solution', 'fit', 'tradeoff']
6
+ const text = (value) => typeof value === 'string' && value.trim().length > 0
7
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
8
+ const issue = (code, message, field) => ({ code, message, ...(field ? { field } : {}) })
9
+
10
+ function resolveBrandPath(inputPath, root) {
11
+ const candidate = isAbsolute(inputPath) ? inputPath : resolve(root, inputPath)
12
+ return extname(candidate).toLowerCase() === '.md' ? candidate : join(candidate, 'BRAND.md')
13
+ }
14
+
15
+ function parseJson(raw, label, errors) {
16
+ try {
17
+ return JSON.parse(raw)
18
+ } catch {
19
+ errors.push(issue('INVALID_JSON', `${label} must use valid JSON syntax.`))
20
+ return null
21
+ }
22
+ }
23
+
24
+ export function validateBrandbook({ path: inputPath }, root = process.cwd()) {
25
+ const errors = []
26
+ const warnings = []
27
+ const suppliedPath = String(inputPath || '').trim()
28
+ if (!suppliedPath) {
29
+ return { ok: false, path: null, errors: [issue('PATH_REQUIRED', 'Provide a BRAND.md file or its containing directory.')], warnings }
30
+ }
31
+
32
+ const brandPath = resolveBrandPath(suppliedPath, root)
33
+ if (!existsSync(brandPath)) {
34
+ return { ok: false, path: brandPath, errors: [issue('BRAND_MD_NOT_FOUND', `BRAND.md was not found at ${brandPath}.`)], warnings }
35
+ }
36
+
37
+ const raw = readFileSync(brandPath, 'utf8')
38
+ const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/)
39
+ if (!frontmatter) errors.push(issue('FRONTMATTER_REQUIRED', 'BRAND.md must start with JSON frontmatter between --- delimiters.'))
40
+ const meta = frontmatter ? parseJson(frontmatter[1], 'BRAND.md frontmatter', errors) : null
41
+ if (meta) {
42
+ if (meta.schema !== 'ceramic.document/v1' || meta.id !== 'brand') errors.push(issue('INVALID_DOCUMENT_IDENTITY', 'Frontmatter must use schema ceramic.document/v1 and id brand.'))
43
+ if (!/^[a-z][a-z0-9-]{0,79}$/.test(meta.brandId || '')) errors.push(issue('INVALID_BRAND_ID', 'brandId must be a lowercase slug.', 'brandId'))
44
+ if (!/^\d+\.\d+\.\d+$/.test(meta.version || '')) errors.push(issue('INVALID_VERSION', 'version must be semantic versioning.', 'version'))
45
+ if (!['draft', 'review', 'approved'].includes(meta.status)) errors.push(issue('INVALID_STATUS', 'status must be draft, review, or approved.', 'status'))
46
+ if (!Array.isArray(meta.requiredLocales) || !meta.requiredLocales.includes('en') || !meta.requiredLocales.includes('ar')) {
47
+ errors.push(issue('REQUIRED_LOCALES', 'requiredLocales must include en and ar unless the user explicitly approved another scope.', 'requiredLocales'))
48
+ }
49
+ }
50
+
51
+ const blocks = [...raw.matchAll(/^```ceramic\s*\r?\n([\s\S]*?)^```\s*$/gm)]
52
+ if (blocks.length !== 1) errors.push(issue('CERAMIC_CONTRACT_COUNT', 'BRAND.md must contain exactly one fenced ceramic JSON contract.'))
53
+ const contract = blocks.length === 1 ? parseJson(blocks[0][1], 'Ceramic contract', errors) : null
54
+ if (contract) {
55
+ for (const chapter of chapters) {
56
+ if (!object(contract[chapter])) {
57
+ errors.push(issue('CHAPTER_CONTRACT_REQUIRED', `Missing ${chapter} contract.`, chapter))
58
+ continue
59
+ }
60
+ const decisions = contract[chapter].decisions
61
+ if (!Array.isArray(decisions) || decisions.length === 0) errors.push(issue('DECISIONS_REQUIRED', `${chapter} needs at least one decision.`, `${chapter}.decisions`))
62
+ if (chapter !== 'brand') {
63
+ const expected = ['marketing', 'solution'].includes(chapter) ? 'core' : 'brand'
64
+ if (contract[chapter].extends !== expected) errors.push(issue('INVALID_INHERITANCE', `${chapter}.extends must be ${expected}.`, `${chapter}.extends`))
65
+ }
66
+ }
67
+
68
+ const session = contract.brand?.directionSession
69
+ if (!object(session) || !Array.isArray(session.options) || session.options.length !== 3) {
70
+ errors.push(issue('DIRECTION_SESSION_REQUIRED', 'brand.directionSession must contain exactly three options.', 'brand.directionSession'))
71
+ } else {
72
+ if (session.options.map((option) => option?.id).join('') !== 'ABC') errors.push(issue('DIRECTION_ORDER', 'Direction options must be ordered A, B, C.', 'brand.directionSession.options'))
73
+ for (const option of session.options) {
74
+ if (!object(option) || !directionFields.every((field) => text(option[field]))) errors.push(issue('DIRECTION_INCOMPLETE', `Direction ${option?.id || '?'} is incomplete.`, 'brand.directionSession.options'))
75
+ }
76
+ if (!['A', 'B', 'C'].includes(session.selected)) errors.push(issue('DIRECTION_SELECTION_REQUIRED', 'A selected direction is required.', 'brand.directionSession.selected'))
77
+ if (!['refined', 'approved'].includes(session.status) || !text(session.refinement)) errors.push(issue('DIRECTION_REFINEMENT_REQUIRED', 'The selected direction must include a refinement note and refined or approved status.', 'brand.directionSession'))
78
+ }
79
+
80
+ const arabic = contract.brand?.localization?.ar
81
+ if (!object(arabic) || !text(arabic.displayFont) || !text(arabic.bodyFont) || !object(arabic.text) || Object.keys(arabic.text).length === 0) {
82
+ errors.push(issue('ARABIC_PARITY_REQUIRED', 'brand.localization.ar must provide displayFont, bodyFont, and a nonempty 1:1 text map.', 'brand.localization.ar'))
83
+ }
84
+
85
+ const showcases = contract.solution?.showcases
86
+ if (!Array.isArray(showcases) || showcases.length < 1 || showcases.length > 3) errors.push(issue('SOLUTION_SHOWCASES', 'solution.showcases must contain one to three brand-fit applications.', 'solution.showcases'))
87
+ else if (new Set(showcases.map((showcase) => showcase?.type)).size !== showcases.length || showcases.some((showcase) => !['phone', 'desktop', 'environment'].includes(showcase?.type))) {
88
+ errors.push(issue('SOLUTION_SHOWCASE_TYPES', 'Solution showcase types must be unique phone, desktop, or environment entries.', 'solution.showcases'))
89
+ }
90
+ }
91
+
92
+ const anchors = [...raw.matchAll(/^## .+ \{#([a-z]+)\}\s*$/gm)].map((match) => match[1])
93
+ for (const chapter of chapters) if (!anchors.includes(chapter)) errors.push(issue('CHAPTER_ANCHOR_REQUIRED', `Missing {#${chapter}} chapter anchor.`, chapter))
94
+
95
+ const manifestPath = join(dirname(brandPath), 'assets', 'manifest.json')
96
+ if (!existsSync(manifestPath)) errors.push(issue('ASSET_MANIFEST_REQUIRED', 'assets/manifest.json is required.'))
97
+ else {
98
+ const manifest = parseJson(readFileSync(manifestPath, 'utf8'), 'Asset manifest', errors)
99
+ if (manifest && (manifest.schema !== 'ceramic.assets/v1' || !Array.isArray(manifest.assets))) errors.push(issue('INVALID_ASSET_MANIFEST', 'Asset manifest must use ceramic.assets/v1 and contain an assets array.'))
100
+ if (manifest?.assets) {
101
+ const families = new Set(manifest.assets.filter((asset) => asset.kind === 'font').map((asset) => asset.family))
102
+ const arabic = contract?.brand?.localization?.ar
103
+ for (const role of ['displayFont', 'bodyFont']) if (text(arabic?.[role]) && !families.has(arabic[role])) errors.push(issue('ARABIC_FONT_NOT_BUNDLED', `Arabic ${role} ${arabic[role]} is not registered as a local font asset.`, `brand.localization.ar.${role}`))
104
+ for (const asset of manifest.assets) {
105
+ if (!text(asset.path) || !asset.path.startsWith('assets/') || asset.path.includes('..') || !existsSync(join(dirname(brandPath), asset.path))) errors.push(issue('ASSET_FILE_MISSING', `Manifest asset is missing or unsafe: ${asset.path || '(no path)'}.`, 'assets'))
106
+ }
107
+ }
108
+ }
109
+
110
+ if (meta?.status === 'approved') warnings.push(issue('APPROVAL_SCOPE', 'Schema validation does not replace visual, linguistic, licensing, or stakeholder approval.'))
111
+ return { ok: errors.length === 0, path: brandPath, errors, warnings, summary: { errorCount: errors.length, warningCount: warnings.length, chapters: anchors.filter((anchor) => chapters.includes(anchor)).length } }
112
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@utopia-studio-design/design-system-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "AI-readable CLI and MCP server for Ceramic Design System",
5
5
  "ceramic": {
6
- "designSystem": "^0.11.0"
6
+ "designSystem": "^0.10.0"
7
7
  },
8
8
  "type": "module",
9
9
  "license": "MIT",
@@ -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,7 +0,0 @@
1
- export { ChoiceCards } from './ChoiceCards'
2
- export { ValidatedField } from './ValidatedField'
3
- export { ListFilters } from './ListFilters'
4
- export { ReviewList } from './ReviewList'
5
- export { ProgressSteps } from './ProgressSteps'
6
- export { OutcomeNotice } from './OutcomeNotice'
7
- export type { PatternOption, ReviewItem } from './types'
@@ -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
- }
@@ -1,27 +0,0 @@
1
- /* Registered auth-interactions composition. All appearance resolves at host theme. */
2
- :root { --auth-form-width: 28rem; --auth-code-cell-min: 2rem; }
3
- :root, [data-theme] { --auth-code-radius: min(var(--radius-input), var(--space-2)); --auth-panel-radius: min(var(--radius-surface), var(--space-4)); }
4
- .auth-part-frame { display: grid; gap: var(--space-5); inline-size: 100%; max-inline-size: var(--auth-form-width); }
5
- .auth-part-frame header, .auth-part-field { display: grid; gap: var(--space-2); }
6
- .auth-part-field label, .auth-part-code legend { font-weight: var(--font-weight-strong); }
7
- .auth-part-password { display: flex; align-items: center; gap: var(--space-2); min-inline-size: 0; }
8
- .auth-part-password > input { min-inline-size: 0; flex: 1; }
9
- .auth-part-field p, .auth-part-rules, .auth-part-code p { font-size: var(--font-size-small); }
10
- .auth-part-field [data-error="true"], .auth-part-code p { color: var(--destructive-text); }
11
- .auth-part-rules { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); padding: 0; list-style: none; margin: 0; color: var(--foreground-muted); }
12
- .auth-part-rules [data-passed="true"] { color: var(--success-text); }
13
- .auth-part-code { padding: 0; border: 0; margin: 0; min-inline-size: 0; }
14
- .auth-part-code legend { margin-block-end: var(--space-3); }
15
- .auth-part-code > div { display: grid; grid-template-columns: repeat(6, minmax(var(--auth-code-cell-min), 1fr)); gap: var(--space-2); }
16
- .auth-part-code input { text-align: center; padding-inline: 0; min-inline-size: 0; inline-size: 100%; border-radius: var(--auth-code-radius); }
17
- .auth-part-providers { display: grid; gap: var(--space-2); }
18
- .auth-part-providers .uds-button { inline-size: 100%; }
19
- .auth-part-divider { display: flex; gap: var(--space-3); align-items: center; color: var(--foreground-muted); font-size: var(--font-size-small); }
20
- .auth-part-divider::before, .auth-part-divider::after { content: ''; flex: 1; border-block-start: var(--border-width-hairline) solid var(--border); }
21
- .auth-part-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
22
- .auth-part-actions .uds-button { white-space: normal; }
23
- .auth-part-card { display: grid; gap: var(--space-3); padding: var(--space-4); background: var(--surface); border: var(--border-width-hairline) solid var(--border); border-radius: var(--auth-panel-radius); }
24
- .auth-part-check { display: flex; align-items: start; gap: var(--space-2); }
25
- .auth-part-check > button { flex-shrink: 0; }
26
- .auth-part-state { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; font-size: var(--font-size-small); }
27
- .auth-part-state select { inline-size: auto; max-inline-size: 100%; }