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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/bin/utopia-ds-mcp.mjs +15 -4
  2. package/bin/utopia-ds.mjs +16 -3
  3. package/data/blocks/semantic-pattern-parts/AuthParts.tsx +48 -0
  4. package/data/blocks/semantic-pattern-parts/ChoiceCards.tsx +7 -0
  5. package/data/blocks/semantic-pattern-parts/ListFilters.tsx +9 -0
  6. package/data/blocks/semantic-pattern-parts/OutcomeNotice.tsx +7 -0
  7. package/data/blocks/semantic-pattern-parts/PatternParts.tsx +7 -0
  8. package/data/blocks/semantic-pattern-parts/ProductParts.tsx +38 -0
  9. package/data/blocks/semantic-pattern-parts/ProgressSteps.tsx +6 -0
  10. package/data/blocks/semantic-pattern-parts/ReviewList.tsx +7 -0
  11. package/data/blocks/semantic-pattern-parts/ValidatedField.tsx +8 -0
  12. package/data/blocks/semantic-pattern-parts/WorkflowParts.tsx +339 -0
  13. package/data/blocks/semantic-pattern-parts/auth-parts.css +27 -0
  14. package/data/blocks/semantic-pattern-parts/pattern-parts.css +25 -0
  15. package/data/blocks/semantic-pattern-parts/product-parts.css +25 -0
  16. package/data/blocks/semantic-pattern-parts/types.ts +3 -0
  17. package/data/blocks/semantic-pattern-parts/workflow-parts.css +118 -0
  18. package/data/blocks/ux-pattern-examples/AccessExamples.tsx +46 -0
  19. package/data/blocks/ux-pattern-examples/AccountExamples.tsx +133 -0
  20. package/data/blocks/ux-pattern-examples/OnboardingExample.tsx +15 -0
  21. package/data/blocks/ux-pattern-examples/ProductExamples.tsx +79 -0
  22. package/data/blocks/ux-pattern-examples/ReviewExample.tsx +14 -0
  23. package/data/blocks/ux-pattern-examples/SignupExample.tsx +22 -0
  24. package/data/blocks/ux-pattern-examples/WorkflowExample.tsx +1761 -0
  25. package/data/blocks/ux-pattern-examples/WorkflowExamples.tsx +190 -0
  26. package/data/blocks/ux-pattern-examples/copy.ts +6 -0
  27. package/data/blocks/ux-pattern-examples/ux-examples.css +18 -0
  28. package/data/blocks/ux-pattern-examples/workflow-copy.ts +76 -0
  29. package/data/blocks/ux-pattern-examples/workflow-definitions.ts +1291 -0
  30. package/data/docs/ui-parts-qa.md +172 -0
  31. package/data/docs/ui-parts.md +105 -0
  32. package/data/docs/ux-pattern-adaptation-qa.md +38 -0
  33. package/data/docs/ux-pattern-adaptation.md +41 -0
  34. package/data/docs/ux-pattern-migration.md +111 -0
  35. package/data/docs/ux-pattern-publish-2026-09-13.md +31 -0
  36. package/data/docs/ux-pattern-release-2026-09-13.md +26 -0
  37. package/data/docs/ux-source-evidence/account-auth.json +41 -0
  38. package/data/docs/ux-source-evidence/account-forgot-password.json +36 -0
  39. package/data/docs/ux-source-evidence/account-invite.json +63 -0
  40. package/data/docs/ux-source-evidence/account-locked.json +63 -0
  41. package/data/docs/ux-source-evidence/account-login.json +28 -0
  42. package/data/docs/ux-source-evidence/account-mfa.json +62 -0
  43. package/data/docs/ux-source-evidence/account-onboarding-credentials.json +52 -0
  44. package/data/docs/ux-source-evidence/account-onboarding-legal.json +56 -0
  45. package/data/docs/ux-source-evidence/account-onboarding-orientation.json +66 -0
  46. package/data/docs/ux-source-evidence/account-onboarding-profile.json +50 -0
  47. package/data/docs/ux-source-evidence/account-onboarding.json +69 -0
  48. package/data/docs/ux-source-evidence/account-reset-password.json +27 -0
  49. package/data/docs/ux-source-evidence/account-session-expired.json +62 -0
  50. package/data/docs/ux-source-evidence/account-signup.json +33 -0
  51. package/data/docs/ux-source-evidence/account-verify-email.json +34 -0
  52. package/data/docs/ux-source-evidence/account-welcome.json +41 -0
  53. package/data/docs/ux-source-evidence/admin-analytics.json +54 -0
  54. package/data/docs/ux-source-evidence/admin-audit.json +31 -0
  55. package/data/docs/ux-source-evidence/admin-broadcasts-new.json +36 -0
  56. package/data/docs/ux-source-evidence/admin-broadcasts.json +41 -0
  57. package/data/docs/ux-source-evidence/admin-closures.json +52 -0
  58. package/data/docs/ux-source-evidence/admin-devices.json +51 -0
  59. package/data/docs/ux-source-evidence/admin-escalations.json +35 -0
  60. package/data/docs/ux-source-evidence/admin-incidents.json +54 -0
  61. package/data/docs/ux-source-evidence/admin-loading.json +17 -0
  62. package/data/docs/ux-source-evidence/admin-onboarding-detail.json +80 -0
  63. package/data/docs/ux-source-evidence/admin-onboarding.json +62 -0
  64. package/data/docs/ux-source-evidence/admin-payments.json +45 -0
  65. package/data/docs/ux-source-evidence/admin-providers.json +38 -0
  66. package/data/docs/ux-source-evidence/admin-settings.json +18 -0
  67. package/data/docs/ux-source-evidence/admin-subscriptions-detail-allocate.json +42 -0
  68. package/data/docs/ux-source-evidence/admin-subscriptions-detail.json +48 -0
  69. package/data/docs/ux-source-evidence/admin-subscriptions-new.json +43 -0
  70. package/data/docs/ux-source-evidence/admin-subscriptions.json +41 -0
  71. package/data/docs/ux-source-evidence/admin-transactions.json +41 -0
  72. package/data/docs/ux-source-evidence/error.json +30 -0
  73. package/data/docs/ux-source-evidence/loading.json +19 -0
  74. package/data/docs/ux-source-evidence/marketing-blog.json +48 -0
  75. package/data/docs/ux-source-evidence/marketing-contact.json +36 -0
  76. package/data/docs/ux-source-evidence/marketing.json +64 -0
  77. package/data/docs/ux-source-evidence/not-found.json +28 -0
  78. package/data/docs/ux-source-evidence/portal-loading.json +17 -0
  79. package/data/docs/ux-source-evidence/portal-mentor-messages.json +31 -0
  80. package/data/docs/ux-source-evidence/portal-mentor-profile.json +23 -0
  81. package/data/docs/ux-source-evidence/portal-mentor-requests-detail.json +54 -0
  82. package/data/docs/ux-source-evidence/portal-mentor-sessions-detail-assess.json +42 -0
  83. package/data/docs/ux-source-evidence/portal-mentor-settings.json +18 -0
  84. package/data/docs/ux-source-evidence/portal-mentor.json +38 -0
  85. package/data/docs/ux-source-evidence/portal-sessions-detail-live.json +48 -0
  86. package/data/docs/ux-source-evidence/portal-sponsor-billing.json +28 -0
  87. package/data/docs/ux-source-evidence/portal-sponsor-profile.json +22 -0
  88. package/data/docs/ux-source-evidence/portal-sponsor-sessions.json +28 -0
  89. package/data/docs/ux-source-evidence/portal-sponsor-settings.json +18 -0
  90. package/data/docs/ux-source-evidence/portal-sponsor-trainees.json +36 -0
  91. package/data/docs/ux-source-evidence/portal-sponsor.json +52 -0
  92. package/data/docs/ux-source-evidence/portal-trainee-competency.json +42 -0
  93. package/data/docs/ux-source-evidence/portal-trainee-credits.json +40 -0
  94. package/data/docs/ux-source-evidence/portal-trainee-mentors.json +48 -0
  95. package/data/docs/ux-source-evidence/portal-trainee-messages.json +32 -0
  96. package/data/docs/ux-source-evidence/portal-trainee-notifications.json +35 -0
  97. package/data/docs/ux-source-evidence/portal-trainee-profile.json +26 -0
  98. package/data/docs/ux-source-evidence/portal-trainee-request.json +54 -0
  99. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +36 -0
  100. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +42 -0
  101. package/data/docs/ux-source-evidence/portal-trainee-sessions-detail.json +55 -0
  102. package/data/docs/ux-source-evidence/portal-trainee-sessions.json +36 -0
  103. package/data/docs/ux-source-evidence/portal-trainee-settings-notifications.json +38 -0
  104. package/data/docs/ux-source-evidence/portal-trainee-settings.json +18 -0
  105. package/data/docs/ux-source-evidence/portal-trainee.json +44 -0
  106. package/data/docs/ux-source-evidence/portal-tv-detail.json +39 -0
  107. package/data/docs/ux-source-evidence/portal-tv.json +44 -0
  108. package/data/docs/ux-source-evidence/shared-admin-admin-shell.json +35 -0
  109. package/data/docs/ux-source-evidence/shared-auth-auth-primitives.json +105 -0
  110. package/data/docs/ux-source-evidence/shared-auth-auth-shell.json +25 -0
  111. package/data/docs/ux-source-evidence/shared-auth-onboarding-primitives.json +52 -0
  112. package/data/docs/ux-source-evidence/shared-auth-onboarding-shell.json +28 -0
  113. package/data/docs/ux-source-evidence/shared-auth-status-indicator.json +41 -0
  114. package/data/docs/ux-source-evidence/shared-portal-empty-state.json +24 -0
  115. package/data/docs/ux-source-evidence/shared-portal-loading-skeleton.json +27 -0
  116. package/data/docs/ux-source-evidence/shared-portal-notifications-dropdown.json +39 -0
  117. package/data/docs/ux-source-evidence/shared-portal-portal-shell.json +36 -0
  118. package/data/docs/ux-source-evidence/shared-portal-profile-form.json +32 -0
  119. package/data/docs/ux-source-evidence/shared-portal-settings-view.json +81 -0
  120. package/data/docs/ux-source-evidence/shared-portal-usage-sparkline.json +18 -0
  121. package/data/docs/ux-source-evidence/shared-portal-user-menu.json +34 -0
  122. package/data/manifests/blocks.json +230 -21
  123. package/data/manifests/patterns.json +330 -0
  124. package/data/manifests/ui-patterns.json +1029 -0
  125. package/data/manifests/ux-examples.json +2177 -0
  126. package/data/manifests/ux-patterns.json +1135 -0
  127. package/data/manifests/ux-source-inventory.json +4669 -0
  128. package/data/ui-parts/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +87 -0
  129. package/data/ui-parts/blocks/analytics-dashboard/analytics-dashboard.css +92 -0
  130. package/data/ui-parts/blocks/semantic-pattern-parts/AuthParts.tsx +48 -0
  131. package/data/ui-parts/blocks/semantic-pattern-parts/ChoiceCards.tsx +7 -0
  132. package/data/ui-parts/blocks/semantic-pattern-parts/ListFilters.tsx +9 -0
  133. package/data/ui-parts/blocks/semantic-pattern-parts/OutcomeNotice.tsx +7 -0
  134. package/data/ui-parts/blocks/semantic-pattern-parts/ProductParts.tsx +38 -0
  135. package/data/ui-parts/blocks/semantic-pattern-parts/ProgressSteps.tsx +6 -0
  136. package/data/ui-parts/blocks/semantic-pattern-parts/ReviewList.tsx +7 -0
  137. package/data/ui-parts/blocks/semantic-pattern-parts/ValidatedField.tsx +8 -0
  138. package/data/ui-parts/blocks/semantic-pattern-parts/WorkflowParts.tsx +339 -0
  139. package/data/ui-parts/blocks/semantic-pattern-parts/auth-parts.css +27 -0
  140. package/data/ui-parts/blocks/semantic-pattern-parts/pattern-parts.css +25 -0
  141. package/data/ui-parts/blocks/semantic-pattern-parts/product-parts.css +25 -0
  142. package/data/ui-parts/blocks/semantic-pattern-parts/types.ts +3 -0
  143. package/data/ui-parts/blocks/semantic-pattern-parts/workflow-parts.css +118 -0
  144. package/data/ui-parts/blocks/ux-pattern-examples/AccessExamples.tsx +46 -0
  145. package/data/ui-parts/blocks/ux-pattern-examples/AccountExamples.tsx +133 -0
  146. package/data/ui-parts/blocks/ux-pattern-examples/ProductExamples.tsx +79 -0
  147. package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExample.tsx +1761 -0
  148. package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExamples.tsx +190 -0
  149. package/data/ui-parts/blocks/ux-pattern-examples/copy.ts +6 -0
  150. package/data/ui-parts/blocks/ux-pattern-examples/ux-examples.css +18 -0
  151. package/data/ui-parts/blocks/ux-pattern-examples/workflow-copy.ts +76 -0
  152. package/data/ui-parts/blocks/ux-pattern-examples/workflow-definitions.ts +1291 -0
  153. package/data/ux-source-evidence/account-auth.json +41 -0
  154. package/data/ux-source-evidence/account-forgot-password.json +36 -0
  155. package/data/ux-source-evidence/account-invite.json +63 -0
  156. package/data/ux-source-evidence/account-locked.json +63 -0
  157. package/data/ux-source-evidence/account-login.json +28 -0
  158. package/data/ux-source-evidence/account-mfa.json +62 -0
  159. package/data/ux-source-evidence/account-onboarding-credentials.json +52 -0
  160. package/data/ux-source-evidence/account-onboarding-legal.json +56 -0
  161. package/data/ux-source-evidence/account-onboarding-orientation.json +66 -0
  162. package/data/ux-source-evidence/account-onboarding-profile.json +50 -0
  163. package/data/ux-source-evidence/account-onboarding.json +69 -0
  164. package/data/ux-source-evidence/account-reset-password.json +27 -0
  165. package/data/ux-source-evidence/account-session-expired.json +62 -0
  166. package/data/ux-source-evidence/account-signup.json +33 -0
  167. package/data/ux-source-evidence/account-verify-email.json +34 -0
  168. package/data/ux-source-evidence/account-welcome.json +41 -0
  169. package/data/ux-source-evidence/admin-analytics.json +54 -0
  170. package/data/ux-source-evidence/admin-audit.json +31 -0
  171. package/data/ux-source-evidence/admin-broadcasts-new.json +36 -0
  172. package/data/ux-source-evidence/admin-broadcasts.json +41 -0
  173. package/data/ux-source-evidence/admin-closures.json +52 -0
  174. package/data/ux-source-evidence/admin-devices.json +51 -0
  175. package/data/ux-source-evidence/admin-escalations.json +35 -0
  176. package/data/ux-source-evidence/admin-incidents.json +54 -0
  177. package/data/ux-source-evidence/admin-loading.json +17 -0
  178. package/data/ux-source-evidence/admin-onboarding-detail.json +80 -0
  179. package/data/ux-source-evidence/admin-onboarding.json +62 -0
  180. package/data/ux-source-evidence/admin-payments.json +45 -0
  181. package/data/ux-source-evidence/admin-providers.json +38 -0
  182. package/data/ux-source-evidence/admin-settings.json +18 -0
  183. package/data/ux-source-evidence/admin-subscriptions-detail-allocate.json +42 -0
  184. package/data/ux-source-evidence/admin-subscriptions-detail.json +48 -0
  185. package/data/ux-source-evidence/admin-subscriptions-new.json +43 -0
  186. package/data/ux-source-evidence/admin-subscriptions.json +41 -0
  187. package/data/ux-source-evidence/admin-transactions.json +41 -0
  188. package/data/ux-source-evidence/error.json +30 -0
  189. package/data/ux-source-evidence/loading.json +19 -0
  190. package/data/ux-source-evidence/marketing-blog.json +48 -0
  191. package/data/ux-source-evidence/marketing-contact.json +36 -0
  192. package/data/ux-source-evidence/marketing.json +64 -0
  193. package/data/ux-source-evidence/not-found.json +28 -0
  194. package/data/ux-source-evidence/portal-loading.json +17 -0
  195. package/data/ux-source-evidence/portal-mentor-messages.json +31 -0
  196. package/data/ux-source-evidence/portal-mentor-profile.json +23 -0
  197. package/data/ux-source-evidence/portal-mentor-requests-detail.json +54 -0
  198. package/data/ux-source-evidence/portal-mentor-sessions-detail-assess.json +42 -0
  199. package/data/ux-source-evidence/portal-mentor-settings.json +18 -0
  200. package/data/ux-source-evidence/portal-mentor.json +38 -0
  201. package/data/ux-source-evidence/portal-sessions-detail-live.json +48 -0
  202. package/data/ux-source-evidence/portal-sponsor-billing.json +28 -0
  203. package/data/ux-source-evidence/portal-sponsor-profile.json +22 -0
  204. package/data/ux-source-evidence/portal-sponsor-sessions.json +28 -0
  205. package/data/ux-source-evidence/portal-sponsor-settings.json +18 -0
  206. package/data/ux-source-evidence/portal-sponsor-trainees.json +36 -0
  207. package/data/ux-source-evidence/portal-sponsor.json +52 -0
  208. package/data/ux-source-evidence/portal-trainee-competency.json +42 -0
  209. package/data/ux-source-evidence/portal-trainee-credits.json +40 -0
  210. package/data/ux-source-evidence/portal-trainee-mentors.json +48 -0
  211. package/data/ux-source-evidence/portal-trainee-messages.json +32 -0
  212. package/data/ux-source-evidence/portal-trainee-notifications.json +35 -0
  213. package/data/ux-source-evidence/portal-trainee-profile.json +26 -0
  214. package/data/ux-source-evidence/portal-trainee-request.json +54 -0
  215. package/data/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +36 -0
  216. package/data/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +42 -0
  217. package/data/ux-source-evidence/portal-trainee-sessions-detail.json +55 -0
  218. package/data/ux-source-evidence/portal-trainee-sessions.json +36 -0
  219. package/data/ux-source-evidence/portal-trainee-settings-notifications.json +38 -0
  220. package/data/ux-source-evidence/portal-trainee-settings.json +18 -0
  221. package/data/ux-source-evidence/portal-trainee.json +44 -0
  222. package/data/ux-source-evidence/portal-tv-detail.json +39 -0
  223. package/data/ux-source-evidence/portal-tv.json +44 -0
  224. package/data/ux-source-evidence/shared-admin-admin-shell.json +35 -0
  225. package/data/ux-source-evidence/shared-auth-auth-primitives.json +105 -0
  226. package/data/ux-source-evidence/shared-auth-auth-shell.json +25 -0
  227. package/data/ux-source-evidence/shared-auth-onboarding-primitives.json +52 -0
  228. package/data/ux-source-evidence/shared-auth-onboarding-shell.json +28 -0
  229. package/data/ux-source-evidence/shared-auth-status-indicator.json +41 -0
  230. package/data/ux-source-evidence/shared-portal-empty-state.json +24 -0
  231. package/data/ux-source-evidence/shared-portal-loading-skeleton.json +27 -0
  232. package/data/ux-source-evidence/shared-portal-notifications-dropdown.json +39 -0
  233. package/data/ux-source-evidence/shared-portal-portal-shell.json +36 -0
  234. package/data/ux-source-evidence/shared-portal-profile-form.json +32 -0
  235. package/data/ux-source-evidence/shared-portal-settings-view.json +81 -0
  236. package/data/ux-source-evidence/shared-portal-usage-sparkline.json +18 -0
  237. package/data/ux-source-evidence/shared-portal-user-menu.json +34 -0
  238. package/lib/api.mjs +230 -4
  239. package/package.json +2 -2
