@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,27 @@
1
+ /* Registered auth-interactions composition. All appearance resolves at host theme. */
2
+ :root { --auth-form-width: 28rem; --auth-code-cell-min: 2rem; }
3
+ :root, [data-theme] { --auth-code-radius: min(var(--radius-input), var(--space-2)); --auth-panel-radius: min(var(--radius-surface), var(--space-4)); }
4
+ .auth-part-frame { display: grid; gap: var(--space-5); inline-size: 100%; max-inline-size: var(--auth-form-width); }
5
+ .auth-part-frame header, .auth-part-field { display: grid; gap: var(--space-2); }
6
+ .auth-part-field label, .auth-part-code legend { font-weight: var(--font-weight-strong); }
7
+ .auth-part-password { display: flex; align-items: center; gap: var(--space-2); min-inline-size: 0; }
8
+ .auth-part-password > input { min-inline-size: 0; flex: 1; }
9
+ .auth-part-field p, .auth-part-rules, .auth-part-code p { font-size: var(--font-size-small); }
10
+ .auth-part-field [data-error="true"], .auth-part-code p { color: var(--destructive-text); }
11
+ .auth-part-rules { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); padding: 0; list-style: none; margin: 0; color: var(--foreground-muted); }
12
+ .auth-part-rules [data-passed="true"] { color: var(--success-text); }
13
+ .auth-part-code { padding: 0; border: 0; margin: 0; min-inline-size: 0; }
14
+ .auth-part-code legend { margin-block-end: var(--space-3); }
15
+ .auth-part-code > div { display: grid; grid-template-columns: repeat(6, minmax(var(--auth-code-cell-min), 1fr)); gap: var(--space-2); }
16
+ .auth-part-code input { text-align: center; padding-inline: 0; min-inline-size: 0; inline-size: 100%; border-radius: var(--auth-code-radius); }
17
+ .auth-part-providers { display: grid; gap: var(--space-2); }
18
+ .auth-part-providers .uds-button { inline-size: 100%; }
19
+ .auth-part-divider { display: flex; gap: var(--space-3); align-items: center; color: var(--foreground-muted); font-size: var(--font-size-small); }
20
+ .auth-part-divider::before, .auth-part-divider::after { content: ''; flex: 1; border-block-start: var(--border-width-hairline) solid var(--border); }
21
+ .auth-part-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
22
+ .auth-part-actions .uds-button { white-space: normal; }
23
+ .auth-part-card { display: grid; gap: var(--space-3); padding: var(--space-4); background: var(--surface); border: var(--border-width-hairline) solid var(--border); border-radius: var(--auth-panel-radius); }
24
+ .auth-part-check { display: flex; align-items: start; gap: var(--space-2); }
25
+ .auth-part-check > button { flex-shrink: 0; }
26
+ .auth-part-state { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; font-size: var(--font-size-small); }
27
+ .auth-part-state select { inline-size: auto; max-inline-size: 100%; }
@@ -0,0 +1,25 @@
1
+ /* Registered semantic-pattern-parts contract; never import from a page. */
2
+ :root, [data-theme] { --pattern-part-radius: min(var(--radius-surface), var(--space-4)); --pattern-part-min: 10rem; }
3
+ .pattern-parts { min-inline-size: 0; color: var(--foreground); font-size: var(--font-size-body); line-height: var(--line-height-body); }
4
+ .pattern-parts p { margin: 0; color: var(--foreground-muted); overflow-wrap: anywhere; }
5
+ .pattern-parts button { white-space: normal; }
6
+ .pattern-choices { border: 0; padding: 0; margin: 0; }
7
+ .pattern-choices legend { margin-block-end: var(--space-3); font-weight: var(--font-weight-strong); }
8
+ .pattern-choices > div { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--pattern-part-min)), 1fr)); gap: var(--space-3); }
9
+ .pattern-choices .uds-button { display: flex; flex-direction: column; align-items: flex-start; block-size: auto; padding: var(--space-4); gap: var(--space-2); text-align: start; border-radius: var(--pattern-part-radius); }
10
+ .pattern-choices .uds-button[aria-pressed="true"] { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }
11
+ .pattern-choices .uds-button-content, .pattern-steps .uds-button-content { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); inline-size: 100%; text-align: start; }
12
+ .pattern-choices span { font-size: var(--font-size-small); }
13
+ .pattern-field { display: grid; gap: var(--space-2); }
14
+ .pattern-field label, .pattern-filters label { font-weight: var(--font-weight-strong); }
15
+ .pattern-field [data-error="true"] { color: var(--foreground); font-weight: var(--font-weight-strong); }
16
+ .pattern-filters { display: flex; align-items: end; flex-wrap: wrap; gap: var(--space-3); }
17
+ .pattern-filters > div { display: grid; gap: var(--space-2); flex: 1 1 var(--pattern-part-min); min-inline-size: 0; }
18
+ .pattern-review .uds-data-table-shell { border-radius: var(--pattern-part-radius); }
19
+ .pattern-review td p { font-size: var(--font-size-small); }
20
+ .pattern-steps ol { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--pattern-part-min)), 1fr)); gap: var(--space-3); padding: 0; margin: 0; list-style: none; }
21
+ .pattern-steps .uds-button { display: flex; flex-direction: column; align-items: flex-start; inline-size: 100%; block-size: auto; padding: var(--space-4); gap: var(--space-2); border-radius: var(--pattern-part-radius); }
22
+ .pattern-result { display: grid; gap: var(--space-3); padding: var(--space-5); background: var(--surface); color: var(--foreground); border: var(--border-width-hairline) solid var(--border); border-radius: var(--pattern-part-radius); }
23
+ .pattern-result[data-tone="success"] { border-color: var(--success); }
24
+ .pattern-result[data-tone="warning"] { border-color: var(--warning); }
25
+ .pattern-result .uds-button { justify-self: start; }
@@ -0,0 +1,25 @@
1
+ /* Registered product-interactions; no original portal shell or brand styles. */
2
+ :root { --product-content-width: 64rem; --product-card-min: 17rem; }
3
+ :root, [data-theme] { --product-panel-radius: min(var(--radius-surface), var(--space-4)); }
4
+ .product-example { container-type: inline-size; }
5
+ .product-content { display: grid; gap: var(--space-6); inline-size: 100%; max-inline-size: var(--product-content-width); margin-inline: auto; padding: var(--space-8) var(--space-5); flex: 1; align-content: start; min-inline-size: 0; }
6
+ .product-content header { display: grid; gap: var(--space-2); }
7
+ .product-content h4 { margin: 0; font-size: var(--font-size-body-lg); }
8
+ .product-toolbar, .product-actions { display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-3); }
9
+ .product-toolbar > label { display: grid; gap: var(--space-2); flex: 1; min-inline-size: min(100%, var(--product-card-min)); }
10
+ .product-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--product-card-min)), 1fr)); gap: var(--space-4); padding: 0; margin: 0; list-style: none; }
11
+ .product-card { display: grid; gap: var(--space-3); align-content: start; padding: var(--space-5); min-inline-size: 0; border: var(--border-width-hairline) solid var(--border); border-radius: var(--product-panel-radius); background: var(--surface); overflow-wrap: anywhere; }
12
+ .product-list { list-style: none; padding: 0; margin: 0; border: var(--border-width-hairline) solid var(--border); border-radius: var(--product-panel-radius); overflow: hidden; }
13
+ .product-list > :not(:first-child) { border-block-start: var(--border-width-hairline) solid var(--border); }
14
+ .product-row { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: start; inline-size: 100%; padding: var(--space-5); text-align: start; border: 0; background: var(--surface); color: var(--foreground); font: inherit; cursor: pointer; }
15
+ .product-row:hover, .product-row[data-unread="true"] { background: var(--surface-strong); }
16
+ .product-row:focus-visible { outline: var(--border-width-focus) solid var(--ring); outline-offset: calc(-1 * var(--space-1)); }
17
+ .product-row__content { display: grid; gap: var(--space-2); flex: 1; min-inline-size: min(100%, var(--product-card-min)); overflow-wrap: anywhere; }
18
+ .product-row__content > span, .product-row__meta { color: var(--foreground-muted); font-size: var(--font-size-small); }
19
+ .product-row__meta { display: grid; gap: var(--space-2); }
20
+ .product-setting { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); padding: var(--space-5); background: var(--surface); }
21
+ .product-setting > div { flex: 1; min-inline-size: min(100%, var(--product-card-min)); }
22
+ .product-setting select { inline-size: auto; max-inline-size: 100%; }
23
+ .product-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-5); align-items: start; }
24
+ .product-actions .uds-button { white-space: normal; }
25
+ @container (max-width: 45rem) { .product-columns { grid-template-columns: minmax(0, 1fr); } }
@@ -0,0 +1,3 @@
1
+ export type PatternOption = { id: string; label: string; description?: string }
2
+
3
+ export type ReviewItem = { id: string; title: string; description?: string; status: string; assignee: string }
@@ -0,0 +1,118 @@
1
+ /* Registered workflow-interactions. Shared field/table/stage geometry, not page CSS. */
2
+ :root {
3
+ --workflow-table-min: 35rem;
4
+ --workflow-media-min: 16rem;
5
+ }
6
+ .workflow-fields,
7
+ .workflow-field {
8
+ display: grid;
9
+ gap: var(--space-2);
10
+ min-inline-size: 0;
11
+ }
12
+ .workflow-fields {
13
+ gap: var(--space-4);
14
+ }
15
+ .workflow-field > label,
16
+ .workflow-field > span {
17
+ font-weight: var(--font-weight-strong);
18
+ }
19
+ .workflow-field small {
20
+ color: var(--foreground-muted);
21
+ overflow-wrap: anywhere;
22
+ }
23
+ .workflow-field small[data-error="true"] {
24
+ color: var(--destructive-text);
25
+ }
26
+ .workflow-field input[type="checkbox"] {
27
+ inline-size: var(--space-5);
28
+ block-size: var(--space-5);
29
+ accent-color: var(--primary);
30
+ }
31
+ .workflow-field input[type="file"] {
32
+ inline-size: 100%;
33
+ min-inline-size: 0;
34
+ max-inline-size: 100%;
35
+ font: inherit;
36
+ }
37
+ .workflow-field meter {
38
+ inline-size: 100%;
39
+ block-size: var(--space-5);
40
+ accent-color: var(--primary);
41
+ }
42
+ .workflow-table-scroll {
43
+ min-inline-size: 0;
44
+ max-inline-size: 100%;
45
+ overflow: auto;
46
+ scrollbar-width: none;
47
+ border: var(--border-width-hairline) solid var(--border);
48
+ border-radius: var(--product-panel-radius);
49
+ }
50
+ .workflow-table-scroll::-webkit-scrollbar {
51
+ display: none;
52
+ }
53
+ .workflow-table-scroll:focus-visible {
54
+ outline: var(--border-width-focus) solid var(--ring);
55
+ outline-offset: var(--space-1);
56
+ }
57
+ .workflow-table-scroll table {
58
+ border-collapse: collapse;
59
+ inline-size: 100%;
60
+ min-inline-size: var(--workflow-table-min);
61
+ background: var(--surface);
62
+ }
63
+ .workflow-table-scroll caption {
64
+ text-align: start;
65
+ padding: var(--space-4);
66
+ font-weight: var(--font-weight-strong);
67
+ }
68
+ .workflow-table-scroll th,
69
+ .workflow-table-scroll td {
70
+ padding: var(--space-4);
71
+ text-align: start;
72
+ border-block-start: var(--border-width-hairline) solid var(--border);
73
+ overflow-wrap: anywhere;
74
+ }
75
+ .workflow-table-scroll th {
76
+ color: var(--foreground-muted);
77
+ font-size: var(--font-size-small);
78
+ }
79
+ .workflow-checklist {
80
+ display: grid;
81
+ gap: var(--space-4);
82
+ margin: 0;
83
+ padding: 0;
84
+ list-style: none;
85
+ }
86
+ .workflow-stats {
87
+ display: grid;
88
+ grid-template-columns: repeat(
89
+ auto-fit,
90
+ minmax(min(100%, var(--product-card-min)), 1fr)
91
+ );
92
+ gap: var(--space-4);
93
+ margin: 0;
94
+ }
95
+ .workflow-stats dd {
96
+ margin: 0;
97
+ font-size: var(--font-size-page-title);
98
+ font-weight: var(--font-weight-strong);
99
+ }
100
+ .workflow-media {
101
+ display: grid;
102
+ place-content: center;
103
+ text-align: center;
104
+ gap: var(--space-4);
105
+ padding: var(--space-6);
106
+ min-block-size: var(--workflow-media-min);
107
+ border: var(--border-width-hairline) solid var(--border);
108
+ border-radius: var(--product-panel-radius);
109
+ background: var(--surface-strong);
110
+ }
111
+ .workflow-tabs {
112
+ display: flex;
113
+ flex-wrap: wrap;
114
+ gap: var(--space-2);
115
+ }
116
+ .workflow-tabs .uds-button {
117
+ white-space: normal;
118
+ }
@@ -0,0 +1,46 @@
1
+ // Source-backed invitation and session recovery. No real membership/auth writes.
2
+ import { useId, useState } from 'react'
3
+ import { Button, Checkbox, TextInput } from '@utopia-studio-design/design-system'
4
+ import { AuthFrame, PasswordField, ProviderButtons } from '../semantic-pattern-parts/AuthParts'
5
+ import { ValidatedField } from '../semantic-pattern-parts/ValidatedField'
6
+ import { OutcomeNotice } from '../semantic-pattern-parts/OutcomeNotice'
7
+ import type { ExampleLocale } from './copy'
8
+ import './ux-examples.css'
9
+
10
+ type Props = { locale?: ExampleLocale; initialState?: string }
11
+ const words = (locale: ExampleLocale) => (en: string, ko: string, ar: string) => locale === 'ko' ? ko : locale === 'ar' ? ar : en
12
+
13
+ export function InvitationExample({locale='en',initialState='new-user'}: Props) {
14
+ const t=words(locale), [state,setState]=useState(initialState), [name,setName]=useState(''), [password,setPassword]=useState(''), [terms,setTerms]=useState(false), [code,setCode]=useState(''), [error,setError]=useState(''), [notice,setNotice]=useState(''), termsId=useId()
15
+ const pwProps={showLabel:t('Show password','비밀번호 보기','إظهار كلمة المرور'),hideLabel:t('Hide password','비밀번호 숨기기','إخفاء كلمة المرور')}
16
+ const change=(next:string)=>{setState(next);setError('');setNotice('');setPassword('')}
17
+ const rules=[{id:'length',label:t('At least 8 characters','8자 이상','٨ أحرف على الأقل'),passed:password.length>=8},{id:'number',label:t('Include a number','숫자 포함','يتضمن رقماً'),passed:/\d/.test(password)}]
18
+ const reset=t('Try again','다시 해보기','حاول مجدداً')
19
+ return <AuthFrame locale={locale} title={state==='code-entry'?t('Enter your invite code','초대 코드 입력','أدخل رمز الدعوة'):state==='expired'?t('Invitation expired','초대가 만료되었어요','انتهت الدعوة'):t('Join the team','팀에 참여하기','انضم إلى الفريق')} subtitle={t('Review the team and role before accepting.','참여 전에 팀과 부여될 역할을 확인하세요.','راجع الفريق والدور قبل القبول.')} footer={t('Sample invitation only. No account is created, no email is sent and no team is joined.','체험용 초대입니다. 실제 계정 생성·메일 발송·팀 참여는 하지 않아요.','دعوة تجريبية فقط. لا حسابات أو رسائل بريد أو انضمام فعلي.')}>
20
+ <div className="auth-part-card"><strong>North Studio</strong><p>{t('24 members · Invited as Editor','멤버 24명 · 편집자로 초대','٢٤ عضواً · دعوة كمحرر')}</p><p>{t('Alex Kim invited you','Alex Kim님의 초대','دعوة من Alex Kim')}</p></div>
21
+ {state==='accepted'||state==='declined' ? <OutcomeNotice title={state==='accepted'?t('Invitation accepted — example','초대 수락 예시','تم قبول الدعوة — مثال'):t('Invitation declined — example','초대 거절 예시','تم رفض الدعوة — مثال')} actionLabel={reset} onAction={()=>change(initialState)}>{t('Your real team membership has not changed.','실제 팀 멤버십은 변경되지 않았어요.','لم تتغير عضويتك الفعلية.')}</OutcomeNotice> : state==='expired' ? <Button onClick={()=>setNotice(t('New invitation request simulated. No email sent.','새 초대 요청을 시뮬레이션했어요. 메일은 발송되지 않았습니다.','تمت محاكاة طلب دعوة جديدة دون إرسال بريد.'))}>{t('Request a new invitation','새 초대 요청','طلب دعوة جديدة')}</Button> : state==='code-entry' ? <form onSubmit={e=>{e.preventDefault();if(code.trim().toUpperCase()==='DEMO-2026')change('existing-user');else setError(t('Invalid code. Try DEMO-2026.','올바르지 않은 코드예요. DEMO-2026을 입력해 보세요.','رمز غير صالح. جرّب DEMO-2026.'))}}>
22
+ <ValidatedField label={t('Invite code','초대 코드','رمز الدعوة')} value={code} onChange={setCode} required hint="DEMO-2026" error={error}/><Button type="submit" disabled={!code.trim()}>{t('Continue','계속','متابعة')}</Button>
23
+ </form> : state==='existing-user' ? <><div className="auth-part-card"><strong>Jamie Park</strong><p>sample@example.com</p></div><Button onClick={()=>change('accepted')}>{t('Accept invitation','초대 수락','قبول الدعوة')}</Button><Button variant="outline" onClick={()=>change('declined')}>{t('Decline invitation','초대 거절','رفض الدعوة')}</Button><Button variant="ghost" onClick={()=>change('new-user')}>{t('Use a different account','다른 계정 사용','استخدام حساب آخر')}</Button></> : <>
24
+ <ProviderButtons label={t('Continue with a provider','외부 계정으로 계속','المتابعة مع مزود')} providers={['Google','Apple','GitHub'].map(id=>({id,label:t(`Continue with ${id}`,`${id}로 계속`,`المتابعة باستخدام ${id}`)}))} onSelect={id=>setNotice(t(`${id} selected. Connect the provider in a real app.`,`${id}를 선택했어요. 실제 앱에서 인증 제공자를 연결하세요.`,`تم اختيار ${id}. اربط المزود في التطبيق الفعلي.`))}/>
25
+ <form onSubmit={e=>{e.preventDefault();if(name.trim().length>=2&&rules.every(r=>r.passed)&&terms)change('accepted')}}>
26
+ <label className="auth-part-field">{t('Invited email','초대된 이메일','البريد المدعو')}<TextInput value="sample@example.com" disabled /></label>
27
+ <ValidatedField label={t('Full name','이름','الاسم الكامل')} value={name} onChange={setName} required />
28
+ <PasswordField label={t('Create password','비밀번호 만들기','إنشاء كلمة المرور')} value={password} onChange={setPassword} rules={rules} {...pwProps}/>
29
+ <div className="auth-part-check"><Checkbox id={termsId} checked={terms} onCheckedChange={v=>setTerms(v===true)}/><label htmlFor={termsId}>{t('Agree to sample terms (demo only)','예시 이용약관에 동의 (체험용)','الموافقة على شروط تجريبية فقط')}</label></div>
30
+ <Button type="submit" disabled={name.trim().length<2||!rules.every(r=>r.passed)||!terms}>{t('Accept invitation and join','초대 수락하고 참여','قبول الدعوة والانضمام')}</Button>
31
+ </form><Button variant="ghost" onClick={()=>change('code-entry')}>{t('Have an invite code?','초대 코드가 있나요?','هل لديك رمز دعوة؟')}</Button>
32
+ </>}
33
+ {notice?<p className="auth-part-card" role="status">{notice}</p>:null}
34
+ </AuthFrame>
35
+ }
36
+
37
+ export function SessionRecoveryExample({locale='en',initialState='expired-full'}:Props) {
38
+ const t=words(locale),[state,setState]=useState(initialState),[password,setPassword]=useState(''),[error,setError]=useState('')
39
+ const change=(next:string)=>{setState(next);setPassword('');setError('')}
40
+ return <AuthFrame locale={locale} title={state==='concurrent'?t('Active on another device','다른 기기에서 사용 중','نشط على جهاز آخر'):state==='logged-out'?t('You are signed out','로그아웃되었어요','تم تسجيل الخروج'):t('Continue your session','작업 이어가기','تابع جلستك')} subtitle={t('A session recovery example. Your current Ceramic sign-in is unaffected.','세션 복구 예시입니다. 현재 Ceramic 로그인 상태에는 영향을 주지 않아요.','مثال لاستعادة الجلسة. لن يتأثر تسجيل دخولك الحالي.')} footer={t('No authentication, device revocation or automatic navigation occurs.','실제 인증·기기 로그아웃·자동 페이지 이동은 하지 않아요.','لا مصادقة أو إلغاء جلسات أجهزة أو انتقال تلقائي.')}>
41
+ {state==='restored'?<OutcomeNotice title={t('Sample session restored','예시 세션을 복구했어요','تمت استعادة الجلسة التجريبية')} actionLabel={t('Try again','다시 해보기','حاول مجدداً')} onAction={()=>change(initialState)}>{t('The host must authenticate before restoring real work.','실제 작업 복구 전에는 호스트의 인증이 필요합니다.','يجب على التطبيق المصادقة قبل استعادة العمل الفعلي.')}</OutcomeNotice>:state==='concurrent'?<>
42
+ <div className="auth-part-card"><strong>Chrome · MacBook</strong><p>{t('Current device · Sample session','현재 기기 · 예시 세션','الجهاز الحالي · جلسة تجريبية')}</p><strong>Safari · iPhone</strong><p>{t('Other device · Active just now','다른 기기 · 방금 활동','جهاز آخر · نشط الآن')}</p></div>
43
+ <Button onClick={()=>change('restored')}>{t('Sign out other devices and continue','다른 기기에서 로그아웃하고 계속','الخروج من الأجهزة الأخرى والمتابعة')}</Button><Button variant="outline" onClick={()=>change('logged-out')}>{t('Cancel and sign out','취소하고 로그아웃','الإلغاء وتسجيل الخروج')}</Button><Button variant="ghost" onClick={()=>change('reauthenticate')}>{t('Secure your account','계정 보호','تأمين حسابك')}</Button>
44
+ </>:state==='reauthenticate'?<form onSubmit={e=>{e.preventDefault();if(password==='demo-session')change('restored');else setError(t('Use the sample password demo-session.','예시 비밀번호 demo-session을 사용하세요.','استخدم كلمة المرور التجريبية demo-session.'))}}><p>sample@example.com</p><PasswordField label={t('Password','비밀번호','كلمة المرور')} value={password} onChange={setPassword} showLabel={t('Show password','비밀번호 보기','إظهار كلمة المرور')} hideLabel={t('Hide password','비밀번호 숨기기','إخفاء كلمة المرور')} hint="demo-session" error={error}/><div className="auth-part-actions"><Button type="submit" disabled={!password}>{t('Continue','계속','متابعة')}</Button><Button type="button" variant="outline" onClick={()=>change('logged-out')}>{t('Cancel','취소','إلغاء')}</Button></div></form>:<><p>{state==='logged-out'?t('Your sample session has ended.','예시 세션이 종료되었어요.','انتهت الجلسة التجريبية.'):t('Sign back in to continue where you left off.','다시 로그인해서 하던 작업을 이어가세요.','سجّل الدخول للمتابعة من حيث توقفت.')}</p><Button onClick={()=>change('reauthenticate')}>{t('Sign back in','다시 로그인','تسجيل الدخول مجدداً')}</Button></>}
45
+ </AuthFrame>
46
+ }
@@ -0,0 +1,133 @@
1
+ // State/layout ports grounded in the pinned account-source inventory.
2
+ // These fixtures never call auth, send mail, persist credentials or navigate away.
3
+ import { useEffect, useId, useState, type ReactNode } from 'react'
4
+ import { Button, Checkbox, TextInput } from '@utopia-studio-design/design-system'
5
+ import { AuthFrame, PasswordField, ProviderButtons, VerificationCode } from '../semantic-pattern-parts/AuthParts'
6
+ import { ChoiceCards } from '../semantic-pattern-parts/ChoiceCards'
7
+ import { ValidatedField } from '../semantic-pattern-parts/ValidatedField'
8
+ import { OutcomeNotice } from '../semantic-pattern-parts/OutcomeNotice'
9
+ import type { ExampleLocale } from './copy'
10
+ import './ux-examples.css'
11
+
12
+ export type AccountExampleProps = { locale?: ExampleLocale; initialState?: string }
13
+ const words = (locale: ExampleLocale) => (en: string, ko: string, ar: string) => locale === 'ko' ? ko : locale === 'ar' ? ar : en
14
+ type Words = ReturnType<typeof words>
15
+ const validEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
16
+ const rulesFor = (value: string, t: Words) => [
17
+ { id: 'length', label: t('At least 8 characters', '8자 이상', '٨ أحرف على الأقل'), passed: value.length >= 8 },
18
+ { id: 'case', label: t('Upper & lowercase letters', '대문자·소문자', 'أحرف كبيرة وصغيرة'), passed: /[a-z]/.test(value) && /[A-Z]/.test(value) },
19
+ { id: 'number', label: t('A number', '숫자 포함', 'رقم'), passed: /[0-9]/.test(value) },
20
+ { id: 'symbol', label: t('A symbol', '특수문자 포함', 'رمز'), passed: /[^A-Za-z0-9]/.test(value) },
21
+ ]
22
+ const sample = (t: Words) => t('Interactive example only. No accounts, emails or security settings are changed. Use sample information.', '체험용 예시입니다. 계정·이메일·보안 설정은 변경되지 않아요. 예시 정보를 입력하세요.', 'مثال تفاعلي فقط. لا تتغير الحسابات أو البريد أو إعدادات الأمان. استخدم بيانات تجريبية.')
23
+ const again = (t: Words) => t('Try again', '다시 해보기', 'حاول مجدداً')
24
+ const continueLabel = (t: Words) => t('Continue', '계속', 'متابعة')
25
+ const emailLabel = (t: Words) => t('Work email', '업무용 이메일', 'البريد الإلكتروني للعمل')
26
+ const passwordProps = (t: Words) => ({ showLabel: t('Show password', '비밀번호 보기', 'إظهار كلمة المرور'), hideLabel: t('Hide password', '비밀번호 숨기기', 'إخفاء كلمة المرور') })
27
+
28
+ function Check({ checked, onChange, children }: { checked: boolean; onChange: (v: boolean) => void; children: ReactNode }) {
29
+ const id = useId()
30
+ return <div className="auth-part-check"><Checkbox id={id} checked={checked} onCheckedChange={v => onChange(v === true)} /><label htmlFor={id}>{children}</label></div>
31
+ }
32
+ function Notice({ children }: { children: ReactNode }) { return <p className="auth-part-card" role="status">{children}</p> }
33
+ function Social({ t }: { t: Words }) {
34
+ const [selected, setSelected] = useState('')
35
+ return <><ProviderButtons label={t('Single sign-on', '외부 계정으로 로그인', 'تسجيل الدخول الموحد')} providers={['Google', 'Apple', 'GitHub'].map(id => ({ id, label: t(`Continue with ${id}`, `${id}로 계속`, `متابعة باستخدام ${id}`) }))} onSelect={setSelected} />{selected ? <Notice>{t(`${selected} was selected. A real provider connection is required.`, `${selected}를 선택했어요. 실제 서비스에서는 인증 제공자를 연결해야 해요.`, `تم اختيار ${selected}. يلزم ربط مزود فعلي.`)}</Notice> : null}<div className="auth-part-divider">{t('or continue with email', '또는 이메일로 계속', 'أو المتابعة بالبريد')}</div></>
36
+ }
37
+ function Email({ t, value, onChange }: { t: Words; value: string; onChange: (v: string) => void }) {
38
+ return <ValidatedField label={emailLabel(t)} value={value} onChange={onChange} type="email" required error={value && !validEmail(value) ? t('Check the email format.', '이메일 형식을 확인하세요.', 'تحقق من صيغة البريد.') : undefined} />
39
+ }
40
+ function useCooldown() {
41
+ const [seconds, setSeconds] = useState(0)
42
+ useEffect(() => { if (!seconds) return; const timer = setTimeout(() => setSeconds(v => Math.max(0, v - 1)), 1000); return () => clearTimeout(timer) }, [seconds])
43
+ return { seconds, start: () => setSeconds(30) }
44
+ }
45
+
46
+ export function RoleSignupExample({ locale = 'en', initialState = 'default' }: AccountExampleProps) {
47
+ const t = words(locale), [state, setState] = useState(initialState)
48
+ const [role, setRole] = useState(''), [name, setName] = useState(''), [email, setEmail] = useState(''), [password, setPassword] = useState(''), [terms, setTerms] = useState(false), [updates, setUpdates] = useState(false)
49
+ const rules = rulesFor(password, t), valid = Boolean(role && name.trim().length >= 2 && validEmail(email) && rules[0].passed && rules.filter(r => r.passed).length >= 2 && terms)
50
+ return <AuthFrame locale={locale} title={state === 'invite-only' ? t('Access by invitation', '초대받아 시작하기', 'الدخول بدعوة') : t('Create your account', '계정 만들기', 'إنشاء حسابك')} subtitle={t('Choose how you will participate, then create your account.', '어떻게 참여할지 선택하고 계정을 만드세요.', 'اختر طريقة مشاركتك ثم أنشئ حسابك.')} footer={sample(t)}>
51
+ {state === 'success' ? <OutcomeNotice title={t('Ready to verify your email', '이메일 인증 준비 완료', 'جاهز للتحقق من بريدك')} actionLabel={again(t)} onAction={() => setState('default')}>{sample(t)}</OutcomeNotice> : state === 'invite-only' ? <form onSubmit={e => { e.preventDefault(); if (validEmail(email)) setState('success') }}><Notice>{t('New accounts need an invitation. Request access with your email.', '현재는 초대가 필요해요. 이메일로 참여를 요청하세요.', 'تحتاج الحسابات الجديدة إلى دعوة. اطلب الدخول ببريدك.')}</Notice><Email t={t} value={email} onChange={setEmail} /><Button disabled={!validEmail(email)} type="submit">{t('Request access', '참여 요청', 'طلب الدخول')}</Button></form> : <>
52
+ {state === 'email-taken' ? <Notice>{t('This email is already registered. Try signing in instead.', '이미 등록된 이메일이에요. 로그인을 이용하세요.', 'هذا البريد مسجل بالفعل. جرّب تسجيل الدخول.')}</Notice> : null}
53
+ <ChoiceCards label={t('I am joining as…', '참여 목적', 'أشارك بصفتي…')} value={role} onChange={setRole} options={[
54
+ { id: 'member', label: t('Learner', '학습자', 'متعلم'), description: t('Learn from experts', '전문가에게 배우기', 'التعلم من الخبراء') },
55
+ { id: 'expert', label: t('Expert', '전문가', 'خبير'), description: t('Share your expertise', '전문성 나누기', 'مشاركة خبراتك') },
56
+ { id: 'organization', label: t('Organization', '조직', 'مؤسسة'), description: t('Support a team', '팀 지원하기', 'دعم الفريق') },
57
+ ]} /><Social t={t} />
58
+ <form onSubmit={e => { e.preventDefault(); if (valid) { setPassword(''); setState('success') } }}>
59
+ <ValidatedField label={t('Full name', '이름', 'الاسم الكامل')} value={name} onChange={setName} required /><Email t={t} value={email} onChange={setEmail} />
60
+ <PasswordField label={t('Password', '비밀번호', 'كلمة المرور')} value={password} onChange={setPassword} rules={rules} {...passwordProps(t)} />
61
+ <Check checked={terms} onChange={setTerms}>{t('I agree to the example terms and privacy policy.', '예시 이용약관과 개인정보 처리방침에 동의합니다.', 'أوافق على الشروط وسياسة الخصوصية التجريبية.')}</Check>
62
+ <Check checked={updates} onChange={setUpdates}>{t('Send me product updates (optional).', '제품 소식 받기 (선택)', 'إرسال تحديثات المنتج (اختياري).')}</Check>
63
+ <Button type="submit" disabled={!valid}>{t('Create account', '계정 만들기', 'إنشاء الحساب')}</Button>
64
+ </form>
65
+ </>}
66
+ </AuthFrame>
67
+ }
68
+
69
+ export function LoginExample({ locale = 'en', initialState = 'default' }: AccountExampleProps) {
70
+ const t = words(locale), [state, setState] = useState(initialState), [mode, setMode] = useState('password'), [email, setEmail] = useState(''), [password, setPassword] = useState(''), [remember, setRemember] = useState(true)
71
+ return <AuthFrame locale={locale} title={t('Welcome back', '다시 만나 반가워요', 'مرحباً بعودتك')} subtitle={t('Sign in with your account.', '계정으로 로그인하세요.', 'سجّل الدخول بحسابك.')} footer={sample(t)}>
72
+ {state !== 'default' ? <Notice>{state === 'error' ? t('Incorrect email or password. Try again or reset your password.', '이메일 또는 비밀번호를 확인하세요. 다시 시도하거나 비밀번호를 재설정할 수 있어요.', 'البريد أو كلمة المرور غير صحيحة. حاول مجدداً أو أعد تعيينها.') : state === 'suspended' ? t('Account suspended. Contact support to restore access.', '계정이 일시 중지됐어요. 지원팀에 복구를 요청하세요.', 'الحساب موقوف. تواصل مع الدعم لاستعادته.') : state === 'magic-sent' ? t('Example: a sign-in link would be sent to your email.', '예시: 이메일로 로그인 링크를 보낸 상태입니다.', 'مثال: سيتم إرسال رابط الدخول إلى بريدك.') : t('Example sign-in completed. No session was created.', '로그인 예시 완료. 실제 세션은 생성되지 않았어요.', 'اكتمل مثال الدخول دون إنشاء جلسة فعلية.')}</Notice> : null}
73
+ <Social t={t} /><form onSubmit={e => { e.preventDefault(); if (validEmail(email) && (mode === 'magic' || password)) { setPassword(''); setState(mode === 'magic' ? 'magic-sent' : 'success') } }}>
74
+ <Email t={t} value={email} onChange={setEmail} />
75
+ {mode === 'password' ? <><PasswordField label={t('Password', '비밀번호', 'كلمة المرور')} value={password} onChange={setPassword} {...passwordProps(t)} /><Check checked={remember} onChange={setRemember}>{t('Keep me signed in', '로그인 유지', 'إبقائي مسجلاً')}</Check></> : null}
76
+ <Button type="submit" disabled={!validEmail(email) || (mode === 'password' && !password)}>{mode === 'password' ? t('Sign in', '로그인', 'تسجيل الدخول') : t('Email me a sign-in link', '이메일로 로그인 링크 받기', 'إرسال رابط الدخول')}</Button>
77
+ <Button type="button" variant="ghost" onClick={() => { setMode(mode === 'password' ? 'magic' : 'password'); setState('default') }}>{mode === 'password' ? t('Use an email link instead', '이메일 링크로 로그인', 'استخدم رابط البريد بدلاً من ذلك') : t('Use a password instead', '비밀번호로 로그인', 'استخدم كلمة المرور')}</Button>
78
+ </form>
79
+ </AuthFrame>
80
+ }
81
+
82
+ export function PasswordRecoveryExample({ locale = 'en', initialState = 'default' }: AccountExampleProps) {
83
+ const t = words(locale), [state, setState] = useState(initialState), [email, setEmail] = useState(''), cooldown = useCooldown()
84
+ return <AuthFrame locale={locale} title={state === 'sent' ? t('Check your inbox', '이메일을 확인하세요', 'تحقق من بريدك') : state === 'expired' ? t('This link has expired', '링크가 만료됐어요', 'انتهت صلاحية الرابط') : t('Forgot your password?', '비밀번호를 잊으셨나요?', 'نسيت كلمة المرور؟')} subtitle={t('Request a link to choose a new password.', '새 비밀번호를 설정할 링크를 요청하세요.', 'اطلب رابطاً لاختيار كلمة مرور جديدة.')} footer={sample(t)}>
85
+ {state === 'expired' ? <Button onClick={() => setState('default')}>{t('Request a new reset link', '새 재설정 링크 요청', 'طلب رابط جديد')}</Button> : state === 'sent' ? <>
86
+ <Notice>{t('If an account exists, reset instructions will arrive. This example sends no email.', '계정이 있다면 재설정 안내가 전송됩니다. 이 예시에서는 이메일을 보내지 않아요.', 'إذا كان الحساب موجوداً فستصلك تعليمات. هذا المثال لا يرسل بريداً.')}</Notice>
87
+ <Button variant="outline" disabled={cooldown.seconds > 0} onClick={cooldown.start}>{cooldown.seconds ? t(`Resend in ${cooldown.seconds}s`, `${cooldown.seconds}초 후 재전송`, `إعادة الإرسال خلال ${cooldown.seconds} ث`) : t('Resend', '다시 보내기', 'إعادة الإرسال')}</Button>
88
+ <Button variant="ghost" onClick={() => setState('default')}>{t('Use a different email', '다른 이메일 사용', 'استخدام بريد آخر')}</Button>
89
+ </> : <form onSubmit={e => { e.preventDefault(); if (validEmail(email)) { setState('sent'); cooldown.start() } else setState('invalid') }}>
90
+ {state === 'invalid' ? <Notice>{t('Check your email and try again.', '이메일을 확인하고 다시 시도하세요.', 'تحقق من بريدك وحاول مجدداً.')}</Notice> : null}
91
+ <Email t={t} value={email} onChange={setEmail} /><Button type="submit" disabled={!validEmail(email)}>{t('Send reset link', '재설정 링크 받기', 'إرسال رابط إعادة التعيين')}</Button>
92
+ </form>}
93
+ </AuthFrame>
94
+ }
95
+
96
+ export function PasswordResetExample({ locale = 'en', initialState = 'default' }: AccountExampleProps) {
97
+ const t = words(locale), [state, setState] = useState(initialState), [password, setPassword] = useState(''), [confirm, setConfirm] = useState(''), [automatic, setAutomatic] = useState(true)
98
+ const rules = rulesFor(password, t), valid = rules[0].passed && rules.filter(r => r.passed).length >= 2 && password === confirm
99
+ return <AuthFrame locale={locale} title={t('Choose a new password', '새 비밀번호 설정', 'اختر كلمة مرور جديدة')} subtitle={t('Use a strong password you have not used before.', '이전에 쓰지 않은 안전한 비밀번호를 사용하세요.', 'استخدم كلمة مرور قوية لم تستخدمها من قبل.')} footer={sample(t)}>
100
+ {state === 'success' ? <OutcomeNotice title={t('Password update preview', '비밀번호 변경 완료 예시', 'معاينة تحديث كلمة المرور')} actionLabel={again(t)} onAction={() => setState('default')}>{sample(t)}</OutcomeNotice> : state === 'expired' ? <OutcomeNotice tone="warning" title={t('Reset link expired', '재설정 링크 만료', 'انتهت صلاحية الرابط')} actionLabel={t('Request a new link', '새 링크 요청', 'طلب رابط جديد')} onAction={() => setState('default')}>{t('An expired link cannot be reused.', '만료된 링크는 다시 사용할 수 없어요.', 'لا يمكن إعادة استخدام رابط منتهي الصلاحية.')}</OutcomeNotice> : <form onSubmit={e => { e.preventDefault(); if (valid) { setPassword(''); setConfirm(''); setState('success') } }}>
101
+ <PasswordField label={t('New password', '새 비밀번호', 'كلمة المرور الجديدة')} value={password} onChange={setPassword} rules={rules} {...passwordProps(t)} />
102
+ <PasswordField label={t('Confirm new password', '새 비밀번호 확인', 'تأكيد كلمة المرور')} value={confirm} onChange={setConfirm} error={state === 'mismatch' || (confirm && password !== confirm) ? t('Passwords do not match.', '비밀번호가 일치하지 않아요.', 'كلمتا المرور غير متطابقتين.') : undefined} {...passwordProps(t)} />
103
+ <Check checked={automatic} onChange={setAutomatic}>{t('Sign me in after resetting', '변경 후 자동 로그인', 'تسجيل الدخول بعد إعادة التعيين')}</Check><Button type="submit" disabled={!valid}>{t('Update password', '비밀번호 변경', 'تحديث كلمة المرور')}</Button>
104
+ </form>}
105
+ </AuthFrame>
106
+ }
107
+
108
+ export function EmailVerificationExample({ locale = 'en', initialState = 'waiting' }: AccountExampleProps) {
109
+ const t = words(locale), [state, setState] = useState(initialState), [email, setEmail] = useState('you@example.com'), [editing, setEditing] = useState(false), [draft, setDraft] = useState(email), cooldown = useCooldown()
110
+ return <AuthFrame locale={locale} title={state === 'success' ? t('Email verified', '이메일 인증 완료', 'تم التحقق من البريد') : state === 'expired' ? t('Verification link expired', '인증 링크 만료', 'انتهت صلاحية رابط التحقق') : t('Check your inbox', '이메일을 확인하세요', 'تحقق من بريدك')} footer={sample(t)}>
111
+ {state === 'success' ? <OutcomeNotice title={t('Verification result preview', '인증 결과 예시', 'معاينة نتيجة التحقق')} actionLabel={again(t)} onAction={() => setState('waiting')}>{sample(t)}</OutcomeNotice> : <>
112
+ <div className="auth-part-card">{editing ? <form onSubmit={e => { e.preventDefault(); if (validEmail(draft)) { setEmail(draft); setEditing(false) } }}><Email t={t} value={draft} onChange={setDraft} /><div className="auth-part-actions"><Button type="submit" disabled={!validEmail(draft)}>{t('Save', '저장', 'حفظ')}</Button><Button type="button" variant="ghost" onClick={() => setEditing(false)}>{t('Cancel', '취소', 'إلغاء')}</Button></div></form> : <><strong>{email}</strong><p>{t('Waiting for verification…', '인증을 기다리고 있어요…', 'بانتظار التحقق…')}</p><Button variant="ghost" onClick={() => { setDraft(email); setEditing(true) }}>{t('Edit email', '이메일 수정', 'تعديل البريد')}</Button></>}</div>
113
+ <Button variant="outline" disabled={cooldown.seconds > 0} onClick={() => { cooldown.start(); setState('waiting') }}>{cooldown.seconds ? t(`Resend in ${cooldown.seconds}s`, `${cooldown.seconds}초 후 재전송`, `إعادة الإرسال خلال ${cooldown.seconds} ث`) : t('Resend verification email', '인증 이메일 다시 보내기', 'إعادة إرسال بريد التحقق')}</Button>
114
+ </>}
115
+ </AuthFrame>
116
+ }
117
+
118
+ export function TwoFactorExample({ locale = 'en', initialState = 'verify' }: AccountExampleProps) {
119
+ const t = words(locale), [mode, setMode] = useState(initialState), [code, setCode] = useState(''), [trust, setTrust] = useState(false), [error, setError] = useState(''), [complete, setComplete] = useState(false)
120
+ const backup = ['demo-0001', 'demo-0002', 'demo-0003', 'demo-0004']
121
+ const verify = () => { if (mode === 'recovery' ? backup.includes(code) : code === '123456') { setCode(''); setError(''); if (mode === 'setup-verify') setMode('backup'); else setComplete(true) } else setError(t('Incorrect code. Try again.', '코드가 올바르지 않아요. 다시 시도하세요.', 'الرمز غير صحيح. حاول مجدداً.')) }
122
+ return <AuthFrame locale={locale} title={mode === 'setup' ? t('Set up an authenticator', '인증 앱 설정', 'إعداد تطبيق المصادقة') : mode === 'backup' ? t('Save your backup codes', '복구 코드 보관', 'حفظ رموز الاسترداد') : mode === 'recovery' ? t('Use a backup code', '복구 코드로 인증', 'استخدام رمز استرداد') : t('Enter your verification code', '인증 코드 입력', 'أدخل رمز التحقق')} footer={sample(t)}>
123
+ {complete ? <OutcomeNotice title={t('Verification preview complete', '인증 예시 완료', 'اكتملت معاينة التحقق')} actionLabel={again(t)} onAction={() => { setComplete(false); setCode('') }}>{sample(t)}</OutcomeNotice> : mode === 'setup' ? <>
124
+ <Notice>{t('A real setup receives a QR code and secret from the server. This preview does not enroll an authenticator.', '실제 설정에서는 서버가 QR 코드와 키를 발급합니다. 여기서는 인증 앱을 등록하지 않아요.', 'يصدر الخادم رمز QR والمفتاح في الإعداد الفعلي. لا يتم تسجيل تطبيق هنا.')}</Notice><details className="auth-part-card"><summary>{t('Cannot scan? Enter manually', '스캔할 수 없나요? 직접 입력', 'لا تستطيع المسح؟ أدخل يدوياً')}</summary><code>DEMO-KEY-NOT-A-SECRET</code></details><Button onClick={() => setMode('setup-verify')}>{continueLabel(t)}</Button>
125
+ </> : mode === 'backup' ? <><div className="auth-part-card"><p>{t('Sample codes — not valid credentials', '예시 코드 — 실제 인증에 사용할 수 없음', 'رموز تجريبية — ليست بيانات اعتماد')}</p>{backup.map(value => <code key={value}>{value}</code>)}</div><Button onClick={() => setMode('recovery')}>{t('Try a backup code', '복구 코드 사용해보기', 'جرّب رمز استرداد')}</Button></> : <form onSubmit={e => { e.preventDefault(); verify() }}>
126
+ {mode === 'recovery' ? <ValidatedField label={t('Backup code', '복구 코드', 'رمز الاسترداد')} value={code} onChange={setCode} hint="demo-0001" error={error} /> : <VerificationCode label={t('6-digit code', '6자리 코드', 'رمز من ٦ أرقام')} digitLabel={index => t(`Digit ${index}`, `${index}번째 숫자`, `الرقم ${index}`)} value={code} onChange={v => { setCode(v); setError('') }} error={error} />}
127
+ <p>{t('Demo: 123456, or backup code demo-0001.', '체험 코드: 123456 / 복구 코드: demo-0001', 'للتجربة: 123456 أو رمز الاسترداد demo-0001.')}</p>
128
+ <Check checked={trust} onChange={setTrust}>{t('Trust this device', '이 기기 기억하기', 'الوثوق بهذا الجهاز')}</Check>
129
+ <Button type="submit" disabled={mode === 'recovery' ? !code : !/^\d{6}$/.test(code)}>{t('Verify', '인증', 'تحقق')}</Button>
130
+ <Button type="button" variant="ghost" onClick={() => { setCode(''); setError(''); setMode(mode === 'recovery' ? 'verify' : 'recovery') }}>{mode === 'recovery' ? t('Use authenticator code', '인증 앱 코드 사용', 'استخدام رمز التطبيق') : t('Use a backup code', '복구 코드 사용', 'استخدام رمز استرداد')}</Button>
131
+ </form>}
132
+ </AuthFrame>
133
+ }
@@ -0,0 +1,79 @@
1
+ // Ports of the indexed directory, conversation list, notification list and
2
+ // delivery preferences. The enclosing source portal is deliberately not copied.
3
+ import { useState, type ReactNode } from 'react'
4
+ import { Button, TextInput, NativeSelect, ToggleButtonGroup, ToggleButtonGroupItem } from '@utopia-studio-design/design-system'
5
+ import { ActivityList, ChannelPreferences, DirectoryCards } from '../semantic-pattern-parts/ProductParts'
6
+ import type { ExampleLocale } from './copy'
7
+ import './ux-examples.css'
8
+
9
+ type Props = { locale?: ExampleLocale; initialState?: string }
10
+ const words = (locale: ExampleLocale) => (en: string, ko: string, ar: string) => locale === 'ko' ? ko : locale === 'ar' ? ar : en
11
+ function Frame({ locale, title, description, children }: { locale: ExampleLocale; title: string; description: string; children: ReactNode }) {
12
+ const t = words(locale)
13
+ return <section className="ux-example product-example" data-slot="product-example" lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}><div className="product-content"><header><h3>{title}</h3><p>{description}</p></header>{children}</div><footer>{t('Interactive sample. Nothing is sent, booked or saved to an account.', '체험용 예시입니다. 실제 메시지 전송·예약·계정 저장은 하지 않아요.', 'مثال تفاعلي. لا يتم إرسال شيء أو حجزه أو حفظه في حساب.')}</footer></section>
14
+ }
15
+
16
+ export function PeopleDirectoryExample({ locale = 'en', initialState = 'default' }: Props) {
17
+ const t = words(locale), [query, setQuery] = useState(initialState === 'empty' ? 'zzzz' : ''), [specialty, setSpecialty] = useState('all'), [availability, setAvailability] = useState(initialState === 'available' ? 'available' : 'all'), [sort, setSort] = useState('rating'), [selection, setSelection] = useState<{id: string; action: boolean} | null>(null)
18
+ const people = [
19
+ { id: 'alex', name: 'Alex Kim', role: t('Product designer', '프로덕트 디자이너', 'مصمم منتجات'), organization: 'North Studio', languages: t('English, Korean', '영어, 한국어', 'الإنجليزية والكورية'), specialty: 'design', available: true, rating: 4.9, sessions: 32 },
20
+ { id: 'noor', name: 'Noor Ali', role: t('Research specialist', '리서치 전문가', 'متخصصة أبحاث'), organization: 'Field Lab', languages: t('Arabic, English', '아랍어, 영어', 'العربية والإنجليزية'), specialty: 'research', available: true, rating: 4.8, sessions: 45 },
21
+ { id: 'sam', name: 'Sam Lee', role: t('Design lead', '디자인 리드', 'قائد تصميم'), organization: 'North Studio', languages: t('English', '영어', 'الإنجليزية'), specialty: 'design', available: false, rating: 4.7, sessions: 61 },
22
+ ]
23
+ const specialtyLabel = (s: string) => s === 'design' ? t('Design', '디자인', 'التصميم') : t('Research', '리서치', 'البحث')
24
+ const filtered = people.filter(p => `${p.name} ${p.organization} ${p.role} ${specialtyLabel(p.specialty)}`.toLocaleLowerCase(locale).includes(query.toLocaleLowerCase(locale)) && (specialty === 'all' || p.specialty === specialty) && (availability === 'all' || (availability === 'available') === p.available)).sort((a,b) => sort === 'name' ? a.name.localeCompare(b.name) : sort === 'sessions' ? b.sessions - a.sessions : b.rating - a.rating)
25
+ const selected = people.find(p => p.id === selection?.id)
26
+ return <Frame locale={locale} title={t('Find an expert', '전문가 찾기', 'ابحث عن خبير')} description={t('Search by name, expertise or organization, then compare availability.', '이름·전문 분야·소속으로 검색하고 참여 가능 여부를 비교하세요.', 'ابحث بالاسم أو التخصص أو المؤسسة وقارن التوفر.')}>
27
+ <div className="product-toolbar"><label>{t('Search people', '사람 검색', 'البحث عن أشخاص')}<TextInput type="search" value={query} onChange={e => setQuery(e.target.value)} /></label>
28
+ <label>{t('Expertise', '전문 분야', 'التخصص')}<NativeSelect aria-label={t('Expertise', '전문 분야', 'التخصص')} value={specialty} onChange={e => setSpecialty(e.target.value)}><option value="all">{t('All expertise', '모든 분야', 'كل التخصصات')}</option>{['design','research'].map(s => <option key={s} value={s}>{specialtyLabel(s)}</option>)}</NativeSelect></label>
29
+ <label>{t('Availability', '참여 가능 여부', 'التوفر')}<NativeSelect aria-label={t('Availability', '참여 가능 여부', 'التوفر')} value={availability} onChange={e => setAvailability(e.target.value)}><option value="all">{t('All', '전체', 'الكل')}</option><option value="available">{t('Available', '참여 가능', 'متاح')}</option><option value="limited">{t('Limited', '제한적', 'محدود')}</option></NativeSelect></label>
30
+ <label>{t('Sort', '정렬', 'الترتيب')}<NativeSelect aria-label={t('Sort', '정렬', 'الترتيب')} value={sort} onChange={e => setSort(e.target.value)}><option value="rating">{t('Highest rated', '평점순', 'الأعلى تقييماً')}</option><option value="sessions">{t('Most sessions', '진행 횟수순', 'الأكثر جلسات')}</option><option value="name">{t('Name', '이름순', 'الاسم')}</option></NativeSelect></label>
31
+ <Button variant="outline" onClick={() => { setQuery(''); setSpecialty('all'); setAvailability('all'); setSort('rating'); setSelection(null) }}>{t('Clear filters', '필터 초기화', 'مسح المرشحات')}</Button>
32
+ </div>
33
+ <p role="status">{t(`${filtered.length} people`, `${filtered.length}명`, `${filtered.length} أشخاص`)}</p>
34
+ <DirectoryCards items={filtered.map(p => ({...p, expertise: specialtyLabel(p.specialty), availability: p.available ? t('Available', '참여 가능', 'متاح') : t('Limited availability', '일정 제한', 'توفر محدود'), stats: t(`${p.rating} rating · ${p.sessions} sessions`, `평점 ${p.rating} · ${p.sessions}회 진행`, `التقييم ${p.rating} · ${p.sessions} جلسة`)}))} emptyLabel={t('No people match these filters.', '조건에 맞는 사람이 없어요.', 'لا أشخاص يطابقون المرشحات.')} profileLabel={t('View profile', '프로필 보기', 'عرض الملف')} actionLabel={t('Request a session', '세션 요청', 'طلب جلسة')} onProfile={id => setSelection({id,action:false})} onAction={id => setSelection({id,action:true})} />
35
+ {selected ? <section className="product-card" role="status"><h4>{selected.name}</h4><p>{selection?.action ? t('Selected for a sample session request. A booking service must handle the next step.', '세션 요청 대상으로 선택했어요. 실제 예약은 예약 서비스를 연결해야 합니다.', 'تم الاختيار لطلب جلسة تجريبي. تتطلب الخطوة التالية خدمة حجز.') : `${selected.role} · ${selected.organization} · ${selected.languages}`}</p><Button variant="outline" onClick={() => setSelection(null)}>{t('Close', '닫기', 'إغلاق')}</Button></section> : null}
36
+ </Frame>
37
+ }
38
+
39
+ export function ConversationListExample({ locale = 'en', initialState = 'default' }: Props) {
40
+ const t = words(locale), [empty, setEmpty] = useState(initialState === 'empty'), [opened, setOpened] = useState(''), [read, setRead] = useState<string[]>([])
41
+ const rows = [
42
+ {id:'alex', title:'Alex Kim', body:t('I will review the files and reply with notes.', '파일을 확인하고 의견을 보내드릴게요.', 'سأراجع الملفات وأرسل ملاحظاتي.'), time:t('5 minutes ago', '5분 전', 'قبل ٥ دقائق'), unread:!read.includes('alex')},
43
+ {id:'noor', title:'Noor Ali', body:t('Can we meet 30 minutes before the session?', '세션 30분 전에 만날 수 있을까요?', 'هل يمكننا اللقاء قبل الجلسة بنصف ساعة؟'), time:t('Yesterday', '어제', 'أمس'), unread:false},
44
+ ]
45
+ const current = rows.find(row => row.id === opened)
46
+ return <Frame locale={locale} title={t('Messages', '메시지', 'الرسائل')} description={t('Private conversations associated with your sessions.', '참여 중인 세션의 개인 대화를 모아 봅니다.', 'محادثات خاصة مرتبطة بجلساتك.')}>
47
+ {current ? <section className="product-card"><h4>{current.title}</h4><p>{current.body}</p><p>{t('Conversation destination preview. Connect the host chat to continue.', '대화 이동 예시입니다. 실제 대화를 이어가려면 호스트 채팅을 연결하세요.', 'معاينة وجهة المحادثة. اربط دردشة التطبيق للمتابعة.')}</p><Button variant="outline" onClick={() => setOpened('')}>{t('Back to messages', '메시지 목록으로', 'العودة إلى الرسائل')}</Button></section> : <ActivityList items={empty ? [] : rows} unreadLabel={t('Unread', '읽지 않음', 'غير مقروء')} emptyLabel={t('No active conversations. A conversation appears when a session is accepted.', '진행 중인 대화가 없어요. 세션이 수락되면 대화가 시작됩니다.', 'لا محادثات نشطة. تظهر المحادثة عند قبول الجلسة.')} onOpen={id => {setOpened(id); setRead(old => [...old,id])}} />}
48
+ {empty ? <Button variant="outline" onClick={() => setEmpty(false)}>{t('Load sample conversations', '예시 대화 불러오기', 'تحميل محادثات تجريبية')}</Button> : null}
49
+ </Frame>
50
+ }
51
+
52
+ export function NotificationsExample({ locale = 'en', initialState = 'all' }: Props) {
53
+ const t = words(locale), [tab,setTab] = useState(initialState === 'empty' ? 'all' : initialState), [opened,setOpened] = useState(''), [read,setRead] = useState<string[]>([])
54
+ const rows = initialState === 'empty' ? [] : [
55
+ {id:'session',title:t('Your session was accepted', '세션 요청이 수락되었어요', 'تم قبول جلستك'),body:t('Tuesday at 10:00 · Sample schedule', '화요일 10:00 · 예시 일정', 'الثلاثاء ١٠:٠٠ · جدول تجريبي'),time:t('12 minutes ago', '12분 전', 'قبل ١٢ دقيقة'),unread:!read.includes('session')},
56
+ {id:'message',title:t('New message from Alex', 'Alex의 새 메시지', 'رسالة جديدة من Alex'),body:t('Please upload the project brief.', '프로젝트 브리프를 올려주세요.', 'يرجى رفع موجز المشروع.'),time:t('1 hour ago', '1시간 전', 'قبل ساعة'),unread:!read.includes('message')},
57
+ {id:'credits',title:t('Credits allocated', '크레딧이 배정되었어요', 'تم تخصيص الرصيد'),body:t('3 credits added to the sample account.', '예시 계정에 3크레딧 추가.', 'أضيفت ٣ وحدات للحساب التجريبي.'),time:t('Yesterday', '어제', 'أمس'),unread:false},
58
+ {id:'check',title:t('Pre-check due soon', '사전 점검을 진행하세요', 'اقترب موعد الفحص المسبق'),body:t('Check connectivity before the session.', '세션 전에 연결 상태를 점검하세요.', 'افحص الاتصال قبل الجلسة.'),time:t('Yesterday', '어제', 'أمس'),unread:false},
59
+ ]
60
+ const selected = rows.find(row => row.id === opened), unread = rows.filter(row => row.unread)
61
+ return <Frame locale={locale} title={t('Notifications', '알림', 'الإشعارات')} description={t('Updates about sessions, messages and credits.', '세션·메시지·크레딧의 새 소식을 확인하세요.', 'تحديثات الجلسات والرسائل والرصيد.')}>
62
+ <ToggleButtonGroup type="single" label={t('Notification filter', '알림 필터', 'مرشح الإشعارات')} value={tab} onValueChange={(v:string) => {if(v) {setTab(v);setOpened('')}}}><ToggleButtonGroupItem value="all" label={t(`All (${rows.length})`, `전체 (${rows.length})`, `الكل (${rows.length})`)} /><ToggleButtonGroupItem value="unread" label={t(`Unread (${unread.length})`, `읽지 않음 (${unread.length})`, `غير مقروء (${unread.length})`)} /><ToggleButtonGroupItem value="archived" label={t('Archived', '보관됨', 'المؤرشفة')} /></ToggleButtonGroup>
63
+ <ActivityList items={tab === 'archived' ? [] : tab === 'unread' ? unread : rows} emptyLabel={tab === 'archived' ? t('No archived notifications.', '보관된 알림이 없어요.', 'لا إشعارات مؤرشفة.') : t('You are all caught up.', '새 알림을 모두 확인했어요.', 'أنت مطلع على كل جديد.')} unreadLabel={t('Unread', '읽지 않음', 'غير مقروء')} onOpen={id => {setOpened(id);setRead(old => [...old,id])}} />
64
+ {selected ? <section className="product-card" role="status"><strong>{selected.title}</strong><p>{selected.body}</p><Button variant="outline" onClick={() => setOpened('')}>{t('Close update', '알림 닫기', 'إغلاق التحديث')}</Button></section> : null}
65
+ </Frame>
66
+ }
67
+
68
+ export function NotificationPreferencesExample({ locale = 'en', initialState = 'default' }: Props) {
69
+ const t = words(locale), defaults = ['both','both','both','email','both','email','email']
70
+ const [channels,setChannels] = useState(defaults), [saved,setSaved] = useState(initialState === 'saved')
71
+ const labels = [t('Session confirmations','세션 확정','تأكيد الجلسات'),t('Session reminders','세션 리마인더','تذكيرات الجلسات'),t('New messages','새 메시지','رسائل جديدة'),t('Feedback','피드백','التعليقات'),t('Connectivity issues','연결 문제','مشكلات الاتصال'),t('Credit allocations','크레딧 배정','تخصيص الرصيد'),t('Product updates','제품 소식','تحديثات المنتج')]
72
+ const descriptions = [t('When a session is accepted or scheduled.','세션이 수락되거나 일정이 정해졌을 때.','عند قبول الجلسة أو جدولتها.'),t('Before an upcoming session starts.','예정된 세션이 시작되기 전에.','قبل بدء الجلسة.'),t('Private messages from collaborators.','협업자의 개인 메시지.','رسائل خاصة من المتعاونين.'),t('When new feedback is ready.','새 피드백이 등록되었을 때.','عندما تتوفر تعليقات جديدة.'),t('Pre-check failures and connection alerts.','사전 점검 실패와 연결 상태 알림.','إخفاقات الفحص وتنبيهات الاتصال.'),t('Changes to your available credits.','사용 가능한 크레딧이 바뀌었을 때.','تغييرات الرصيد المتاح.'),t('Feature announcements and news.','새 기능 안내와 소식.','الأخبار وإعلانات الميزات.')]
73
+ return <Frame locale={locale} title={t('Notification preferences','알림 설정','تفضيلات الإشعارات')} description={t('Choose a delivery channel for each event. Required channels here are sample policy, not your account settings.','항목별 수신 방식을 선택하세요. 필수 수신 항목은 이 예시의 정책이며 실제 계정 설정이 아닙니다.','اختر قناة لكل حدث. القنوات المطلوبة سياسة تجريبية وليست إعدادات حسابك.')}>
74
+ {saved ? <p className="product-card" role="status">{t('Sample preferences saved in this preview only.','이 미리보기 안에만 예시 설정을 저장했어요.','تم حفظ التفضيلات داخل هذه المعاينة فقط.')}</p> : null}
75
+ <ChannelPreferences items={labels.map((title,i) => ({id:String(i),title,description:descriptions[i],channel:channels[i],disallowed:[1,4].includes(i)?['none']:[]}))} options={['push','email','both','none'].map((id,i)=>({id,label:[t('Push','푸시','إشعارات فورية'),t('Email','이메일','البريد'),t('Both','모두','كلاهما'),t('None','받지 않음','لا شيء')][i]}))} requiredLabel={t('At least one channel required','최소 한 개 채널 필수','قناة واحدة على الأقل مطلوبة')} onChange={(id,channel)=> {setChannels(old=>old.map((v,i)=>String(i)===id?channel:v));setSaved(false)}} />
76
+ <section className="product-card"><h4>{t('Delivery channels','수신 채널','قنوات التوصيل')}</h4><p>{t('Push · Sample device enabled','푸시 · 예시 기기 활성화','الإشعارات · جهاز تجريبي مفعّل')}</p><p>{t('Email · sample@example.com · Example verified address','이메일 · sample@example.com · 예시 인증 주소','البريد · sample@example.com · عنوان تجريبي موثّق')}</p></section>
77
+ <div className="product-actions"><Button onClick={()=>setSaved(true)} disabled={saved}>{t('Save preferences','설정 저장','حفظ التفضيلات')}</Button><Button variant="outline" onClick={()=>{setChannels(defaults);setSaved(false)}}>{t('Reset preferences','설정 초기화','إعادة التفضيلات')}</Button></div>
78
+ </Frame>
79
+ }