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