@@ -0,0 +1,1761 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ Button,
4
+ TextInput,
5
+ TextArea,
6
+ NativeSelect,
7
+ } from "@utopia-studio-design/design-system";
8
+ import {
9
+ WorkflowFields,
10
+ RecordTable,
11
+ StepChecklist,
12
+ StatStrip,
13
+ RatingFields,
14
+ TrendSummary,
15
+ MediaStage,
16
+ } from "../semantic-pattern-parts/WorkflowParts";
17
+ import "../semantic-pattern-parts/product-parts.css";
18
+ import { fieldsFor, workflowDefinitions, type WorkflowDefinition } from "./workflow-definitions";
19
+ import { L, workflowCopy as copy, type Copy } from "./workflow-copy";
20
+ import type { ExampleLocale } from "./copy";
21
+ import "./ux-examples.css";
22
+
23
+ export type WorkflowProps = { locale: ExampleLocale; initialState?: string };
24
+ const text = (locale: ExampleLocale) => (value: Copy) => value[locale];
25
+ const href = (id: string) => "#/templates/patterns/" + id;
26
+
27
+ /** The composition owns fixture state only. Reuse the registered parts with host data in products. */
28
+ export function WorkflowExample({
29
+ definition: d,
30
+ locale,
31
+ initialState = "default",
32
+ }: WorkflowProps & { definition: WorkflowDefinition }) {
33
+ const t = text(locale);
34
+ return (
35
+ <div
36
+ className="ux-example product-example"
37
+ data-slot="workflow-example"
38
+ data-workflow={d.id}
39
+ dir={locale === "ar" ? "rtl" : "ltr"}
40
+ >
41
+ <div className="product-content">
42
+ <header>
43
+ <h3>{t(d.title)}</h3>
44
+ <p>{t(d.description)}</p>
45
+ </header>
46
+ {d.kind === "form" ? (
47
+ <FormFlow d={d} locale={locale} initialState={initialState} />
48
+ ) : d.kind === "table" ? (
49
+ <TableFlow d={d} locale={locale} initialState={initialState} />
50
+ ) : d.kind === "checklist" || d.kind === "progress" ? (
51
+ <ChecklistFlow d={d} locale={locale} initialState={initialState} />
52
+ ) : d.kind === "rating" ? (
53
+ <AssessmentFlow d={d} locale={locale} initialState={initialState} />
54
+ ) : d.kind === "allocation" ? (
55
+ <AllocationFlow locale={locale} initialState={initialState} />
56
+ ) : d.kind === "dashboard" ? (
57
+ <DashboardFlow d={d} locale={locale} initialState={initialState} />
58
+ ) : d.kind === "settings" ? (
59
+ <SettingsFlow locale={locale} initialState={initialState} />
60
+ ) : d.kind === "media" ? (
61
+ <MediaFlow d={d} locale={locale} initialState={initialState} />
62
+ ) : d.kind === "articles" ? (
63
+ <ContentFlow d={d} locale={locale} initialState={initialState} />
64
+ ) : (
65
+ <RecoveryFlow d={d} locale={locale} initialState={initialState} />
66
+ )}
67
+ {d.related?.length ? (
68
+ <nav
69
+ className="product-actions"
70
+ aria-label={t(L("Related examples", "연결된 예시", "أمثلة مرتبطة"))}
71
+ >
72
+ {d.related.map((id) => (
73
+ <a className="ux-catalog__link" href={href(id)} key={id}>
74
+ {t(workflowDefinitions.find(item => item.id === id)?.title ?? L(id, id, id))}
75
+ </a>
76
+ ))}
77
+ </nav>
78
+ ) : null}
79
+ </div>
80
+ <footer>{t(copy.demo)}</footer>
81
+ </div>
82
+ );
83
+ }
84
+ type FlowProps = WorkflowProps & { d: WorkflowDefinition };
85
+ function Result({
86
+ locale,
87
+ message,
88
+ onReset,
89
+ }: {
90
+ locale: ExampleLocale;
91
+ message?: string;
92
+ onReset: () => void;
93
+ }) {
94
+ const t = text(locale);
95
+ return (
96
+ <section className="product-card" role="status">
97
+ <h4>{message || t(copy.done)}</h4>
98
+ <p>{t(copy.demo)}</p>
99
+ <Button variant="outline" onClick={onReset}>
100
+ {t(copy.reset)}
101
+ </Button>
102
+ </section>
103
+ );
104
+ }
105
+
106
+ function FormFlow({ d, locale, initialState = "default" }: FlowProps) {
107
+ const t = text(locale),
108
+ definitions = fieldsFor(d, initialState);
109
+ const [values, setValues] = useState<Record<string, string>>(initialState === 'specific' ? {mode: t(L('Choose a specific expert', '특정 전문가 선택', 'اختيار خبير محدد'))} : {}),
110
+ [errors, setErrors] = useState<Record<string, string>>(
111
+ initialState === "validation"
112
+ ? Object.fromEntries(
113
+ definitions
114
+ .filter((f) => f.required)
115
+ .map((f) => [f.id, t(copy.error)])
116
+ )
117
+ : {}
118
+ );
119
+ const [done, setDone] = useState(initialState === "success"),
120
+ [saved, setSaved] = useState(false);
121
+ const specific =
122
+ d.id === "booking-request-01" &&
123
+ (values.mode ===
124
+ t(
125
+ L("Choose a specific expert", "특정 전문가 선택", "اختيار خبير محدد")
126
+ ));
127
+ const fields = [
128
+ ...definitions,
129
+ ...(specific
130
+ ? [
131
+ {
132
+ id: "expert",
133
+ label: L("Expert", "전문가", "الخبير"),
134
+ type: "select" as const,
135
+ options: [
136
+ L("Alex Kim", "김아름", "أليكس كيم"),
137
+ L("Noor Ali", "누르 알리", "نور علي"),
138
+ ],
139
+ required: true,
140
+ },
141
+ ]
142
+ : []),
143
+ ];
144
+ const set = (id: string, value: string) => {
145
+ setValues((v) => {
146
+ // Tier changes update the dependent fixture fields in every locale.
147
+ const tier = fields.find(f => f.id === 'tier')?.options?.findIndex(option => t(option) === value) ?? -1
148
+ return {...v, [id]: value, ...(id === 'tier' && tier >= 0 ? {credits: String([100,250,600,0][tier]), fee: String([15000,15000,15000,0][tier])} : {})}
149
+ });
150
+ setErrors((e) => ({ ...e, [id]: "" }));
151
+ setSaved(false);
152
+ };
153
+ const validate = () => {
154
+ const next: Record<string, string> = {};
155
+ for (const f of fields) {
156
+ const v = values[f.id]?.trim() ?? "";
157
+ if (
158
+ (f.required && (!v || (f.type === "check" && v !== "true"))) ||
159
+ (v && f.minLength && v.length < f.minLength) ||
160
+ (v && f.type === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) ||
161
+ (v && f.type === "url" && !/^https?:\/\/[^\s]+$/.test(v)) ||
162
+ (v && f.type === 'select' && !f.options?.some(option => t(option) === v)) ||
163
+ (v && f.type === 'multi-select' && v.split('\n').some(value => !f.options?.some(option => t(option) === value))) ||
164
+ (v &&
165
+ f.type === "number" &&
166
+ (!Number.isFinite(Number(v)) ||
167
+ (f.min !== undefined && Number(v) < f.min) ||
168
+ (f.max !== undefined && Number(v) > f.max)))
169
+ )
170
+ next[f.id] = f.minLength
171
+ ? `${t(copy.error)} (${f.minLength}+)`
172
+ : t(copy.error);
173
+ }
174
+ setErrors(next);
175
+ return !Object.keys(next).length;
176
+ };
177
+ if (done)
178
+ return (
179
+ <Result
180
+ locale={locale}
181
+ onReset={() => {
182
+ setDone(false);
183
+ setValues({});
184
+ setErrors({});
185
+ }}
186
+ />
187
+ );
188
+ if (initialState === "no-credits")
189
+ return (
190
+ <section className="product-card" role="status">
191
+ <h4>
192
+ {t(
193
+ L(
194
+ "No available credits",
195
+ "사용 가능한 크레딧이 없어요",
196
+ "لا يوجد رصيد متاح"
197
+ )
198
+ )}
199
+ </h4>
200
+ <p>
201
+ {t(
202
+ L(
203
+ "Request an allocation before booking.",
204
+ "예약 전에 크레딧 배분을 요청하세요.",
205
+ "اطلب تخصيص رصيد قبل الحجز."
206
+ )
207
+ )}
208
+ </p>
209
+ <a href={href("credit-balance-01")}>
210
+ {t(L("View balance", "잔액 보기", "عرض الرصيد"))}
211
+ </a>
212
+ </section>
213
+ );
214
+ return (
215
+ <form
216
+ noValidate
217
+ onSubmit={(e) => {
218
+ e.preventDefault();
219
+ if (validate()) setDone(true);
220
+ }}
221
+ >
222
+ <WorkflowFields
223
+ fields={fields.map((f) => ({
224
+ ...f,
225
+ label: t(f.label),
226
+ options: f.options?.map(t),
227
+ }))}
228
+ values={values}
229
+ errors={errors}
230
+ onChange={set}
231
+ onFile={(id, file) => {
232
+ const allowed = fields.find((f) => f.id === id)?.accept ?? "";
233
+ const extension = file?.name.split(".").pop()?.toLowerCase();
234
+ const validType =
235
+ file &&
236
+ ((allowed.includes(file.type) && Boolean(file.type)) ||
237
+ (["pdf", "png", "jpg", "jpeg"].includes(extension ?? "") &&
238
+ allowed.includes("." + extension)));
239
+ if (!file) {
240
+ set(id, "");
241
+ return;
242
+ }
243
+ if (file.size > 5 * 1024 * 1024 || !validType) {
244
+ setValues((v) => ({ ...v, [id]: "" }));
245
+ setErrors((e) => ({ ...e, [id]: t(copy.fileError) }));
246
+ return;
247
+ }
248
+ set(id, file.name); // Metadata only; no file bytes leave this tab.
249
+ }}
250
+ />
251
+ {["subscription-create-01", "plan-selection-01"].includes(d.id) ? (
252
+ <StatStrip
253
+ items={[
254
+ {
255
+ label: t(L("Example credits", "예시 크레딧", "رصيد المثال")),
256
+ value:
257
+ values.credits ||
258
+ '—',
259
+ },
260
+ {
261
+ label: t(L("Example access fee", "예시 이용료", "رسوم المثال")),
262
+ value: values.fee || "0",
263
+ },
264
+ ]}
265
+ />
266
+ ) : null}
267
+ {Object.values(errors).some(Boolean) ? (
268
+ <p role="alert">{t(copy.error)}</p>
269
+ ) : null}
270
+ {saved ? <p role="status">{t(copy.saved)}</p> : null}
271
+ <div className="product-actions">
272
+ <Button type="submit">{t(copy.submit)}</Button>
273
+ <Button type="button" variant="outline" onClick={() => setSaved(true)}>
274
+ {t(copy.draft)}
275
+ </Button>
276
+ <Button
277
+ type="button"
278
+ variant="ghost"
279
+ onClick={() => {
280
+ setValues({});
281
+ setErrors({});
282
+ setSaved(false);
283
+ }}
284
+ >
285
+ {t(copy.cancel)}
286
+ </Button>
287
+ </div>
288
+ </form>
289
+ );
290
+ }
291
+
292
+ function TableFlow({ d, locale, initialState = "default" }: FlowProps) {
293
+ const t = text(locale),
294
+ [query, setQuery] = useState(
295
+ initialState === "empty" ? "no matching fixture" : ""
296
+ ),
297
+ [status, setStatus] = useState(d.tabStatuses?.[0] ? t(d.tabStatuses[0]) : "all"),
298
+ [category, setCategory] = useState("all"),
299
+ [sort, setSort] = useState("newest");
300
+ const [rows, setRows] = useState(
301
+ (d.rows ?? []).map((cells, i) => ({
302
+ id: `record-${i}`,
303
+ cells: cells.map(t),
304
+ }))
305
+ );
306
+ const [opened, setOpened] = useState(""),
307
+ [confirmed, setConfirmed] = useState(false),
308
+ [assignee, setAssignee] = useState(""),
309
+ [failed, setFailed] = useState(initialState === "load-error"),
310
+ [message, setMessage] = useState(""),
311
+ [documentName, setDocumentName] = useState(""),
312
+ [messages, setMessages] = useState<string[]>([]),
313
+ [declined, setDeclined] = useState(initialState === "declined"),
314
+ [decision, setDecision] = useState(initialState === "accepted");
315
+ const [tab, setTab] = useState(
316
+ initialState === "documents" ? 1 : initialState === "conversation" ? 2 : 0
317
+ );
318
+ const selected = rows.find((r) => r.id === opened);
319
+ const filtered = rows.filter(
320
+ (r) =>
321
+ (status === "all" || r.cells.at(-1) === status) &&
322
+ (category === "all" || r.cells[1] === category) &&
323
+ r.cells
324
+ .join(" ")
325
+ .toLocaleLowerCase(locale)
326
+ .includes(query.toLocaleLowerCase(locale))
327
+ );
328
+ const dateIndex = d.columns?.findIndex(column => column.en === 'Date') ?? -1
329
+ const ordered = [...filtered].sort((a,b) => {
330
+ const comparison = dateIndex >= 0 ? a.cells[dateIndex].localeCompare(b.cells[dateIndex]) : a.cells[0].localeCompare(b.cells[0], locale)
331
+ return sort === 'oldest' ? comparison : -comparison
332
+ });
333
+ const statuses = [...new Set(rows.map((r) => r.cells.at(-1) ?? ""))],
334
+ categories = [...new Set(rows.map((r) => r.cells[1] ?? ""))];
335
+ const exportCsv = () => {
336
+ const escaped = (v: string) => `"${v.replace(/"/g, '""')}"`;
337
+ const data = [(d.columns ?? []).map(t), ...ordered.map((r) => r.cells)]
338
+ .map((r) => r.map(escaped).join(","))
339
+ .join("\n");
340
+ const url = URL.createObjectURL(
341
+ new Blob([data], { type: "text/csv;charset=utf-8" })
342
+ );
343
+ const a = document.createElement("a");
344
+ a.href = url;
345
+ a.download = d.id + "-example.csv";
346
+ a.click();
347
+ URL.revokeObjectURL(url);
348
+ };
349
+ if (failed)
350
+ return (
351
+ <section className="product-card" role="alert">
352
+ <p>
353
+ {t(
354
+ L(
355
+ "Records could not be loaded.",
356
+ "목록을 불러오지 못했어요.",
357
+ "تعذر تحميل السجلات."
358
+ )
359
+ )}
360
+ </p>
361
+ <Button onClick={() => setFailed(false)}>
362
+ {t(L("Retry", "다시 시도", "إعادة المحاولة"))}
363
+ </Button>
364
+ </section>
365
+ );
366
+ if (d.id === "session-detail-01")
367
+ return (
368
+ <>
369
+ <StatStrip
370
+ items={[
371
+ {
372
+ label: t(L("Session", "세션", "الجلسة")),
373
+ value: t(L("Design review", "디자인 리뷰", "مراجعة التصميم")),
374
+ },
375
+ {
376
+ label: t(L("Scheduled", "예정", "موعد")),
377
+ value: "2026-09-14 10:00",
378
+ },
379
+ ]}
380
+ />
381
+ <div className="workflow-tabs">
382
+ {d.tabs?.map((label, i) => (
383
+ <Button
384
+ key={i}
385
+ variant="outline"
386
+ aria-pressed={tab === i}
387
+ onClick={() => setTab(i)}
388
+ >
389
+ {t(label)}
390
+ </Button>
391
+ ))}
392
+ </div>
393
+ {tab === 0 ? (
394
+ <section className="product-card">
395
+ <h4>
396
+ {t(L("Shared preparation", "공동 준비 사항", "التحضير المشترك"))}
397
+ </h4>
398
+ {["Learning goals", "Strategy", "Challenges", "Contingency"].map(
399
+ (_, i) => (
400
+ <p key={i}>
401
+ {t(
402
+ [
403
+ L(
404
+ "Goals: validate the first-run experience.",
405
+ "목표: 첫 사용 경험 검증.",
406
+ "الأهداف: التحقق من تجربة الاستخدام الأولى."
407
+ ),
408
+ L(
409
+ "Strategy: review the prototype together.",
410
+ "전략: 프로토타입 공동 리뷰.",
411
+ "الاستراتيجية: مراجعة النموذج معاً."
412
+ ),
413
+ L(
414
+ "Challenge: first-time users need context.",
415
+ "과제: 신규 사용자에게 맥락 전달.",
416
+ "التحدي: يحتاج المستخدم الجديد للسياق."
417
+ ),
418
+ L(
419
+ "Fallback: continue with the shared notes.",
420
+ "대안: 공유 메모로 이어가기.",
421
+ "البديل: المتابعة بالملاحظات المشتركة."
422
+ ),
423
+ ][i]
424
+ )}
425
+ </p>
426
+ )
427
+ )}
428
+ </section>
429
+ ) : tab === 1 ? (
430
+ <WorkflowFields
431
+ fields={[
432
+ {
433
+ id: "document",
434
+ label: t(L("Add document", "문서 추가", "إضافة مستند")),
435
+ type: "file",
436
+ accept: ".pdf",
437
+ },
438
+ ]}
439
+ values={{ document: documentName }}
440
+ onChange={() => {}}
441
+ onFile={(_, file) => setDocumentName(file?.name ?? "")}
442
+ />
443
+ ) : (
444
+ <div className="product-card">
445
+ <ul aria-live="polite">
446
+ {messages.map((m, i) => (
447
+ <li key={i}>{m}</li>
448
+ ))}
449
+ </ul>
450
+ <form
451
+ onSubmit={(e) => {
452
+ e.preventDefault();
453
+ if (message.trim()) {
454
+ setMessages((m) => [...m, message.trim()]);
455
+ setMessage("");
456
+ }
457
+ }}
458
+ >
459
+ <label>
460
+ {t(copy.message)}
461
+ <TextInput
462
+ value={message}
463
+ onChange={(e) => setMessage(e.target.value)}
464
+ />
465
+ </label>
466
+ <Button type="submit" disabled={!message.trim()}>
467
+ {t(copy.send)}
468
+ </Button>
469
+ </form>
470
+ </div>
471
+ )}
472
+ </>
473
+ );
474
+ if (d.id === "request-decision-01")
475
+ return (
476
+ <>
477
+ {decision || declined ? (
478
+ <Result
479
+ locale={locale}
480
+ message={t(
481
+ decision
482
+ ? L(
483
+ "Request accepted in preview.",
484
+ "미리보기에서 수락했어요.",
485
+ "قُبل الطلب في المعاينة."
486
+ )
487
+ : L(
488
+ "Request declined in preview.",
489
+ "미리보기에서 거절했어요.",
490
+ "رُفض الطلب في المعاينة."
491
+ )
492
+ )}
493
+ onReset={() => {
494
+ setDecision(false);
495
+ setDeclined(false);
496
+ }}
497
+ />
498
+ ) : (
499
+ <section className="product-card">
500
+ <h4>
501
+ {t(
502
+ L(
503
+ "Design review with Alex",
504
+ "Alex와 디자인 리뷰",
505
+ "مراجعة التصميم مع أليكس"
506
+ )
507
+ )}
508
+ </h4>
509
+ <p>2026-09-14 · 10:00</p>
510
+ <p>
511
+ {t(
512
+ L(
513
+ "Goals: improve first-run navigation. Supporting documents and previous feedback are reviewed before a decision.",
514
+ "목표: 첫 사용 탐색 개선. 결정 전에 참고 문서와 이전 피드백을 검토합니다.",
515
+ "الهدف: تحسين التنقل الأول. راجع المستندات والملاحظات السابقة قبل القرار."
516
+ )
517
+ )}
518
+ </p>
519
+ <div className="product-actions">
520
+ <Button onClick={() => setDecision(true)}>{t(d.action!)}</Button>
521
+ <Button variant="outline" onClick={() => setDeclined(true)}>
522
+ {t(L("Decline", "거절", "رفض"))}
523
+ </Button>
524
+ </div>
525
+ </section>
526
+ )}
527
+ </>
528
+ );
529
+ return (
530
+ <>
531
+ <div className="product-toolbar">
532
+ <label>
533
+ {t(copy.search)}
534
+ <TextInput
535
+ type="search"
536
+ value={query}
537
+ onChange={(e) => setQuery(e.target.value)}
538
+ />
539
+ </label>
540
+ <label>
541
+ {t(copy.status)}
542
+ <NativeSelect
543
+ aria-label={t(copy.status)}
544
+ value={status}
545
+ onChange={(e) => {
546
+ setStatus(e.target.value);
547
+ setTab(d.tabStatuses?.findIndex(value => t(value) === e.target.value) ?? -1);
548
+ }}
549
+ >
550
+ <option value="all">{t(copy.all)}</option>
551
+ {statuses.map((v) => (
552
+ <option key={v}>{v}</option>
553
+ ))}
554
+ </NativeSelect>
555
+ </label>
556
+ <label>
557
+ {t(copy.category)}
558
+ <NativeSelect
559
+ aria-label={t(copy.category)}
560
+ value={category}
561
+ onChange={(e) => setCategory(e.target.value)}
562
+ >
563
+ <option value="all">{t(copy.all)}</option>
564
+ {categories.map((v) => (
565
+ <option key={v}>{v}</option>
566
+ ))}
567
+ </NativeSelect>
568
+ </label>
569
+ <label>
570
+ {t(copy.sort)}
571
+ <NativeSelect
572
+ aria-label={t(copy.sort)}
573
+ value={sort}
574
+ onChange={(e) => setSort(e.target.value)}
575
+ >
576
+ <option value="newest">{t(copy.newest)}</option>
577
+ <option value="oldest">{t(copy.oldest)}</option>
578
+ </NativeSelect>
579
+ </label>
580
+ </div>
581
+ <div className="product-actions">
582
+ <Button
583
+ variant="outline"
584
+ onClick={() => {
585
+ setQuery("");
586
+ setStatus("all");
587
+ setTab(-1);
588
+ setCategory("all");
589
+ }}
590
+ >
591
+ {t(copy.reset)}
592
+ </Button>
593
+ <Button variant="outline" onClick={exportCsv}>
594
+ {t(copy.export)}
595
+ </Button>
596
+ </div>
597
+ {d.tabs?.length ? (
598
+ <div className="workflow-tabs">
599
+ {d.tabs.map((label, i) => (
600
+ <Button
601
+ key={i}
602
+ variant="outline"
603
+ aria-pressed={tab === i}
604
+ onClick={() => {
605
+ setTab(i);
606
+ setStatus(d.tabStatuses?.[i] ? t(d.tabStatuses[i]) : statuses[i] ?? "all");
607
+ }}
608
+ >
609
+ {t(label)}
610
+ </Button>
611
+ ))}
612
+ </div>
613
+ ) : null}
614
+ <RecordTable
615
+ caption={t(d.title)}
616
+ columns={(d.columns ?? []).map(t)}
617
+ rows={ordered}
618
+ emptyLabel={t(copy.empty)}
619
+ actionLabel={t(copy.open)}
620
+ onOpen={(id) => {
621
+ setOpened(id);
622
+ setConfirmed(false);
623
+ }}
624
+ />
625
+ {selected ? (
626
+ <section className="product-card" aria-label={t(copy.open)}>
627
+ <h4>{selected.cells[0]}</h4>
628
+ {selected.cells.slice(1).map((v, i) => (
629
+ <p key={i}>
630
+ {t(d.columns![i + 1])}: {v}
631
+ </p>
632
+ ))}
633
+ {d.action ? (
634
+ <>
635
+ <p>{t(copy.confirmHint)}</p>
636
+ {d.id === "matching-escalations-01" ? (
637
+ <label>
638
+ {t(
639
+ L("Eligible expert", "배정 가능한 전문가", "الخبير المؤهل")
640
+ )}
641
+ <NativeSelect
642
+ value={assignee}
643
+ onChange={(e) => setAssignee(e.target.value)}
644
+ >
645
+ <option value="">—</option>
646
+ <option>Alex Kim</option>
647
+ <option>Noor Ali</option>
648
+ </NativeSelect>
649
+ </label>
650
+ ) : null}
651
+ <Button
652
+ disabled={
653
+ confirmed ||
654
+ (d.id === 'payment-cycle-01' && selected.cells.at(-1) === t(L('Paid', '지급 완료', 'مدفوع'))) ||
655
+ (d.id === 'broadcast-management-01' && selected.cells.at(-1) === t(copy.ended)) ||
656
+ (d.id === "matching-escalations-01" && !assignee) ||
657
+ (d.id === "closure-queue-01" &&
658
+ selected.cells.at(-1) !==
659
+ t(L("Completed", "완료", "مكتمل")))
660
+ }
661
+ onClick={() => {
662
+ setConfirmed(true);
663
+ if (d.id === "provider-connections-01") return;
664
+ const next =
665
+ d.id === "payment-cycle-01"
666
+ ? selected.cells.at(-1) ===
667
+ t(L("Queued", "대기", "في الانتظار"))
668
+ ? t(L("Invoiced", "청구됨", "مفوتر"))
669
+ : t(L("Paid", "지급 완료", "مدفوع"))
670
+ : d.id === "broadcast-management-01"
671
+ ? selected.cells.at(-1) === t(L("Draft", "초안", "مسودة"))
672
+ ? t(L("Scheduled", "예약됨", "مجدول"))
673
+ : selected.cells.at(-1) === t(L('Scheduled','예약됨','مجدول')) ? t(L('Live','방송 중','مباشر')) : t(copy.ended)
674
+ : t(L("Completed", "완료", "مكتمل"));
675
+ setRows((rs) =>
676
+ rs.map((r) =>
677
+ r.id === selected.id
678
+ ? { ...r, cells: [...r.cells.slice(0, -1), next] }
679
+ : r
680
+ )
681
+ );
682
+ }}
683
+ >
684
+ {t(d.action)}
685
+ </Button>
686
+ </>
687
+ ) : null}
688
+ {confirmed ? <p role="status">{t(copy.done)}</p> : null}
689
+ <Button variant="ghost" onClick={() => setOpened("")}>
690
+ {t(copy.close)}
691
+ </Button>
692
+ </section>
693
+ ) : null}
694
+ </>
695
+ );
696
+ }
697
+
698
+ function ChecklistFlow({ d, locale, initialState = "default" }: FlowProps) {
699
+ const t = text(locale),
700
+ checks = d.checks ?? [],
701
+ [values, setValues] = useState(
702
+ checks.map((_, i) =>
703
+ initialState === "complete"
704
+ ? "pass"
705
+ : initialState === "failed" && i === 0
706
+ ? "fail"
707
+ : ['midway','review'].includes(initialState) && i === 0
708
+ ? "pass"
709
+ : "pending"
710
+ )
711
+ ),
712
+ [finished, setFinished] = useState(false),
713
+ [notes, setNotes] = useState("");
714
+ const allPass = values.every((v) => v === "pass"),
715
+ progress = values.filter((v) => v === "pass").length;
716
+ if (finished)
717
+ return (
718
+ <Result
719
+ locale={locale}
720
+ onReset={() => {
721
+ setFinished(false);
722
+ setValues(checks.map(() => "pending"));
723
+ }}
724
+ />
725
+ );
726
+ return (
727
+ <>
728
+ <StatStrip
729
+ items={[
730
+ {
731
+ label: t(L("Completed steps", "완료한 항목", "الخطوات المكتملة")),
732
+ value: `${progress} / ${checks.length}`,
733
+ },
734
+ ]}
735
+ />
736
+ {initialState === "review" ? (
737
+ <p role="status">
738
+ {t(
739
+ L(
740
+ "Waiting for the reviewer. Completed steps are preserved.",
741
+ "검토 담당자를 기다리는 중입니다. 완료한 단계는 유지됩니다.",
742
+ "بانتظار المراجع. تُحفظ الخطوات المكتملة."
743
+ )
744
+ )}
745
+ </p>
746
+ ) : null}
747
+ {d.kind === "progress" ? (
748
+ <ol className="workflow-checklist">
749
+ {checks.map((c, i) => (
750
+ <li className="product-card" key={i}>
751
+ <h4>{t(c)}</h4>
752
+ {i > progress || (initialState === 'review' && i === progress) ? (
753
+ <p>{t(copy.locked)}</p>
754
+ ) : (
755
+ <a href={href(d.related![i])}>{t(copy.next)}</a>
756
+ )}
757
+ </li>
758
+ ))}
759
+ </ol>
760
+ ) : (
761
+ <StepChecklist
762
+ items={checks.map((c, i) => ({
763
+ id: String(i),
764
+ label: t(c),
765
+ description:
766
+ values[i] === "fail"
767
+ ? t(
768
+ L(
769
+ "Retry this check after resolving the issue. No support request was sent.",
770
+ "문제를 해결한 뒤 다시 확인하세요. 지원 요청은 전송하지 않았습니다.",
771
+ "أعد الفحص بعد حل المشكلة. لم يُرسل طلب دعم."
772
+ )
773
+ )
774
+ : t(copy.demo),
775
+ state: values[i],
776
+ disabled:
777
+ d.id === "verification-checklist-01" &&
778
+ i === checks.length - 1 &&
779
+ values.slice(0, -1).some((v) => v !== "pass"),
780
+ }))}
781
+ passLabel={t(copy.pass)}
782
+ failLabel={t(copy.fail)}
783
+ onChange={(id, state) =>
784
+ setValues((v) => v.map((x, i) => (i === Number(id) ? state : x)))
785
+ }
786
+ />
787
+ )}
788
+ {d.id === "verification-checklist-01" ? (
789
+ <label>
790
+ {t(copy.note)}
791
+ <TextArea value={notes} onChange={(e) => setNotes(e.target.value)} />
792
+ </label>
793
+ ) : null}
794
+ <Button disabled={!allPass} onClick={() => setFinished(true)}>
795
+ {t(copy.next)}
796
+ </Button>
797
+ </>
798
+ );
799
+ }
800
+
801
+ function AssessmentFlow({ d, locale, initialState }: FlowProps) {
802
+ const t = text(locale),
803
+ [scores, setScores] = useState<number[]>((d.checks ?? []).map(() => 0)),
804
+ [feedback, setFeedback] = useState(""),
805
+ [recommendation, setRecommendation] = useState(""),
806
+ [level, setLevel] = useState(""),
807
+ [done, setDone] = useState(initialState === "complete"),
808
+ [draft, setDraft] = useState(false);
809
+ const structured = d.id === "structured-assessment-01",
810
+ all = scores.every(Boolean),
811
+ canSubmit =
812
+ all &&
813
+ (!structured ||
814
+ (feedback.trim().length >= 200 && recommendation.trim() && level));
815
+ if (done)
816
+ return (
817
+ <Result
818
+ locale={locale}
819
+ onReset={() => {
820
+ setDone(false);
821
+ setScores(scores.map(() => 0));
822
+ setFeedback("");
823
+ setRecommendation("");
824
+ setLevel("");
825
+ }}
826
+ />
827
+ );
828
+ return (
829
+ <>
830
+ <RatingFields
831
+ labels={(d.checks ?? []).map(t)}
832
+ values={scores}
833
+ onChange={(i, n) => {
834
+ setScores((s) => s.map((v, j) => (i === j ? n : v)));
835
+ setDraft(false);
836
+ }}
837
+ />
838
+ <StatStrip
839
+ items={[
840
+ {
841
+ label: t(L("Average", "평균", "المتوسط")),
842
+ value: all
843
+ ? (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1)
844
+ : "—",
845
+ },
846
+ ]}
847
+ />
848
+ {structured ? (
849
+ <label>
850
+ {t(L("Supervision level", "지도 수준", "مستوى الإشراف"))}
851
+ <NativeSelect
852
+ value={level}
853
+ aria-label={t(L('Supervision level', '지도 수준', 'مستوى الإشراف'))}
854
+ onChange={(e) => setLevel(e.target.value)}
855
+ >
856
+ <option value="">—</option>
857
+ {[
858
+ L("Observe", "관찰", "مراقبة"),
859
+ L("Assist", "보조", "مساعدة"),
860
+ L("Supervised", "지도하 수행", "تحت إشراف"),
861
+ L("Independent", "독립 수행", "مستقل"),
862
+ ].map((c) => (
863
+ <option key={c.en}>{t(c)}</option>
864
+ ))}
865
+ </NativeSelect>
866
+ </label>
867
+ ) : null}
868
+ <label>
869
+ {t(L("Feedback", "피드백", "الملاحظات"))}
870
+ {structured ? " (200+)" : ""}
871
+ <TextArea
872
+ aria-label={`${t(L("Feedback", "피드백", "الملاحظات"))}${structured ? " (200+)" : ""}`}
873
+ maxLength={structured ? 1000 : 500}
874
+ value={feedback}
875
+ onChange={(e) => {
876
+ setFeedback(e.target.value);
877
+ setDraft(false);
878
+ }}
879
+ />
880
+ </label>
881
+ {structured ? (
882
+ <label>
883
+ {t(
884
+ L(
885
+ "Next-step recommendations",
886
+ "다음 단계 권고",
887
+ "توصيات الخطوة التالية"
888
+ )
889
+ )}
890
+ <TextArea
891
+ maxLength={500}
892
+ value={recommendation}
893
+ onChange={(e) => setRecommendation(e.target.value)}
894
+ />
895
+ </label>
896
+ ) : null}
897
+ <div className="product-actions">
898
+ <Button disabled={!canSubmit} onClick={() => setDone(true)}>
899
+ {t(copy.submit)}
900
+ </Button>
901
+ <Button variant="outline" onClick={() => setDraft(true)}>
902
+ {t(copy.draft)}
903
+ </Button>
904
+ </div>
905
+ {draft ? <p role="status">{t(copy.saved)}</p> : null}
906
+ </>
907
+ );
908
+ }
909
+
910
+ function AllocationFlow({ locale, initialState }: WorkflowProps) {
911
+ const t = text(locale),
912
+ [amounts, setAmounts] = useState([
913
+ initialState === "over-budget" ? 30 : 0,
914
+ 0,
915
+ ]),
916
+ [done, setDone] = useState(initialState === "complete"),
917
+ total = amounts.reduce((a, b) => a + b, 0),
918
+ budget = 20;
919
+ if (done)
920
+ return (
921
+ <Result
922
+ locale={locale}
923
+ onReset={() => {
924
+ setDone(false);
925
+ setAmounts([0, 0]);
926
+ }}
927
+ />
928
+ );
929
+ return (
930
+ <>
931
+ <StatStrip
932
+ items={[
933
+ { label: t(copy.available), value: String(budget) },
934
+ { label: t(copy.total), value: String(total) },
935
+ { label: t(copy.remaining), value: String(budget - total) },
936
+ ]}
937
+ />
938
+ <WorkflowFields
939
+ fields={["Alex Kim", "Noor Ali"].map((label, i) => ({
940
+ id: String(i),
941
+ label,
942
+ type: "number",
943
+ min: 0,
944
+ max: budget,
945
+ }))}
946
+ values={Object.fromEntries(amounts.map((v, i) => [i, String(v)]))}
947
+ onChange={(id, value) =>
948
+ setAmounts((a) =>
949
+ a.map((v, i) =>
950
+ i === Number(id) ? Math.max(0, Math.floor(Number(value) || 0)) : v
951
+ )
952
+ )
953
+ }
954
+ />
955
+ {total > budget ? (
956
+ <p role="alert">
957
+ {t(
958
+ L(
959
+ "Allocation exceeds the available pool.",
960
+ "배분 합계가 잔액을 초과해요.",
961
+ "التوزيع يتجاوز الرصيد المتاح."
962
+ )
963
+ )}
964
+ </p>
965
+ ) : null}
966
+ <Button
967
+ disabled={total <= 0 || total > budget}
968
+ onClick={() => setDone(true)}
969
+ >
970
+ {t(copy.confirm)}
971
+ </Button>
972
+ </>
973
+ );
974
+ }
975
+
976
+ function DashboardFlow({ d, locale, initialState }: FlowProps) {
977
+ const t = text(locale),
978
+ [tab, setTab] = useState(0),
979
+ empty = initialState === "empty",
980
+ skill = d.id === "skill-progression-01",
981
+ expert = d.id === "expert-overview-01",
982
+ analytics = d.id === "analytics-01";
983
+ const labels = skill
984
+ ? [
985
+ L("Sessions", "세션", "الجلسات"),
986
+ L("Average rating", "평균 점수", "متوسط التقييم"),
987
+ L("Milestones", "마일스톤", "المراحل"),
988
+ ]
989
+ : expert
990
+ ? [
991
+ L("Available requests", "가능한 요청", "الطلبات المتاحة"),
992
+ L("Upcoming sessions", "예정 세션", "الجلسات القادمة"),
993
+ L("Rating", "평점", "التقييم"),
994
+ ]
995
+ : analytics
996
+ ? [
997
+ L("Completed sessions", "완료 세션", "الجلسات المكتملة"),
998
+ L("Active people", "활성 사용자", "الأشخاص النشطون"),
999
+ L("On-time assessments", "기한 내 평가", "تقييمات في الموعد"),
1000
+ ]
1001
+ : [
1002
+ L("Allocated", "배분", "المخصص"),
1003
+ L("In use", "사용 중", "قيد الاستخدام"),
1004
+ L("Consumed", "소진", "المستهلك"),
1005
+ ];
1006
+ const tabs = [
1007
+ L("Overview", "현황", "نظرة عامة"),
1008
+ skill
1009
+ ? L("Skill trends", "역량 추세", "اتجاه المهارات")
1010
+ : L("Trends", "추세", "الاتجاهات"),
1011
+ skill
1012
+ ? L("Milestones", "마일스톤", "المراحل")
1013
+ : analytics
1014
+ ? L("Risk signals", "위험 신호", "إشارات المخاطر")
1015
+ : L("History", "내역", "السجل"),
1016
+ ];
1017
+ return (
1018
+ <>
1019
+ {initialState === "low-balance" ? (
1020
+ <p role="status">
1021
+ {t(
1022
+ L(
1023
+ "The available balance is low. Review the allocation before scheduling.",
1024
+ "남은 잔액이 적어요. 일정을 잡기 전에 배분을 확인하세요.",
1025
+ "الرصيد منخفض. راجع التخصيص قبل الجدولة."
1026
+ )
1027
+ )}
1028
+ </p>
1029
+ ) : null}
1030
+ <StatStrip
1031
+ items={labels.map((label, i) => ({
1032
+ label: t(label),
1033
+ value: empty ? '0' : (skill ? ['4','3.8 / 5','2'] : expert ? ['3','2','4.8 / 5'] : analytics ? ['56','33','96%'] : ['20','4','12'])[i],
1034
+ }))}
1035
+ />
1036
+ <div className="workflow-tabs">
1037
+ {tabs.map((c, i) => (
1038
+ <Button
1039
+ variant="outline"
1040
+ aria-pressed={tab === i}
1041
+ key={i}
1042
+ onClick={() => setTab(i)}
1043
+ >
1044
+ {t(c)}
1045
+ </Button>
1046
+ ))}
1047
+ </div>
1048
+ {empty ? (
1049
+ <p role="status">{t(copy.empty)}</p>
1050
+ ) : tab === 1 ? (
1051
+ <TrendSummary
1052
+ title={t(tabs[1])}
1053
+ items={["Apr", "May", "Jun", "Jul"].map((label, i) => ({
1054
+ label,
1055
+ value: skill ? [2.7,3.1,3.8,4.2][i] : [4, 11, 22, 38][i],
1056
+ max: skill ? 5 : 40,
1057
+ }))}
1058
+ />
1059
+ ) : tab === 2 && skill ? (
1060
+ <ul className="workflow-checklist">
1061
+ {[
1062
+ L(
1063
+ "First session completed",
1064
+ "첫 세션 완료",
1065
+ "اكتملت الجلسة الأولى"
1066
+ ),
1067
+ L("Five sessions — 4 / 5", "5회 세션 — 4 / 5", "خمس جلسات — 4 / 5"),
1068
+ L(
1069
+ "Supervised practice achieved",
1070
+ "지도하 수행 달성",
1071
+ "تحققت الممارسة تحت إشراف"
1072
+ ),
1073
+ ].map((c) => (
1074
+ <li className="product-card" key={c.en}>
1075
+ {t(c)}
1076
+ </li>
1077
+ ))}
1078
+ </ul>
1079
+ ) : tab === 2 && analytics ? (
1080
+ <section className="product-card">
1081
+ <h4>
1082
+ {t(
1083
+ L(
1084
+ "Progress needs attention",
1085
+ "성장 추이 확인 필요",
1086
+ "التقدم يحتاج انتباهاً"
1087
+ )
1088
+ )}
1089
+ </h4>
1090
+ <p>
1091
+ {t(
1092
+ L(
1093
+ "The latest two ratings are lower. Review the assessment details.",
1094
+ "최근 두 평가 점수가 낮아졌습니다. 상세 평가를 확인하세요.",
1095
+ "انخفض آخر تقييمين. راجع تفاصيل التقييم."
1096
+ )
1097
+ )}
1098
+ </p>
1099
+ <a href={href("structured-assessment-01")}>{t(copy.open)}</a>
1100
+ </section>
1101
+ ) : (
1102
+ <RecordTable
1103
+ caption={t(tabs[tab])}
1104
+ columns={(d.columns ?? []).map(t)}
1105
+ rows={(d.rows ?? []).map((r, i) => ({
1106
+ id: String(i),
1107
+ cells: r.map(t),
1108
+ }))}
1109
+ emptyLabel={t(copy.empty)}
1110
+ actionLabel={t(copy.open)}
1111
+ />
1112
+ )}
1113
+ </>
1114
+ );
1115
+ }
1116
+
1117
+ function SettingsFlow({ locale, initialState = "account" }: WorkflowProps) {
1118
+ const t = text(locale),
1119
+ tabs = [
1120
+ "account",
1121
+ "notifications",
1122
+ "security",
1123
+ "appearance",
1124
+ "devices",
1125
+ "closure",
1126
+ ],
1127
+ labels = [
1128
+ L("Account", "계정", "الحساب"),
1129
+ L("Notifications", "알림", "الإشعارات"),
1130
+ L("Security", "보안", "الأمان"),
1131
+ L("Appearance", "표시", "المظهر"),
1132
+ L("Devices", "기기", "الأجهزة"),
1133
+ L("Close account", "계정 종료", "إغلاق الحساب"),
1134
+ ];
1135
+ const [tab, setTab] = useState(initialState),
1136
+ [values, setValues] = useState<Record<string, string>>({
1137
+ name: "Alex Kim",
1138
+ email: "alex@example.test",
1139
+ }),
1140
+ [saved, setSaved] = useState(false),
1141
+ [devices, setDevices] = useState(["Laptop · Chrome", "Phone · Safari"]),
1142
+ [confirm, setConfirm] = useState(""),
1143
+ [closed, setClosed] = useState(false);
1144
+ const fields =
1145
+ tab === "account"
1146
+ ? [
1147
+ { id: "name", label: t(L("Name", "이름", "الاسم")) },
1148
+ {
1149
+ id: "email",
1150
+ label: t(L("Email", "이메일", "البريد")),
1151
+ type: "email" as const,
1152
+ },
1153
+ ]
1154
+ : tab === "notifications"
1155
+ ? ["Weekly digest", "Browser notifications", "Product news"].map(
1156
+ (_, i) => ({
1157
+ id: "n" + i,
1158
+ label: t(
1159
+ [
1160
+ L("Weekly digest", "주간 요약", "ملخص أسبوعي"),
1161
+ L("Browser notifications", "브라우저 알림", "إشعارات المتصفح"),
1162
+ L("Product news", "제품 소식", "أخبار المنتج"),
1163
+ ][i]
1164
+ ),
1165
+ type: "check" as const,
1166
+ })
1167
+ )
1168
+ : tab === "appearance"
1169
+ ? [
1170
+ {
1171
+ id: "theme",
1172
+ label: t(L("Color mode", "색상 모드", "وضع الألوان")),
1173
+ type: "select" as const,
1174
+ options: [
1175
+ t(L("System", "시스템", "النظام")),
1176
+ t(L("Light", "밝게", "فاتح")),
1177
+ t(L("Dark", "어둡게", "داكن")),
1178
+ ],
1179
+ },
1180
+ {
1181
+ id: "density",
1182
+ label: t(L("Density", "밀도", "الكثافة")),
1183
+ type: "select" as const,
1184
+ options: [
1185
+ t(L("Comfortable", "편안하게", "مريح")),
1186
+ t(L("Compact", "촘촘하게", "مضغوط")),
1187
+ ],
1188
+ },
1189
+ ]
1190
+ : [];
1191
+ return (
1192
+ <>
1193
+ <div className="workflow-tabs">
1194
+ {tabs.map((key, i) => (
1195
+ <Button
1196
+ key={key}
1197
+ variant="outline"
1198
+ aria-pressed={tab === key}
1199
+ onClick={() => {
1200
+ setTab(key);
1201
+ setSaved(false);
1202
+ }}
1203
+ >
1204
+ {t(labels[i])}
1205
+ </Button>
1206
+ ))}
1207
+ </div>
1208
+ {fields.length ? (
1209
+ <WorkflowFields
1210
+ fields={fields}
1211
+ values={values}
1212
+ onChange={(id, value) => {
1213
+ setValues((v) => ({ ...v, [id]: value }));
1214
+ setSaved(false);
1215
+ }}
1216
+ />
1217
+ ) : tab === "security" ? (
1218
+ <div className="product-card">
1219
+ <a href={href("password-reset-01")}>
1220
+ {t(L("Change password", "비밀번호 변경", "تغيير كلمة المرور"))}
1221
+ </a>
1222
+ <a href={href("two-factor-01")}>
1223
+ {t(
1224
+ L("Two-factor authentication", "2단계 인증", "المصادقة الثنائية")
1225
+ )}
1226
+ </a>
1227
+ <Button variant="outline" onClick={() => setTab("devices")}>
1228
+ {t(
1229
+ L(
1230
+ "Review active devices",
1231
+ "접속 기기 검토",
1232
+ "مراجعة الأجهزة النشطة"
1233
+ )
1234
+ )}
1235
+ </Button>
1236
+ </div>
1237
+ ) : tab === "devices" ? (
1238
+ <ul className="workflow-checklist">
1239
+ {devices.map((device, i) => (
1240
+ <li className="product-card" key={device}>
1241
+ <strong>{device}</strong>
1242
+ {i === 0 ? (
1243
+ <p>{t(L("Current device", "현재 기기", "الجهاز الحالي"))}</p>
1244
+ ) : (
1245
+ <Button
1246
+ variant="outline"
1247
+ onClick={() =>
1248
+ setDevices((ds) => ds.filter((x) => x !== device))
1249
+ }
1250
+ >
1251
+ {t(
1252
+ L(
1253
+ "Sign out in preview",
1254
+ "미리보기에서 로그아웃",
1255
+ "تسجيل الخروج في المعاينة"
1256
+ )
1257
+ )}
1258
+ </Button>
1259
+ )}
1260
+ </li>
1261
+ ))}
1262
+ </ul>
1263
+ ) : closed ? (
1264
+ <Result
1265
+ locale={locale}
1266
+ onReset={() => {
1267
+ setClosed(false);
1268
+ setConfirm("");
1269
+ }}
1270
+ />
1271
+ ) : (
1272
+ <section className="product-card">
1273
+ <p>
1274
+ {t(
1275
+ L(
1276
+ "Type CLOSE to simulate an account closure request. No account is removed.",
1277
+ "계정 종료 요청 체험은 CLOSE를 입력하세요. 실제 계정은 삭제되지 않습니다.",
1278
+ "اكتب CLOSE لمحاكاة طلب الإغلاق. لن يُحذف حساب."
1279
+ )
1280
+ )}
1281
+ </p>
1282
+ <TextInput
1283
+ aria-label={t(L("Confirmation", "확인 문구", "التأكيد"))}
1284
+ value={confirm}
1285
+ onChange={(e) => setConfirm(e.target.value)}
1286
+ />
1287
+ <Button
1288
+ disabled={confirm !== "CLOSE"}
1289
+ onClick={() => setClosed(true)}
1290
+ >
1291
+ {t(copy.confirm)}
1292
+ </Button>
1293
+ </section>
1294
+ )}
1295
+ {fields.length ? (
1296
+ <Button onClick={() => setSaved(true)}>{t(copy.save)}</Button>
1297
+ ) : null}
1298
+ {saved ? <p role="status">{t(copy.saved)}</p> : null}
1299
+ </>
1300
+ );
1301
+ }
1302
+
1303
+ function MediaFlow({ d, locale, initialState = "live" }: FlowProps) {
1304
+ const t = text(locale),
1305
+ collaboration = d.id === "live-session-01",
1306
+ [status, setStatus] = useState(initialState),
1307
+ [playing, setPlaying] = useState(false),
1308
+ [mic, setMic] = useState(true),
1309
+ [note, setNote] = useState(""),
1310
+ [events, setEvents] = useState<string[]>([]),
1311
+ [reminder, setReminder] = useState(false),
1312
+ [endConfirm, setEndConfirm] = useState(false);
1313
+ return (
1314
+ <>
1315
+ <MediaStage
1316
+ label={t(d.title)}
1317
+ status={
1318
+ status === "connection-lost"
1319
+ ? t(
1320
+ L(
1321
+ "Connection interrupted. Use the agreed fallback.",
1322
+ "연결이 끊겼어요. 약속된 대체 수단을 사용하세요.",
1323
+ "انقطع الاتصال. استخدم البديل المتفق عليه."
1324
+ )
1325
+ )
1326
+ : status === "ended"
1327
+ ? t(copy.ended)
1328
+ : status === "scheduled"
1329
+ ? "2026-09-14 · 10:00"
1330
+ : t(
1331
+ playing
1332
+ ? L("Playing example", "재생 체험 중", "تشغيل المثال")
1333
+ : copy.previewMedia
1334
+ )
1335
+ }
1336
+ >
1337
+ {status === "connection-lost" ? (
1338
+ <Button onClick={() => setStatus("live")}>
1339
+ {t(L("Simulate recovery", "복구 체험", "محاكاة الاسترداد"))}
1340
+ </Button>
1341
+ ) : status === "scheduled" ? (
1342
+ <Button
1343
+ aria-pressed={reminder}
1344
+ onClick={() => setReminder((r) => !r)}
1345
+ >
1346
+ {t(
1347
+ reminder
1348
+ ? L(
1349
+ "Reminder selected (preview)",
1350
+ "알림 선택됨(체험)",
1351
+ "تم اختيار التذكير (معاينة)"
1352
+ )
1353
+ : L("Notify me", "알림 받기", "ذكّرني")
1354
+ )}
1355
+ </Button>
1356
+ ) : status !== "ended" ? (
1357
+ <Button onClick={() => setPlaying((p) => !p)}>
1358
+ {t(playing ? copy.pause : copy.play)}
1359
+ </Button>
1360
+ ) : null}
1361
+ </MediaStage>
1362
+ {collaboration && status !== "ended" ? (
1363
+ <>
1364
+ <div className="product-card">
1365
+ <h4>{t(L("Participants", "참가자", "المشاركون"))}</h4>
1366
+ <p>Alex Kim · Noor Ali</p>
1367
+ <Button
1368
+ variant="outline"
1369
+ aria-pressed={!mic}
1370
+ onClick={() => setMic((v) => !v)}
1371
+ >
1372
+ {t(
1373
+ mic
1374
+ ? L(
1375
+ "Mute microphone (preview)",
1376
+ "마이크 끄기(체험)",
1377
+ "كتم الميكروفون (معاينة)"
1378
+ )
1379
+ : L(
1380
+ "Unmute microphone (preview)",
1381
+ "마이크 켜기(체험)",
1382
+ "تشغيل الميكروفون (معاينة)"
1383
+ )
1384
+ )}
1385
+ </Button>
1386
+ </div>
1387
+ <form
1388
+ onSubmit={(e) => {
1389
+ e.preventDefault();
1390
+ if (note.trim()) {
1391
+ setEvents((v) => [...v, note.trim()]);
1392
+ setNote("");
1393
+ }
1394
+ }}
1395
+ >
1396
+ <label>
1397
+ {t(L("Incident note", "이슈 메모", "ملاحظة حادث"))}
1398
+ <TextArea
1399
+ maxLength={200}
1400
+ value={note}
1401
+ onChange={(e) => setNote(e.target.value)}
1402
+ />
1403
+ </label>
1404
+ <Button type="submit" disabled={!note.trim()}>
1405
+ {t(L("Log example event", "예시 이슈 기록", "تسجيل حدث تجريبي"))}
1406
+ </Button>
1407
+ </form>
1408
+ <ul aria-live="polite">
1409
+ {events.map((event, i) => (
1410
+ <li key={i}>{event}</li>
1411
+ ))}
1412
+ </ul>
1413
+ <div className="product-actions">
1414
+ <Button
1415
+ variant="outline"
1416
+ onClick={() => setStatus("connection-lost")}
1417
+ >
1418
+ {t(
1419
+ L(
1420
+ "Simulate disconnection",
1421
+ "연결 끊김 체험",
1422
+ "محاكاة انقطاع الاتصال"
1423
+ )
1424
+ )}
1425
+ </Button>
1426
+ <Button variant="outline" onClick={() => setEndConfirm(true)}>
1427
+ {t(L("End session", "세션 종료", "إنهاء الجلسة"))}
1428
+ </Button>
1429
+ </div>
1430
+ {endConfirm ? (
1431
+ <section className="product-card">
1432
+ <p>{t(copy.confirmHint)}</p>
1433
+ <Button
1434
+ onClick={() => {
1435
+ setStatus("ended");
1436
+ setEndConfirm(false);
1437
+ }}
1438
+ >
1439
+ {t(copy.confirm)}
1440
+ </Button>
1441
+ <Button variant="ghost" onClick={() => setEndConfirm(false)}>
1442
+ {t(copy.cancel)}
1443
+ </Button>
1444
+ </section>
1445
+ ) : null}
1446
+ </>
1447
+ ) : null}
1448
+ </>
1449
+ );
1450
+ }
1451
+
1452
+ function ContentFlow({ d, locale, initialState }: FlowProps) {
1453
+ const t = text(locale),
1454
+ [tab, setTab] = useState(0),
1455
+ [opened, setOpened] = useState(false),
1456
+ [email, setEmail] = useState(""),
1457
+ [subscribed, setSubscribed] = useState(false),
1458
+ intro = d.id === "product-introduction-01";
1459
+ const titles = [
1460
+ L(
1461
+ "Structured learning at scale",
1462
+ "확장 가능한 체계적 학습",
1463
+ "تعلم منظم على نطاق واسع"
1464
+ ),
1465
+ L(
1466
+ "How technology supports better feedback",
1467
+ "더 나은 피드백을 돕는 기술",
1468
+ "كيف تدعم التقنية ملاحظات أفضل"
1469
+ ),
1470
+ ];
1471
+ return (
1472
+ <>
1473
+ <div className="workflow-tabs">
1474
+ {d.tabs?.map((c, i) => (
1475
+ <Button
1476
+ key={i}
1477
+ variant="outline"
1478
+ aria-pressed={tab === i}
1479
+ onClick={() => {
1480
+ setTab(i);
1481
+ setOpened(false);
1482
+ }}
1483
+ >
1484
+ {t(c)}
1485
+ </Button>
1486
+ ))}
1487
+ </div>
1488
+ {intro ? (
1489
+ <>
1490
+ <section className="product-card">
1491
+ <h4>{t(d.tabs![tab])}</h4>
1492
+ <p>
1493
+ {t(
1494
+ [
1495
+ L(
1496
+ "Get guidance, practice and structured feedback.",
1497
+ "안내·실습·체계적인 피드백을 경험하세요.",
1498
+ "احصل على الإرشاد والتدريب والملاحظات المنظمة."
1499
+ ),
1500
+ L(
1501
+ "Manage availability, requests and follow-up.",
1502
+ "가능 시간·요청·후속 작업을 관리하세요.",
1503
+ "إدارة التوفر والطلبات والمتابعة."
1504
+ ),
1505
+ L(
1506
+ "Allocate resources and follow team outcomes.",
1507
+ "자원을 배분하고 팀의 성과를 확인하세요.",
1508
+ "وزع الموارد وتابع نتائج الفريق."
1509
+ ),
1510
+ ][tab]
1511
+ )}
1512
+ </p>
1513
+ </section>
1514
+ {[
1515
+ L("How do I start?", "어떻게 시작하나요?", "كيف أبدأ؟"),
1516
+ L(
1517
+ "Can a team participate?",
1518
+ "팀으로 참여할 수 있나요?",
1519
+ "هل يمكن للفريق المشاركة؟"
1520
+ ),
1521
+ ].map((c) => (
1522
+ <details className="product-card" key={c.en}>
1523
+ <summary>{t(c)}</summary>
1524
+ <p>
1525
+ {t(
1526
+ L(
1527
+ "Choose the flow that matches your goal and complete the required setup.",
1528
+ "목적에 맞는 흐름을 선택하고 필요한 설정을 완료하세요.",
1529
+ "اختر المسار المناسب لهدفك وأكمل الإعداد المطلوب."
1530
+ )
1531
+ )}
1532
+ </p>
1533
+ </details>
1534
+ ))}
1535
+ </>
1536
+ ) : initialState === "empty" ? (
1537
+ <p role="status">{t(copy.empty)}</p>
1538
+ ) : opened ? (
1539
+ <section className="product-card">
1540
+ <h4>{t(titles[tab === 2 ? 1 : 0])}</h4>
1541
+ <p>
1542
+ {t(
1543
+ L(
1544
+ "Clear goals, timely feedback and a record of decisions help people build on what they have learned. This is sample article content.",
1545
+ "명확한 목표·적시의 피드백·결정 기록은 배운 내용을 다음 작업에 활용하도록 돕습니다. 예시 본문입니다.",
1546
+ "تساعد الأهداف الواضحة والملاحظات في وقتها وسجل القرارات على البناء على التعلم. هذا نص تجريبي."
1547
+ )
1548
+ )}
1549
+ </p>
1550
+ <Button variant="outline" onClick={() => setOpened(false)}>
1551
+ {t(copy.back)}
1552
+ </Button>
1553
+ </section>
1554
+ ) : (
1555
+ <ul className="product-grid">
1556
+ {titles
1557
+ .filter((_, i) => tab === 0 || i === tab - 1)
1558
+ .map((c) => (
1559
+ <li className="product-card" key={c.en}>
1560
+ <h4>{t(c)}</h4>
1561
+ <p>2026-09-10 · Alex Kim</p>
1562
+ <Button variant="outline" onClick={() => setOpened(true)}>
1563
+ {t(L("Read article", "글 읽기", "قراءة المقال"))}
1564
+ </Button>
1565
+ </li>
1566
+ ))}
1567
+ </ul>
1568
+ )}
1569
+ {!intro ? (
1570
+ <form
1571
+ onSubmit={(e) => {
1572
+ e.preventDefault();
1573
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) setSubscribed(true);
1574
+ }}
1575
+ >
1576
+ <label>
1577
+ {t(L("Newsletter email", "뉴스레터 이메일", "بريد النشرة"))}
1578
+ <TextInput
1579
+ type="email"
1580
+ required
1581
+ value={email}
1582
+ onChange={(e) => {
1583
+ setEmail(e.target.value);
1584
+ setSubscribed(false);
1585
+ }}
1586
+ />
1587
+ </label>
1588
+ <Button type="submit">
1589
+ {t(L("Subscribe in preview", "구독 체험", "الاشتراك في المعاينة"))}
1590
+ </Button>
1591
+ {subscribed ? <p role="status">{t(copy.done)}</p> : null}
1592
+ </form>
1593
+ ) : null}
1594
+ </>
1595
+ );
1596
+ }
1597
+
1598
+ function RecoveryFlow({ d, locale, initialState = "default" }: FlowProps) {
1599
+ const t = text(locale),
1600
+ [state, setState] = useState(initialState),
1601
+ [value, setValue] = useState(""),
1602
+ [done, setDone] = useState(false),
1603
+ [seconds, setSeconds] = useState(15),
1604
+ [error, setError] = useState(false);
1605
+ useEffect(() => {
1606
+ if (state !== "attempts" || seconds <= 0) return;
1607
+ const timer = setTimeout(() => setSeconds((s) => s - 1), 1000);
1608
+ return () => clearTimeout(timer);
1609
+ }, [state, seconds]);
1610
+ if (done)
1611
+ return (
1612
+ <Result
1613
+ locale={locale}
1614
+ onReset={() => {
1615
+ setDone(false);
1616
+ setValue("");
1617
+ setError(false);
1618
+ }}
1619
+ />
1620
+ );
1621
+ if (d.kind === "welcome")
1622
+ return (
1623
+ <section className="product-card">
1624
+ <h4>{t(L("Your account is ready", "계정 준비 완료", "حسابك جاهز"))}</h4>
1625
+ <a href={href(d.next!)}>
1626
+ {t(L("Set up your profile", "프로필 설정", "إعداد الملف"))}
1627
+ </a>
1628
+ <Button variant="outline" onClick={() => setDone(true)}>
1629
+ {t(L("Skip for now", "나중에 하기", "التخطي الآن"))}
1630
+ </Button>
1631
+ </section>
1632
+ );
1633
+ if (d.id === "loading-recovery-01")
1634
+ return (
1635
+ <section
1636
+ className="product-card"
1637
+ role={state === "load-error" ? "alert" : "status"}
1638
+ >
1639
+ <h4>
1640
+ {t(
1641
+ state === "loading"
1642
+ ? L(
1643
+ "Loading records…",
1644
+ "목록을 불러오는 중…",
1645
+ "جار تحميل السجلات…"
1646
+ )
1647
+ : state === "not-found"
1648
+ ? L(
1649
+ "Page not found",
1650
+ "페이지를 찾을 수 없어요",
1651
+ "الصفحة غير موجودة"
1652
+ )
1653
+ : state === "empty"
1654
+ ? copy.empty
1655
+ : L("Something went wrong", "문제가 발생했어요", "حدث خطأ")
1656
+ )}
1657
+ </h4>
1658
+ <Button onClick={() => setDone(true)}>
1659
+ {t(
1660
+ state === "loading"
1661
+ ? L(
1662
+ "Simulate load completion",
1663
+ "불러오기 완료 체험",
1664
+ "محاكاة اكتمال التحميل"
1665
+ )
1666
+ : L("Retry example", "재시도 체험", "إعادة محاولة المثال")
1667
+ )}
1668
+ </Button>
1669
+ <a href="#/templates?view=patterns">
1670
+ {t(L("Back to patterns", "패턴 목록으로", "العودة للأنماط"))}
1671
+ </a>
1672
+ </section>
1673
+ );
1674
+ return (
1675
+ <section className="product-card">
1676
+ {state === "attempts" ? (
1677
+ <>
1678
+ <h4>
1679
+ {t(L("Too many attempts", "시도 횟수 초과", "محاولات كثيرة"))}
1680
+ </h4>
1681
+ <p>
1682
+ {seconds}s · {t(copy.demo)}
1683
+ </p>
1684
+ <Button
1685
+ disabled={seconds > 0}
1686
+ onClick={() => setState("device-code")}
1687
+ >
1688
+ {t(L("Try again", "다시 시도", "المحاولة مجدداً"))}
1689
+ </Button>
1690
+ <a href={href("password-recovery-01")}>
1691
+ {t(
1692
+ L("Reset password", "비밀번호 재설정", "إعادة تعيين كلمة المرور")
1693
+ )}
1694
+ </a>
1695
+ </>
1696
+ ) : state === "suspicious" ? (
1697
+ <>
1698
+ <h4>
1699
+ {t(
1700
+ L("Unrecognized device", "확인되지 않은 기기", "جهاز غير معروف")
1701
+ )}
1702
+ </h4>
1703
+ <p>Chrome · Windows</p>
1704
+ <Button onClick={() => setState("device-code")}>
1705
+ {t(L("This was me", "제가 한 거예요", "هذا أنا"))}
1706
+ </Button>
1707
+ <a href={href("account-settings-01")}>
1708
+ {t(L("Secure account", "계정 보호하기", "تأمين الحساب"))}
1709
+ </a>
1710
+ </>
1711
+ ) : (
1712
+ <form
1713
+ onSubmit={(e) => {
1714
+ e.preventDefault();
1715
+ const valid =
1716
+ state === "device-code"
1717
+ ? value === "123456"
1718
+ : value === "demo-confirm";
1719
+ setError(!valid);
1720
+ if (valid) setDone(true);
1721
+ }}
1722
+ >
1723
+ <label>
1724
+ {t(
1725
+ state === "device-code"
1726
+ ? L(
1727
+ "Verification code (example: 123456)",
1728
+ "인증 코드(예시: 123456)",
1729
+ "رمز التحقق (مثال: 123456)"
1730
+ )
1731
+ : L(
1732
+ "Password (example: demo-confirm)",
1733
+ "비밀번호(예시: demo-confirm)",
1734
+ "كلمة المرور (مثال: demo-confirm)"
1735
+ )
1736
+ )}
1737
+ <TextInput
1738
+ type={state === "device-code" ? "text" : "password"}
1739
+ inputMode={state === "device-code" ? "numeric" : undefined}
1740
+ value={value}
1741
+ onChange={(e) => setValue(e.target.value)}
1742
+ aria-invalid={error}
1743
+ />
1744
+ </label>
1745
+ {error ? <p role="alert">{t(copy.error)}</p> : null}
1746
+ <Button type="submit">{t(copy.confirm)}</Button>
1747
+ <Button
1748
+ variant="outline"
1749
+ onClick={() => {
1750
+ setState("suspicious");
1751
+ setValue("");
1752
+ setError(false);
1753
+ }}
1754
+ >
1755
+ {t(copy.cancel)}
1756
+ </Button>
1757
+ </form>
1758
+ )}
1759
+ </section>
1760
+ );
1761
+ }