@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.
- package/bin/utopia-ds-mcp.mjs +5 -15
- package/bin/utopia-ds.mjs +3 -16
- package/data/docs/create-a-brandbook.md +11 -1
- package/data/manifests/blocks.json +21 -230
- package/data/manifests/patterns.json +0 -330
- package/lib/api.mjs +6 -193
- package/lib/brandbook-validation.mjs +112 -0
- package/package.json +2 -2
- package/data/blocks/semantic-pattern-parts/AuthParts.tsx +0 -48
- package/data/blocks/semantic-pattern-parts/ChoiceCards.tsx +0 -7
- package/data/blocks/semantic-pattern-parts/ListFilters.tsx +0 -9
- package/data/blocks/semantic-pattern-parts/OutcomeNotice.tsx +0 -7
- package/data/blocks/semantic-pattern-parts/PatternParts.tsx +0 -7
- package/data/blocks/semantic-pattern-parts/ProductParts.tsx +0 -38
- package/data/blocks/semantic-pattern-parts/ProgressSteps.tsx +0 -6
- package/data/blocks/semantic-pattern-parts/ReviewList.tsx +0 -7
- package/data/blocks/semantic-pattern-parts/ValidatedField.tsx +0 -8
- package/data/blocks/semantic-pattern-parts/WorkflowParts.tsx +0 -339
- package/data/blocks/semantic-pattern-parts/auth-parts.css +0 -27
- package/data/blocks/semantic-pattern-parts/pattern-parts.css +0 -25
- package/data/blocks/semantic-pattern-parts/product-parts.css +0 -25
- package/data/blocks/semantic-pattern-parts/types.ts +0 -3
- package/data/blocks/semantic-pattern-parts/workflow-parts.css +0 -118
- package/data/blocks/ux-pattern-examples/AccessExamples.tsx +0 -46
- package/data/blocks/ux-pattern-examples/AccountExamples.tsx +0 -133
- package/data/blocks/ux-pattern-examples/OnboardingExample.tsx +0 -15
- package/data/blocks/ux-pattern-examples/ProductExamples.tsx +0 -79
- package/data/blocks/ux-pattern-examples/ReviewExample.tsx +0 -14
- package/data/blocks/ux-pattern-examples/SignupExample.tsx +0 -22
- package/data/blocks/ux-pattern-examples/WorkflowExample.tsx +0 -1761
- package/data/blocks/ux-pattern-examples/WorkflowExamples.tsx +0 -190
- package/data/blocks/ux-pattern-examples/copy.ts +0 -6
- package/data/blocks/ux-pattern-examples/ux-examples.css +0 -18
- package/data/blocks/ux-pattern-examples/workflow-copy.ts +0 -76
- package/data/blocks/ux-pattern-examples/workflow-definitions.ts +0 -1291
- package/data/docs/ui-parts-qa.md +0 -172
- package/data/docs/ui-parts.md +0 -105
- package/data/docs/ux-pattern-adaptation-qa.md +0 -38
- package/data/docs/ux-pattern-adaptation.md +0 -41
- package/data/docs/ux-pattern-migration.md +0 -111
- package/data/docs/ux-pattern-release-2026-09-13.md +0 -26
- package/data/docs/ux-source-evidence/account-auth.json +0 -41
- package/data/docs/ux-source-evidence/account-forgot-password.json +0 -36
- package/data/docs/ux-source-evidence/account-invite.json +0 -63
- package/data/docs/ux-source-evidence/account-locked.json +0 -63
- package/data/docs/ux-source-evidence/account-login.json +0 -28
- package/data/docs/ux-source-evidence/account-mfa.json +0 -62
- package/data/docs/ux-source-evidence/account-onboarding-credentials.json +0 -52
- package/data/docs/ux-source-evidence/account-onboarding-legal.json +0 -56
- package/data/docs/ux-source-evidence/account-onboarding-orientation.json +0 -66
- package/data/docs/ux-source-evidence/account-onboarding-profile.json +0 -50
- package/data/docs/ux-source-evidence/account-onboarding.json +0 -69
- package/data/docs/ux-source-evidence/account-reset-password.json +0 -27
- package/data/docs/ux-source-evidence/account-session-expired.json +0 -62
- package/data/docs/ux-source-evidence/account-signup.json +0 -33
- package/data/docs/ux-source-evidence/account-verify-email.json +0 -34
- package/data/docs/ux-source-evidence/account-welcome.json +0 -41
- package/data/docs/ux-source-evidence/admin-analytics.json +0 -54
- package/data/docs/ux-source-evidence/admin-audit.json +0 -31
- package/data/docs/ux-source-evidence/admin-broadcasts-new.json +0 -36
- package/data/docs/ux-source-evidence/admin-broadcasts.json +0 -41
- package/data/docs/ux-source-evidence/admin-closures.json +0 -52
- package/data/docs/ux-source-evidence/admin-devices.json +0 -51
- package/data/docs/ux-source-evidence/admin-escalations.json +0 -35
- package/data/docs/ux-source-evidence/admin-incidents.json +0 -54
- package/data/docs/ux-source-evidence/admin-loading.json +0 -17
- package/data/docs/ux-source-evidence/admin-onboarding-detail.json +0 -80
- package/data/docs/ux-source-evidence/admin-onboarding.json +0 -62
- package/data/docs/ux-source-evidence/admin-payments.json +0 -45
- package/data/docs/ux-source-evidence/admin-providers.json +0 -38
- package/data/docs/ux-source-evidence/admin-settings.json +0 -18
- package/data/docs/ux-source-evidence/admin-subscriptions-detail-allocate.json +0 -42
- package/data/docs/ux-source-evidence/admin-subscriptions-detail.json +0 -48
- package/data/docs/ux-source-evidence/admin-subscriptions-new.json +0 -43
- package/data/docs/ux-source-evidence/admin-subscriptions.json +0 -41
- package/data/docs/ux-source-evidence/admin-transactions.json +0 -41
- package/data/docs/ux-source-evidence/error.json +0 -30
- package/data/docs/ux-source-evidence/loading.json +0 -19
- package/data/docs/ux-source-evidence/marketing-blog.json +0 -48
- package/data/docs/ux-source-evidence/marketing-contact.json +0 -36
- package/data/docs/ux-source-evidence/marketing.json +0 -64
- package/data/docs/ux-source-evidence/not-found.json +0 -28
- package/data/docs/ux-source-evidence/portal-loading.json +0 -17
- package/data/docs/ux-source-evidence/portal-mentor-messages.json +0 -31
- package/data/docs/ux-source-evidence/portal-mentor-profile.json +0 -23
- package/data/docs/ux-source-evidence/portal-mentor-requests-detail.json +0 -54
- package/data/docs/ux-source-evidence/portal-mentor-sessions-detail-assess.json +0 -42
- package/data/docs/ux-source-evidence/portal-mentor-settings.json +0 -18
- package/data/docs/ux-source-evidence/portal-mentor.json +0 -38
- package/data/docs/ux-source-evidence/portal-sessions-detail-live.json +0 -48
- package/data/docs/ux-source-evidence/portal-sponsor-billing.json +0 -28
- package/data/docs/ux-source-evidence/portal-sponsor-profile.json +0 -22
- package/data/docs/ux-source-evidence/portal-sponsor-sessions.json +0 -28
- package/data/docs/ux-source-evidence/portal-sponsor-settings.json +0 -18
- package/data/docs/ux-source-evidence/portal-sponsor-trainees.json +0 -36
- package/data/docs/ux-source-evidence/portal-sponsor.json +0 -52
- package/data/docs/ux-source-evidence/portal-trainee-competency.json +0 -42
- package/data/docs/ux-source-evidence/portal-trainee-credits.json +0 -40
- package/data/docs/ux-source-evidence/portal-trainee-mentors.json +0 -48
- package/data/docs/ux-source-evidence/portal-trainee-messages.json +0 -32
- package/data/docs/ux-source-evidence/portal-trainee-notifications.json +0 -35
- package/data/docs/ux-source-evidence/portal-trainee-profile.json +0 -26
- package/data/docs/ux-source-evidence/portal-trainee-request.json +0 -54
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +0 -36
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +0 -42
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail.json +0 -55
- package/data/docs/ux-source-evidence/portal-trainee-sessions.json +0 -36
- package/data/docs/ux-source-evidence/portal-trainee-settings-notifications.json +0 -38
- package/data/docs/ux-source-evidence/portal-trainee-settings.json +0 -18
- package/data/docs/ux-source-evidence/portal-trainee.json +0 -44
- package/data/docs/ux-source-evidence/portal-tv-detail.json +0 -39
- package/data/docs/ux-source-evidence/portal-tv.json +0 -44
- package/data/docs/ux-source-evidence/shared-admin-admin-shell.json +0 -35
- package/data/docs/ux-source-evidence/shared-auth-auth-primitives.json +0 -105
- package/data/docs/ux-source-evidence/shared-auth-auth-shell.json +0 -25
- package/data/docs/ux-source-evidence/shared-auth-onboarding-primitives.json +0 -52
- package/data/docs/ux-source-evidence/shared-auth-onboarding-shell.json +0 -28
- package/data/docs/ux-source-evidence/shared-auth-status-indicator.json +0 -41
- package/data/docs/ux-source-evidence/shared-portal-empty-state.json +0 -24
- package/data/docs/ux-source-evidence/shared-portal-loading-skeleton.json +0 -27
- package/data/docs/ux-source-evidence/shared-portal-notifications-dropdown.json +0 -39
- package/data/docs/ux-source-evidence/shared-portal-portal-shell.json +0 -36
- package/data/docs/ux-source-evidence/shared-portal-profile-form.json +0 -32
- package/data/docs/ux-source-evidence/shared-portal-settings-view.json +0 -81
- package/data/docs/ux-source-evidence/shared-portal-usage-sparkline.json +0 -18
- package/data/docs/ux-source-evidence/shared-portal-user-menu.json +0 -34
- package/data/manifests/ui-patterns.json +0 -1029
- package/data/manifests/ux-examples.json +0 -2177
- package/data/manifests/ux-patterns.json +0 -1135
- package/data/manifests/ux-source-inventory.json +0 -4669
- package/data/ui-parts/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +0 -87
- package/data/ui-parts/blocks/analytics-dashboard/analytics-dashboard.css +0 -92
- package/data/ui-parts/blocks/semantic-pattern-parts/AuthParts.tsx +0 -48
- package/data/ui-parts/blocks/semantic-pattern-parts/ChoiceCards.tsx +0 -7
- package/data/ui-parts/blocks/semantic-pattern-parts/ListFilters.tsx +0 -9
- package/data/ui-parts/blocks/semantic-pattern-parts/OutcomeNotice.tsx +0 -7
- package/data/ui-parts/blocks/semantic-pattern-parts/ProductParts.tsx +0 -38
- package/data/ui-parts/blocks/semantic-pattern-parts/ProgressSteps.tsx +0 -6
- package/data/ui-parts/blocks/semantic-pattern-parts/ReviewList.tsx +0 -7
- package/data/ui-parts/blocks/semantic-pattern-parts/ValidatedField.tsx +0 -8
- package/data/ui-parts/blocks/semantic-pattern-parts/WorkflowParts.tsx +0 -339
- package/data/ui-parts/blocks/semantic-pattern-parts/auth-parts.css +0 -27
- package/data/ui-parts/blocks/semantic-pattern-parts/pattern-parts.css +0 -25
- package/data/ui-parts/blocks/semantic-pattern-parts/product-parts.css +0 -25
- package/data/ui-parts/blocks/semantic-pattern-parts/types.ts +0 -3
- package/data/ui-parts/blocks/semantic-pattern-parts/workflow-parts.css +0 -118
- package/data/ui-parts/blocks/ux-pattern-examples/AccessExamples.tsx +0 -46
- package/data/ui-parts/blocks/ux-pattern-examples/AccountExamples.tsx +0 -133
- package/data/ui-parts/blocks/ux-pattern-examples/ProductExamples.tsx +0 -79
- package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExample.tsx +0 -1761
- package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExamples.tsx +0 -190
- package/data/ui-parts/blocks/ux-pattern-examples/copy.ts +0 -6
- package/data/ui-parts/blocks/ux-pattern-examples/ux-examples.css +0 -18
- package/data/ui-parts/blocks/ux-pattern-examples/workflow-copy.ts +0 -76
- package/data/ui-parts/blocks/ux-pattern-examples/workflow-definitions.ts +0 -1291
- package/data/ux-source-evidence/account-auth.json +0 -41
- package/data/ux-source-evidence/account-forgot-password.json +0 -36
- package/data/ux-source-evidence/account-invite.json +0 -63
- package/data/ux-source-evidence/account-locked.json +0 -63
- package/data/ux-source-evidence/account-login.json +0 -28
- package/data/ux-source-evidence/account-mfa.json +0 -62
- package/data/ux-source-evidence/account-onboarding-credentials.json +0 -52
- package/data/ux-source-evidence/account-onboarding-legal.json +0 -56
- package/data/ux-source-evidence/account-onboarding-orientation.json +0 -66
- package/data/ux-source-evidence/account-onboarding-profile.json +0 -50
- package/data/ux-source-evidence/account-onboarding.json +0 -69
- package/data/ux-source-evidence/account-reset-password.json +0 -27
- package/data/ux-source-evidence/account-session-expired.json +0 -62
- package/data/ux-source-evidence/account-signup.json +0 -33
- package/data/ux-source-evidence/account-verify-email.json +0 -34
- package/data/ux-source-evidence/account-welcome.json +0 -41
- package/data/ux-source-evidence/admin-analytics.json +0 -54
- package/data/ux-source-evidence/admin-audit.json +0 -31
- package/data/ux-source-evidence/admin-broadcasts-new.json +0 -36
- package/data/ux-source-evidence/admin-broadcasts.json +0 -41
- package/data/ux-source-evidence/admin-closures.json +0 -52
- package/data/ux-source-evidence/admin-devices.json +0 -51
- package/data/ux-source-evidence/admin-escalations.json +0 -35
- package/data/ux-source-evidence/admin-incidents.json +0 -54
- package/data/ux-source-evidence/admin-loading.json +0 -17
- package/data/ux-source-evidence/admin-onboarding-detail.json +0 -80
- package/data/ux-source-evidence/admin-onboarding.json +0 -62
- package/data/ux-source-evidence/admin-payments.json +0 -45
- package/data/ux-source-evidence/admin-providers.json +0 -38
- package/data/ux-source-evidence/admin-settings.json +0 -18
- package/data/ux-source-evidence/admin-subscriptions-detail-allocate.json +0 -42
- package/data/ux-source-evidence/admin-subscriptions-detail.json +0 -48
- package/data/ux-source-evidence/admin-subscriptions-new.json +0 -43
- package/data/ux-source-evidence/admin-subscriptions.json +0 -41
- package/data/ux-source-evidence/admin-transactions.json +0 -41
- package/data/ux-source-evidence/error.json +0 -30
- package/data/ux-source-evidence/loading.json +0 -19
- package/data/ux-source-evidence/marketing-blog.json +0 -48
- package/data/ux-source-evidence/marketing-contact.json +0 -36
- package/data/ux-source-evidence/marketing.json +0 -64
- package/data/ux-source-evidence/not-found.json +0 -28
- package/data/ux-source-evidence/portal-loading.json +0 -17
- package/data/ux-source-evidence/portal-mentor-messages.json +0 -31
- package/data/ux-source-evidence/portal-mentor-profile.json +0 -23
- package/data/ux-source-evidence/portal-mentor-requests-detail.json +0 -54
- package/data/ux-source-evidence/portal-mentor-sessions-detail-assess.json +0 -42
- package/data/ux-source-evidence/portal-mentor-settings.json +0 -18
- package/data/ux-source-evidence/portal-mentor.json +0 -38
- package/data/ux-source-evidence/portal-sessions-detail-live.json +0 -48
- package/data/ux-source-evidence/portal-sponsor-billing.json +0 -28
- package/data/ux-source-evidence/portal-sponsor-profile.json +0 -22
- package/data/ux-source-evidence/portal-sponsor-sessions.json +0 -28
- package/data/ux-source-evidence/portal-sponsor-settings.json +0 -18
- package/data/ux-source-evidence/portal-sponsor-trainees.json +0 -36
- package/data/ux-source-evidence/portal-sponsor.json +0 -52
- package/data/ux-source-evidence/portal-trainee-competency.json +0 -42
- package/data/ux-source-evidence/portal-trainee-credits.json +0 -40
- package/data/ux-source-evidence/portal-trainee-mentors.json +0 -48
- package/data/ux-source-evidence/portal-trainee-messages.json +0 -32
- package/data/ux-source-evidence/portal-trainee-notifications.json +0 -35
- package/data/ux-source-evidence/portal-trainee-profile.json +0 -26
- package/data/ux-source-evidence/portal-trainee-request.json +0 -54
- package/data/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +0 -36
- package/data/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +0 -42
- package/data/ux-source-evidence/portal-trainee-sessions-detail.json +0 -55
- package/data/ux-source-evidence/portal-trainee-sessions.json +0 -36
- package/data/ux-source-evidence/portal-trainee-settings-notifications.json +0 -38
- package/data/ux-source-evidence/portal-trainee-settings.json +0 -18
- package/data/ux-source-evidence/portal-trainee.json +0 -44
- package/data/ux-source-evidence/portal-tv-detail.json +0 -39
- package/data/ux-source-evidence/portal-tv.json +0 -44
- package/data/ux-source-evidence/shared-admin-admin-shell.json +0 -35
- package/data/ux-source-evidence/shared-auth-auth-primitives.json +0 -105
- package/data/ux-source-evidence/shared-auth-auth-shell.json +0 -25
- package/data/ux-source-evidence/shared-auth-onboarding-primitives.json +0 -52
- package/data/ux-source-evidence/shared-auth-onboarding-shell.json +0 -28
- package/data/ux-source-evidence/shared-auth-status-indicator.json +0 -41
- package/data/ux-source-evidence/shared-portal-empty-state.json +0 -24
- package/data/ux-source-evidence/shared-portal-loading-skeleton.json +0 -27
- package/data/ux-source-evidence/shared-portal-notifications-dropdown.json +0 -39
- package/data/ux-source-evidence/shared-portal-portal-shell.json +0 -36
- package/data/ux-source-evidence/shared-portal-profile-form.json +0 -32
- package/data/ux-source-evidence/shared-portal-settings-view.json +0 -81
- package/data/ux-source-evidence/shared-portal-usage-sparkline.json +0 -18
- 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.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "AI-readable CLI and MCP server for Ceramic Design System",
|
|
5
5
|
"ceramic": {
|
|
6
|
-
"designSystem": "^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%; }
|