@gntik-ai/templates 0.0.0-stage → 0.6.0

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 (300) hide show
  1. package/dist/index.d.ts +2420 -0
  2. package/dist/index.js +14520 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +64 -3
  5. package/src/accept-invitation/Page.test.tsx +40 -0
  6. package/src/accept-invitation/Page.tsx +153 -0
  7. package/src/accept-invitation/data.ts +22 -0
  8. package/src/accept-invitation/template.meta.ts +12 -0
  9. package/src/assistant-chat/Page.test.tsx +51 -0
  10. package/src/assistant-chat/Page.tsx +273 -0
  11. package/src/assistant-chat/data.ts +71 -0
  12. package/src/assistant-chat/template.meta.ts +12 -0
  13. package/src/audit-log/Page.test.tsx +58 -0
  14. package/src/audit-log/Page.tsx +284 -0
  15. package/src/audit-log/data.ts +133 -0
  16. package/src/audit-log/template.meta.ts +12 -0
  17. package/src/board/Lane.tsx +117 -0
  18. package/src/board/Page.test.tsx +68 -0
  19. package/src/board/Page.tsx +178 -0
  20. package/src/board/data.ts +127 -0
  21. package/src/board/template.meta.ts +12 -0
  22. package/src/board/useBoard.ts +146 -0
  23. package/src/changelog/Page.test.tsx +31 -0
  24. package/src/changelog/Page.tsx +174 -0
  25. package/src/changelog/data.ts +105 -0
  26. package/src/changelog/template.meta.ts +12 -0
  27. package/src/code-editor/Page.test.tsx +83 -0
  28. package/src/code-editor/Page.tsx +207 -0
  29. package/src/code-editor/data.ts +130 -0
  30. package/src/code-editor/template.meta.ts +12 -0
  31. package/src/contact/Page.test.tsx +33 -0
  32. package/src/contact/Page.tsx +272 -0
  33. package/src/contact/PublicFrame.tsx +84 -0
  34. package/src/contact/data.ts +129 -0
  35. package/src/contact/template.meta.ts +12 -0
  36. package/src/create-wizard/Overlay.test.tsx +68 -0
  37. package/src/create-wizard/Page.test.tsx +57 -0
  38. package/src/create-wizard/Page.tsx +246 -0
  39. package/src/create-wizard/data.ts +139 -0
  40. package/src/create-wizard/examples/Overlay.tsx +60 -0
  41. package/src/create-wizard/template.meta.ts +12 -0
  42. package/src/create-workspace/Page.test.tsx +41 -0
  43. package/src/create-workspace/Page.tsx +227 -0
  44. package/src/create-workspace/data.ts +61 -0
  45. package/src/create-workspace/template.meta.ts +12 -0
  46. package/src/data-explorer/Page.test.tsx +45 -0
  47. package/src/data-explorer/Page.tsx +247 -0
  48. package/src/data-explorer/data.ts +94 -0
  49. package/src/data-explorer/template.meta.ts +12 -0
  50. package/src/docs-article/ArticleFeedback.tsx +75 -0
  51. package/src/docs-article/Page.test.tsx +34 -0
  52. package/src/docs-article/Page.tsx +200 -0
  53. package/src/docs-article/data.ts +65 -0
  54. package/src/docs-article/template.meta.ts +12 -0
  55. package/src/evaluations/Page.test.tsx +40 -0
  56. package/src/evaluations/Page.tsx +238 -0
  57. package/src/evaluations/data.ts +194 -0
  58. package/src/evaluations/template.meta.ts +12 -0
  59. package/src/forgot-password/Page.test.tsx +199 -0
  60. package/src/forgot-password/Page.tsx +66 -0
  61. package/src/forgot-password/data.ts +5 -0
  62. package/src/forgot-password/examples/UsernameOrEmail.tsx +25 -0
  63. package/src/forgot-password/template.meta.ts +12 -0
  64. package/src/getting-started/Page.test.tsx +27 -0
  65. package/src/getting-started/Page.tsx +125 -0
  66. package/src/getting-started/data.ts +67 -0
  67. package/src/getting-started/template.meta.ts +12 -0
  68. package/src/help-home/Page.test.tsx +33 -0
  69. package/src/help-home/Page.tsx +159 -0
  70. package/src/help-home/data.ts +151 -0
  71. package/src/help-home/template.meta.ts +12 -0
  72. package/src/home-dashboard/Page.test.tsx +57 -0
  73. package/src/home-dashboard/Page.tsx +152 -0
  74. package/src/home-dashboard/data.ts +66 -0
  75. package/src/home-dashboard/template.meta.ts +12 -0
  76. package/src/incident-board/Page.test.tsx +77 -0
  77. package/src/incident-board/Page.tsx +263 -0
  78. package/src/incident-board/data.ts +220 -0
  79. package/src/incident-board/template.meta.ts +12 -0
  80. package/src/incident-console/Page.test.tsx +49 -0
  81. package/src/incident-console/Page.tsx +274 -0
  82. package/src/incident-console/data.ts +176 -0
  83. package/src/incident-console/template.meta.ts +12 -0
  84. package/src/index.ts +250 -0
  85. package/src/keyboard-shortcuts/Page.test.tsx +45 -0
  86. package/src/keyboard-shortcuts/Page.tsx +94 -0
  87. package/src/keyboard-shortcuts/ShortcutSheet.tsx +80 -0
  88. package/src/keyboard-shortcuts/data.ts +84 -0
  89. package/src/keyboard-shortcuts/template.meta.ts +12 -0
  90. package/src/landing/Page.test.tsx +27 -0
  91. package/src/landing/Page.tsx +110 -0
  92. package/src/landing/data.ts +17 -0
  93. package/src/landing/template.meta.ts +11 -0
  94. package/src/live-event-stream/Page.test.tsx +251 -0
  95. package/src/live-event-stream/Page.tsx +307 -0
  96. package/src/live-event-stream/data.ts +44 -0
  97. package/src/live-event-stream/examples/Connecting.tsx +5 -0
  98. package/src/live-event-stream/examples/Error.tsx +5 -0
  99. package/src/live-event-stream/examples/LiveEmpty.tsx +5 -0
  100. package/src/live-event-stream/examples/LiveWithEvents.tsx +6 -0
  101. package/src/live-event-stream/examples/Stopped.tsx +6 -0
  102. package/src/live-event-stream/template.meta.ts +74 -0
  103. package/src/messaging/Page.test.tsx +34 -0
  104. package/src/messaging/Page.tsx +236 -0
  105. package/src/messaging/data.ts +118 -0
  106. package/src/messaging/template.meta.ts +12 -0
  107. package/src/meta.ts +24 -0
  108. package/src/mfa/Page.test.tsx +22 -0
  109. package/src/mfa/Page.tsx +45 -0
  110. package/src/mfa/data.ts +6 -0
  111. package/src/mfa/template.meta.ts +12 -0
  112. package/src/model-providers/Page.test.tsx +58 -0
  113. package/src/model-providers/Page.tsx +198 -0
  114. package/src/model-providers/data.ts +101 -0
  115. package/src/model-providers/template.meta.ts +12 -0
  116. package/src/notification-inbox/Page.test.tsx +49 -0
  117. package/src/notification-inbox/Page.tsx +237 -0
  118. package/src/notification-inbox/data.ts +129 -0
  119. package/src/notification-inbox/template.meta.ts +12 -0
  120. package/src/pending-activation/Page.test.tsx +390 -0
  121. package/src/pending-activation/Page.tsx +145 -0
  122. package/src/pending-activation/data.ts +25 -0
  123. package/src/pending-activation/template.meta.ts +12 -0
  124. package/src/pricing/ComparisonTable.tsx +75 -0
  125. package/src/pricing/Page.test.tsx +29 -0
  126. package/src/pricing/Page.tsx +83 -0
  127. package/src/pricing/PublicFrame.tsx +84 -0
  128. package/src/pricing/data.ts +104 -0
  129. package/src/pricing/template.meta.ts +12 -0
  130. package/src/prompt-playground/Page.test.tsx +72 -0
  131. package/src/prompt-playground/Page.tsx +254 -0
  132. package/src/prompt-playground/data.ts +68 -0
  133. package/src/prompt-playground/template.meta.ts +12 -0
  134. package/src/public-hub/Page.test.tsx +176 -0
  135. package/src/public-hub/Page.tsx +125 -0
  136. package/src/public-hub/Source.test.ts +54 -0
  137. package/src/public-hub/data.ts +24 -0
  138. package/src/public-hub/examples/Dark.tsx +11 -0
  139. package/src/public-hub/examples/Full.tsx +6 -0
  140. package/src/public-hub/examples/HighContrast.tsx +11 -0
  141. package/src/public-hub/examples/Light.tsx +11 -0
  142. package/src/public-hub/examples/Loading.tsx +6 -0
  143. package/src/public-hub/examples/Minimal.tsx +5 -0
  144. package/src/public-hub/examples/MinimalLoading.tsx +5 -0
  145. package/src/public-hub/template.meta.ts +60 -0
  146. package/src/reset-password/Page.test.tsx +27 -0
  147. package/src/reset-password/Page.tsx +46 -0
  148. package/src/reset-password/data.ts +5 -0
  149. package/src/reset-password/template.meta.ts +12 -0
  150. package/src/resource-detail/Page.test.tsx +260 -0
  151. package/src/resource-detail/Page.tsx +262 -0
  152. package/src/resource-detail/data.ts +55 -0
  153. package/src/resource-detail/examples/postgres-table-detail.tsx +28 -0
  154. package/src/resource-detail/examples/realtime-configuration.tsx +26 -0
  155. package/src/resource-detail/examples/runtime-with-transition.tsx +30 -0
  156. package/src/resource-detail/examples/runtime-without-transition.tsx +33 -0
  157. package/src/resource-detail/examples/shell.ts +12 -0
  158. package/src/resource-detail/template.meta.ts +21 -0
  159. package/src/resource-form/Page.test.tsx +40 -0
  160. package/src/resource-form/Page.tsx +186 -0
  161. package/src/resource-form/data.ts +63 -0
  162. package/src/resource-form/template.meta.ts +12 -0
  163. package/src/resource-gallery/Page.test.tsx +42 -0
  164. package/src/resource-gallery/Page.tsx +125 -0
  165. package/src/resource-gallery/data.ts +44 -0
  166. package/src/resource-gallery/template.meta.ts +20 -0
  167. package/src/resource-index/Page.test.tsx +120 -0
  168. package/src/resource-index/Page.tsx +216 -0
  169. package/src/resource-index/data.ts +84 -0
  170. package/src/resource-index/template.meta.ts +21 -0
  171. package/src/resource-tree/Page.test.tsx +31 -0
  172. package/src/resource-tree/Page.tsx +232 -0
  173. package/src/resource-tree/data.ts +180 -0
  174. package/src/resource-tree/template.meta.ts +12 -0
  175. package/src/run-trace/Page.test.tsx +56 -0
  176. package/src/run-trace/Page.tsx +242 -0
  177. package/src/run-trace/data.ts +171 -0
  178. package/src/run-trace/template.meta.ts +12 -0
  179. package/src/scorecard/Page.test.tsx +38 -0
  180. package/src/scorecard/Page.tsx +160 -0
  181. package/src/scorecard/TargetsTable.tsx +92 -0
  182. package/src/scorecard/data.ts +118 -0
  183. package/src/scorecard/template.meta.ts +12 -0
  184. package/src/search-results/Page.test.tsx +43 -0
  185. package/src/search-results/Page.tsx +243 -0
  186. package/src/search-results/data.ts +130 -0
  187. package/src/search-results/template.meta.ts +12 -0
  188. package/src/settings-api-keys/Page.test.tsx +39 -0
  189. package/src/settings-api-keys/Page.tsx +171 -0
  190. package/src/settings-api-keys/data.ts +58 -0
  191. package/src/settings-api-keys/template.meta.ts +12 -0
  192. package/src/settings-appearance/Page.test.tsx +44 -0
  193. package/src/settings-appearance/Page.tsx +92 -0
  194. package/src/settings-appearance/data.ts +21 -0
  195. package/src/settings-appearance/template.meta.ts +11 -0
  196. package/src/settings-billing/Page.test.tsx +31 -0
  197. package/src/settings-billing/Page.tsx +71 -0
  198. package/src/settings-billing/data.ts +15 -0
  199. package/src/settings-billing/template.meta.ts +19 -0
  200. package/src/settings-integrations/Page.test.tsx +31 -0
  201. package/src/settings-integrations/Page.tsx +185 -0
  202. package/src/settings-integrations/data.ts +116 -0
  203. package/src/settings-integrations/template.meta.ts +11 -0
  204. package/src/settings-members/Page.test.tsx +32 -0
  205. package/src/settings-members/Page.tsx +140 -0
  206. package/src/settings-members/data.ts +28 -0
  207. package/src/settings-members/template.meta.ts +12 -0
  208. package/src/settings-notifications/Page.test.tsx +31 -0
  209. package/src/settings-notifications/Page.tsx +71 -0
  210. package/src/settings-notifications/data.ts +12 -0
  211. package/src/settings-notifications/template.meta.ts +11 -0
  212. package/src/settings-profile/Baseline.test.tsx +9 -0
  213. package/src/settings-profile/Editable.test.tsx +21 -0
  214. package/src/settings-profile/Page.test.tsx +41 -0
  215. package/src/settings-profile/Page.tsx +133 -0
  216. package/src/settings-profile/ReadOnly.test.tsx +143 -0
  217. package/src/settings-profile/ReadOnlyProfile.tsx +85 -0
  218. package/src/settings-profile/SettingsFrame.test.tsx +55 -0
  219. package/src/settings-profile/SettingsFrame.tsx +88 -0
  220. package/src/settings-profile/__tests__/dom.ts +8 -0
  221. package/src/settings-profile/data.ts +41 -0
  222. package/src/settings-profile/examples/ReadOnly.tsx +31 -0
  223. package/src/settings-profile/fixtures/editable.snap +1 -0
  224. package/src/settings-profile/settings-nav.ts +98 -0
  225. package/src/settings-profile/template.meta.ts +21 -0
  226. package/src/settings-security/MfaSetupDialog.tsx +96 -0
  227. package/src/settings-security/Page.test.tsx +44 -0
  228. package/src/settings-security/Page.tsx +158 -0
  229. package/src/settings-security/data.ts +11 -0
  230. package/src/settings-security/template.meta.ts +11 -0
  231. package/src/settings-workspace/Page.test.tsx +44 -0
  232. package/src/settings-workspace/Page.tsx +120 -0
  233. package/src/settings-workspace/data.ts +44 -0
  234. package/src/settings-workspace/template.meta.ts +19 -0
  235. package/src/shared/ConsoleShell.test.tsx +81 -0
  236. package/src/shared/ConsoleShell.tsx +98 -0
  237. package/src/shell-sidebar/Page.test.tsx +21 -0
  238. package/src/shell-sidebar/Page.tsx +85 -0
  239. package/src/shell-sidebar/data.ts +24 -0
  240. package/src/shell-sidebar/template.meta.ts +12 -0
  241. package/src/shell-topnav/Page.test.tsx +25 -0
  242. package/src/shell-topnav/Page.tsx +94 -0
  243. package/src/shell-topnav/data.ts +25 -0
  244. package/src/shell-topnav/template.meta.ts +12 -0
  245. package/src/sign-in/BrandPanel.tsx +34 -0
  246. package/src/sign-in/Page.test.tsx +41 -0
  247. package/src/sign-in/Page.tsx +73 -0
  248. package/src/sign-in/data.ts +11 -0
  249. package/src/sign-in/template.meta.ts +12 -0
  250. package/src/sign-in-card/Page.test.tsx +182 -0
  251. package/src/sign-in-card/Page.tsx +82 -0
  252. package/src/sign-in-card/data.ts +8 -0
  253. package/src/sign-in-card/examples/Username.tsx +28 -0
  254. package/src/sign-in-card/template.meta.ts +12 -0
  255. package/src/sign-up/Page.test.tsx +33 -0
  256. package/src/sign-up/Page.tsx +70 -0
  257. package/src/sign-up/data.ts +15 -0
  258. package/src/sign-up/template.meta.ts +12 -0
  259. package/src/sso-login/Page.test.tsx +41 -0
  260. package/src/sso-login/Page.tsx +151 -0
  261. package/src/sso-login/data.ts +10 -0
  262. package/src/sso-login/template.meta.ts +12 -0
  263. package/src/status-403/Page.test.tsx +23 -0
  264. package/src/status-403/Page.tsx +92 -0
  265. package/src/status-403/data.ts +13 -0
  266. package/src/status-403/template.meta.ts +12 -0
  267. package/src/status-404/Page.test.tsx +23 -0
  268. package/src/status-404/Page.tsx +60 -0
  269. package/src/status-404/PageHeading.test.tsx +52 -0
  270. package/src/status-404/data.ts +12 -0
  271. package/src/status-404/examples/PageHeader.tsx +28 -0
  272. package/src/status-404/template.meta.ts +12 -0
  273. package/src/status-500/Page.test.tsx +24 -0
  274. package/src/status-500/Page.tsx +70 -0
  275. package/src/status-500/data.ts +20 -0
  276. package/src/status-500/template.meta.ts +12 -0
  277. package/src/status-maintenance/Page.test.tsx +24 -0
  278. package/src/status-maintenance/Page.tsx +84 -0
  279. package/src/status-maintenance/data.ts +14 -0
  280. package/src/status-maintenance/template.meta.ts +12 -0
  281. package/src/styles.css +11 -0
  282. package/src/test/a11y.ts +18 -0
  283. package/src/test/setup.ts +36 -0
  284. package/src/upgrade-required/Page.test.tsx +28 -0
  285. package/src/upgrade-required/Page.tsx +79 -0
  286. package/src/upgrade-required/data.ts +19 -0
  287. package/src/upgrade-required/template.meta.ts +12 -0
  288. package/src/usage-analytics/Page.test.tsx +83 -0
  289. package/src/usage-analytics/Page.tsx +184 -0
  290. package/src/usage-analytics/data.ts +161 -0
  291. package/src/usage-analytics/template.meta.ts +21 -0
  292. package/src/verify-email/Page.test.tsx +32 -0
  293. package/src/verify-email/Page.tsx +118 -0
  294. package/src/verify-email/data.ts +7 -0
  295. package/src/verify-email/template.meta.ts +12 -0
  296. package/src/workflow-builder/Page.test.tsx +97 -0
  297. package/src/workflow-builder/Page.tsx +163 -0
  298. package/src/workflow-builder/data.ts +13 -0
  299. package/src/workflow-builder/template.meta.ts +12 -0
  300. package/README.md +0 -3
@@ -0,0 +1,129 @@
1
+ import type { BreadcrumbItem, StatusDotTone } from '@gntik-ai/ui';
2
+
3
+ export type InboxCategory = 'mention' | 'deployment' | 'billing' | 'access';
4
+
5
+ export interface InboxNotification {
6
+ id: string;
7
+ title: string;
8
+ /** One-line preview in the list. */
9
+ preview: string;
10
+ /** Full message in the detail pane (paragraphs). */
11
+ body: string[];
12
+ from: string;
13
+ time: Date;
14
+ read: boolean;
15
+ category: InboxCategory;
16
+ /** Colour of the unread dot. */
17
+ tone: StatusDotTone;
18
+ /** Where "Open" leads. */
19
+ href?: string;
20
+ }
21
+
22
+ export const inboxCategoryLabels: Record<InboxCategory, string> = {
23
+ mention: 'Mention',
24
+ deployment: 'Deployment',
25
+ billing: 'Billing',
26
+ access: 'Access',
27
+ };
28
+
29
+ const MIN = 60_000;
30
+ const NOW = Date.now();
31
+
32
+ /** Neutral sample notifications, newest first. */
33
+ export const inboxNotifications: InboxNotification[] = [
34
+ {
35
+ id: 'ntf_01',
36
+ title: 'Budget alert: api-gateway at 97%',
37
+ preview: 'Requests will be throttled once the monthly limit is reached.',
38
+ body: [
39
+ 'The api-gateway project has used 97% of its monthly budget. Requests will be throttled once the limit is reached.',
40
+ 'Raise the limit from Billing or reduce traffic to stay under it until the cycle resets on the 1st.',
41
+ ],
42
+ from: 'Billing',
43
+ time: new Date(NOW - 4 * MIN),
44
+ read: false,
45
+ category: 'billing',
46
+ tone: 'warning',
47
+ href: '/billing',
48
+ },
49
+ {
50
+ id: 'ntf_02',
51
+ title: 'Sam Patel mentioned you in “Release checklist”',
52
+ preview: '@dana can you confirm the rollback plan before Friday?',
53
+ body: [
54
+ '“@dana can you confirm the rollback plan before Friday? The migration touches the orders table.”',
55
+ 'Reply in the thread to keep the discussion in one place.',
56
+ ],
57
+ from: 'Sam Patel',
58
+ time: new Date(NOW - 22 * MIN),
59
+ read: false,
60
+ category: 'mention',
61
+ tone: 'primary',
62
+ href: '/projects/web-app/docs/release-checklist',
63
+ },
64
+ {
65
+ id: 'ntf_03',
66
+ title: 'Deployment web-app build 418 succeeded',
67
+ preview: 'Promoted to production in eu-west-1 and us-east-1.',
68
+ body: [
69
+ 'Build 418 of web-app was promoted to production in eu-west-1 and us-east-1.',
70
+ 'Health checks passed in 42 s. The previous build stays available for instant rollback for 24 hours.',
71
+ ],
72
+ from: 'Deployments',
73
+ time: new Date(NOW - 75 * MIN),
74
+ read: false,
75
+ category: 'deployment',
76
+ tone: 'success',
77
+ href: '/deployments',
78
+ },
79
+ {
80
+ id: 'ntf_04',
81
+ title: 'Deployment orders-api build 212 failed',
82
+ preview: 'Health check timed out in ap-south-1. Traffic stayed on build 211.',
83
+ body: [
84
+ 'Build 212 of orders-api failed its health check in ap-south-1 after 30 s. Traffic stayed on build 211.',
85
+ 'Open the deployment to see the logs and retry.',
86
+ ],
87
+ from: 'Deployments',
88
+ time: new Date(NOW - 5 * 60 * MIN),
89
+ read: true,
90
+ category: 'deployment',
91
+ tone: 'destructive',
92
+ href: '/deployments',
93
+ },
94
+ {
95
+ id: 'ntf_05',
96
+ title: 'Lee Chen requested access to Billing',
97
+ preview: 'Approve or decline from Members.',
98
+ body: [
99
+ 'Lee Chen asked for a role that includes Billing permissions.',
100
+ 'Approve or decline the request from the Members page. Requests expire after 7 days.',
101
+ ],
102
+ from: 'Access requests',
103
+ time: new Date(NOW - 26 * 60 * MIN),
104
+ read: true,
105
+ category: 'access',
106
+ tone: 'info',
107
+ href: '/members',
108
+ },
109
+ {
110
+ id: 'ntf_06',
111
+ title: 'Invoice INV-2044 is available',
112
+ preview: '$4,210.00 for September · paid by card ending 4242.',
113
+ body: [
114
+ 'Your September invoice INV-2044 for $4,210.00 was paid with the card ending in 4242.',
115
+ 'Download the PDF from Billing.',
116
+ ],
117
+ from: 'Billing',
118
+ time: new Date(NOW - 3 * 24 * 60 * MIN),
119
+ read: true,
120
+ category: 'billing',
121
+ tone: 'neutral',
122
+ href: '/billing',
123
+ },
124
+ ];
125
+
126
+ export const inboxBreadcrumbs: BreadcrumbItem[] = [
127
+ { label: 'Acme Industries', href: '/overview' },
128
+ { label: 'Notifications' },
129
+ ];
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Notification inbox',
5
+ family: 'Work',
6
+ priority: 'P2',
7
+ status: 'beta',
8
+ description:
9
+ 'Master–detail inbox in the console: All / Unread / Mentions filter (ToggleGroup), read and unread rows, a detail pane with Open, Mark as unread and Archive, and Back on small screens.',
10
+ layout: 'SidebarLayout · Page · SplitLayout',
11
+ blocks: ['app-sidebar', 'app-topbar', 'page-header'],
12
+ };
@@ -0,0 +1,390 @@
1
+ import { render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectTypeOf } from 'vitest';
4
+ import { expectNoAxeViolations } from '../test/a11y';
5
+ import PendingActivationPage, { type PendingActivationPageProps } from './Page';
6
+
7
+ const recoveryLinks = [
8
+ { label: 'Sign in', href: '/sign-in' },
9
+ { label: 'Review sign-up', href: '/sign-up' },
10
+ ];
11
+ const states = [
12
+ ['default', {}],
13
+ ['loading', { loading: true }],
14
+ ['no-access', { errorState: 'no-access' }],
15
+ ['not-found', { errorState: 'not-found' }],
16
+ ['error', { errorState: 'error' }],
17
+ ] as const;
18
+ const onAction = vi.fn();
19
+ const actions = [
20
+ { label: 'Review request', href: '/requests/2048?from=pending' },
21
+ { label: 'Check status', onClick: onAction },
22
+ { label: 'Account details', href: '/account' },
23
+ ];
24
+
25
+ describe('PendingActivationPage', () => {
26
+ it('accepts consumer labels for recovery, skip navigation, actions and retry', () => {
27
+ const { rerender } = render(
28
+ <PendingActivationPage
29
+ recoveryLinks={recoveryLinks}
30
+ recoveryLabel="Return to your account"
31
+ skipLinkLabel="Skip to account status"
32
+ actionsLabel="Next steps"
33
+ actions={actions}
34
+ />,
35
+ );
36
+ expect(screen.getByRole('navigation', { name: 'Return to your account' })).toBeVisible();
37
+ expect(screen.getByRole('link', { name: 'Skip to account status' })).toBeInTheDocument();
38
+ expect(screen.getByRole('region', { name: 'Next steps' })).toBeVisible();
39
+ rerender(
40
+ <PendingActivationPage
41
+ recoveryLinks={recoveryLinks}
42
+ errorState="error"
43
+ retryLabel="Check again"
44
+ onRetry={() => {}}
45
+ />,
46
+ );
47
+ expect(screen.getByRole('button', { name: 'Check again' })).toBeVisible();
48
+ });
49
+
50
+ it.each(states)('tabs through the visible controls in %s with focus rings', async (_, state) => {
51
+ const user = userEvent.setup();
52
+ render(<PendingActivationPage {...state} recoveryLinks={recoveryLinks} actions={actions} onRetry={() => {}} />);
53
+ // AuthLayout supplies its skip link before the page's own controls.
54
+ await user.tab();
55
+ expect(screen.getByRole('link', { name: /Skip/ })).toHaveFocus();
56
+ const expected =
57
+ 'errorState' in state
58
+ ? state.errorState === 'error'
59
+ ? ['Retry', 'Sign in', 'Review sign-up']
60
+ : ['Sign in', 'Review sign-up']
61
+ : 'loading' in state
62
+ ? ['Sign in', 'Review sign-up']
63
+ : ['Review request', 'Check status', 'Account details', 'Sign in', 'Review sign-up'];
64
+ for (const name of expected) {
65
+ await user.tab();
66
+ const control = screen.getByRole(name === 'Retry' || name === 'Check status' ? 'button' : 'link', { name });
67
+ expect(control).toHaveFocus();
68
+ expect(control).toHaveClass('focus-visible:outline-focus-ring');
69
+ }
70
+ await user.tab();
71
+ expect(document.body).toHaveFocus();
72
+ });
73
+
74
+ it('preserves reading order and renders labels as text', () => {
75
+ const { container } = render(
76
+ <PendingActivationPage
77
+ recoveryLinks={recoveryLinks}
78
+ title="Heading"
79
+ intro="Intro"
80
+ statusTitle="Status"
81
+ statusMessage="Message"
82
+ reference={{ label: 'Reference', value: 'REF-1' }}
83
+ actions={[
84
+ {
85
+ label: '<script>throw new Error()</script>',
86
+ href: '/continue?next=%3Cscript%3E',
87
+ },
88
+ ]}
89
+ />,
90
+ );
91
+ const content = screen.getByRole('main').textContent ?? '';
92
+ const offsets = [
93
+ 'Heading',
94
+ 'Intro',
95
+ 'Status',
96
+ 'Message',
97
+ 'Reference',
98
+ 'REF-1',
99
+ '<script>throw new Error()</script>',
100
+ 'Sign in',
101
+ ].map((text) => content.indexOf(text));
102
+ expect(offsets.every((offset) => offset >= 0)).toBe(true);
103
+ expect(offsets).toEqual([...offsets].sort((a, b) => a - b));
104
+ expect(container.querySelector('script')).not.toBeInTheDocument();
105
+ expect(screen.getByRole('link', { name: '<script>throw new Error()</script>' })).toHaveAttribute(
106
+ 'href',
107
+ '/continue?next=%3Cscript%3E',
108
+ );
109
+ });
110
+
111
+ it.each(states)('has no email-verification affordances or network calls in %s', (_, state) => {
112
+ const fetchSpy = vi.spyOn(globalThis, 'fetch');
113
+ const xhrSpy = vi.spyOn(XMLHttpRequest.prototype, 'open');
114
+ try {
115
+ render(<PendingActivationPage {...state} recoveryLinks={recoveryLinks} onRetry={() => {}} />);
116
+ expect(
117
+ screen.queryByRole('button', {
118
+ name: /resend|change.*email|support|request access/i,
119
+ }),
120
+ ).not.toBeInTheDocument();
121
+ expect(screen.queryByRole('link', { name: /resend|change.*email|support/i })).not.toBeInTheDocument();
122
+ expect(screen.queryByText(/\d+:\d{2}/)).not.toBeInTheDocument();
123
+ expect(fetchSpy).not.toHaveBeenCalled();
124
+ expect(xhrSpy).not.toHaveBeenCalled();
125
+ } finally {
126
+ fetchSpy.mockRestore();
127
+ xhrSpy.mockRestore();
128
+ }
129
+ });
130
+
131
+ it.each([
132
+ ['default', {}],
133
+ ['loading without actions', { loading: true }],
134
+ ['loading with actions', { loading: true, actions }],
135
+ ['with reference', { reference: { label: 'Request received', value: 'REQ-2048' } }],
136
+ ['with actions', { actions }],
137
+ ['no-access', { errorState: 'no-access' }],
138
+ ['not-found', { errorState: 'not-found' }],
139
+ ['error', { errorState: 'error', onRetry: () => {} }],
140
+ ] as const)('has no non-contrast axe violations in %s across all themes', async (_, state) => {
141
+ const originalClassName = document.documentElement.className;
142
+ try {
143
+ for (const theme of ['dark', 'light', 'high_contrast']) {
144
+ document.documentElement.className = theme;
145
+ const { container, unmount } = render(<PendingActivationPage {...state} recoveryLinks={recoveryLinks} />);
146
+ try {
147
+ await expectNoAxeViolations(container);
148
+ } finally {
149
+ unmount();
150
+ }
151
+ }
152
+ } finally {
153
+ document.documentElement.className = originalClassName;
154
+ }
155
+ });
156
+
157
+ it.each(states)('keeps both exact recovery destinations in %s', (_, state) => {
158
+ render(<PendingActivationPage {...state} recoveryLinks={recoveryLinks} />);
159
+ const recovery = screen.getByRole('navigation', {
160
+ name: 'Account recovery',
161
+ });
162
+ expect(within(recovery).getAllByRole('link')).toHaveLength(2);
163
+ expect(within(recovery).getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/sign-in');
164
+ expect(within(recovery).getByRole('link', { name: 'Review sign-up' })).toHaveAttribute('href', '/sign-up');
165
+ expectTypeOf<Pick<PendingActivationPageProps, 'recoveryLinks'>>().toEqualTypeOf<{
166
+ recoveryLinks: readonly { label: string; href: string }[];
167
+ }>();
168
+ });
169
+
170
+ it.each(['no-access', 'not-found'] as const)('shows consumer %s copy without action controls', (errorState) => {
171
+ const { container } = render(
172
+ <PendingActivationPage
173
+ recoveryLinks={recoveryLinks}
174
+ errorState={errorState}
175
+ errorTitle="Cannot open this account"
176
+ errorMessage="Return to registration to continue."
177
+ actions={actions}
178
+ loading
179
+ />,
180
+ );
181
+ expect(
182
+ screen.getByRole('heading', {
183
+ level: 2,
184
+ name: 'Cannot open this account',
185
+ }),
186
+ ).toBeVisible();
187
+ expect(screen.getByText('Return to registration to continue.')).toBeVisible();
188
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
189
+ expect(screen.queryByRole('region', { name: 'Available actions' })).not.toBeInTheDocument();
190
+ if (errorState === 'no-access') expect(container.querySelector('svg.lucide-lock')).toBeInTheDocument();
191
+ });
192
+
193
+ it('omits internal error codes and demo content with default error copy', () => {
194
+ render(<PendingActivationPage recoveryLinks={recoveryLinks} errorState="error" onRetry={() => {}} />);
195
+ const error = screen.getByRole('region', {
196
+ name: 'Account status unavailable',
197
+ });
198
+ expect(error).toHaveTextContent(
199
+ /^Account status unavailableWe could not load your account status\. Try again in a moment\.Retry$/,
200
+ );
201
+ expect(within(error).getAllByRole('button')).toEqual([within(error).getByRole('button', { name: 'Retry' })]);
202
+ });
203
+
204
+ it.each(['click', 'Enter', 'Space'])('retries once using %s without demo controls or details', async (gesture) => {
205
+ const onRetry = vi.fn();
206
+ const user = userEvent.setup();
207
+ render(
208
+ <PendingActivationPage
209
+ recoveryLinks={recoveryLinks}
210
+ errorState="error"
211
+ errorTitle="Status unavailable"
212
+ errorMessage="Try loading your status again."
213
+ onRetry={onRetry}
214
+ actions={actions}
215
+ />,
216
+ );
217
+ expect(screen.getByRole('heading', { level: 2, name: 'Status unavailable' })).toBeVisible();
218
+ expect(screen.getByText('Try loading your status again.')).toBeVisible();
219
+ expect(screen.getByRole('region', { name: 'Status unavailable' })).toHaveTextContent(
220
+ /^Status unavailableTry loading your status again\.Retry$/,
221
+ );
222
+ const retry = screen.getByRole('button', { name: 'Retry' });
223
+ expect(screen.getAllByRole('button')).toEqual([retry]);
224
+ expect(screen.queryByText(/req_8f2c41d07a9b|503|GET \/v1/)).not.toBeInTheDocument();
225
+ if (gesture === 'click') await user.click(retry);
226
+ else {
227
+ retry.focus();
228
+ await user.keyboard(gesture === 'Enter' ? '{Enter}' : ' ');
229
+ }
230
+ expect(onRetry).toHaveBeenCalledOnce();
231
+ });
232
+
233
+ it.each([[[]], [actions]])('shows busy skeletons and no controls while loading %j', (suppliedActions) => {
234
+ render(<PendingActivationPage recoveryLinks={recoveryLinks} loading actions={suppliedActions} />);
235
+ const region = screen.getByRole('region', { name: 'Available actions' });
236
+ expect(region).toHaveAttribute('aria-busy', 'true');
237
+ expect(region.querySelectorAll('[aria-hidden="true"]').length).toBeGreaterThan(0);
238
+ expect(within(region).queryByRole('button')).not.toBeInTheDocument();
239
+ expect(within(region).queryByRole('link')).not.toBeInTheDocument();
240
+ for (const placeholder of region.querySelectorAll('[aria-hidden="true"]')) {
241
+ expect(placeholder).toHaveClass('motion-reduce:animate-none');
242
+ }
243
+ });
244
+
245
+ it('renders supplied links and buttons in order, calls buttons, and omits an empty region', async () => {
246
+ onAction.mockClear();
247
+ const user = userEvent.setup();
248
+ const { rerender } = render(<PendingActivationPage recoveryLinks={recoveryLinks} actions={actions} />);
249
+ const region = screen.getByRole('region', { name: 'Available actions' });
250
+ expect([...region.querySelectorAll('a,button')].map((node) => node.textContent)).toEqual([
251
+ 'Review request',
252
+ 'Check status',
253
+ 'Account details',
254
+ ]);
255
+ expect(within(region).getByRole('link', { name: 'Review request' })).toHaveAttribute(
256
+ 'href',
257
+ '/requests/2048?from=pending',
258
+ );
259
+ expect(within(region).getByRole('link', { name: 'Account details' })).toHaveAttribute('href', '/account');
260
+ await user.click(within(region).getByRole('button', { name: 'Check status' }));
261
+ expect(onAction).toHaveBeenCalledOnce();
262
+ rerender(<PendingActivationPage recoveryLinks={recoveryLinks} actions={[]} />);
263
+ expect(screen.queryByRole('region', { name: 'Available actions' })).not.toBeInTheDocument();
264
+ });
265
+
266
+ it('removes stale actions during refresh and hides the region when none remain', () => {
267
+ const { rerender } = render(<PendingActivationPage recoveryLinks={recoveryLinks} actions={actions} />);
268
+ expect(screen.getByRole('link', { name: 'Review request' })).toBeVisible();
269
+
270
+ rerender(<PendingActivationPage recoveryLinks={recoveryLinks} actions={actions} loading />);
271
+ const busyRegion = screen.getByRole('region', {
272
+ name: 'Available actions',
273
+ });
274
+ expect(busyRegion).toHaveAttribute('aria-busy', 'true');
275
+ expect(within(busyRegion).queryByRole('link')).not.toBeInTheDocument();
276
+ expect(within(busyRegion).queryByRole('button')).not.toBeInTheDocument();
277
+
278
+ // An empty action response must keep placeholders until loading finishes.
279
+ rerender(<PendingActivationPage recoveryLinks={recoveryLinks} actions={[]} loading />);
280
+ expect(screen.getByRole('region', { name: 'Available actions' })).toHaveAttribute('aria-busy', 'true');
281
+
282
+ rerender(<PendingActivationPage recoveryLinks={recoveryLinks} actions={[]} />);
283
+ expect(screen.queryByRole('region', { name: 'Available actions' })).not.toBeInTheDocument();
284
+ expect(screen.getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/sign-in');
285
+ expect(screen.getByRole('link', { name: 'Review sign-up' })).toHaveAttribute('href', '/sign-up');
286
+ });
287
+
288
+ it('shows an optional reference after status and removes it when absent', () => {
289
+ const { rerender } = render(
290
+ <PendingActivationPage
291
+ recoveryLinks={recoveryLinks}
292
+ reference={{ label: 'Request received', value: 'REQ-2048' }}
293
+ />,
294
+ );
295
+ const reference = screen.getByRole('status', { name: 'Request received' });
296
+ expect(reference).toHaveAccessibleDescription('REQ-2048');
297
+ expect(screen.getAllByRole('status')[1]).toBe(reference);
298
+ rerender(<PendingActivationPage recoveryLinks={recoveryLinks} />);
299
+ expect(screen.queryByRole('status', { name: 'Request received' })).not.toBeInTheDocument();
300
+ expect(screen.getAllByRole('status')).toHaveLength(1);
301
+ });
302
+
303
+ it.each(['no-access', 'not-found', 'error'] as const)(
304
+ 'retains status, reference and recovery when entering and leaving %s',
305
+ (errorState) => {
306
+ const props = {
307
+ recoveryLinks,
308
+ actions,
309
+ title: 'Awaiting review',
310
+ intro: 'Your registration is complete.',
311
+ statusTitle: 'Review pending',
312
+ statusMessage: 'We will notify you after approval.',
313
+ reference: { label: 'Request received', value: 'REQ-2048' },
314
+ onRetry: () => {},
315
+ };
316
+ const { rerender } = render(<PendingActivationPage {...props} />);
317
+
318
+ rerender(
319
+ <PendingActivationPage
320
+ {...props}
321
+ errorState={errorState}
322
+ errorTitle="Cannot load account status"
323
+ errorMessage="Return to sign in or try again."
324
+ loading
325
+ />,
326
+ );
327
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
328
+ expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Awaiting review');
329
+ expect(screen.getByText('Your registration is complete.')).toBeVisible();
330
+ expect(screen.getByRole('status', { name: 'Review pending' })).toHaveAccessibleDescription(
331
+ 'We will notify you after approval.',
332
+ );
333
+ expect(screen.getByRole('status', { name: 'Request received' })).toHaveAccessibleDescription('REQ-2048');
334
+ expect(screen.queryByRole('region', { name: 'Available actions' })).not.toBeInTheDocument();
335
+ expect(screen.queryAllByRole('button')).toEqual(
336
+ errorState === 'error' ? [screen.getByRole('button', { name: 'Retry' })] : [],
337
+ );
338
+ expect(screen.getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/sign-in');
339
+ expect(screen.getByRole('link', { name: 'Review sign-up' })).toHaveAttribute('href', '/sign-up');
340
+
341
+ rerender(<PendingActivationPage {...props} />);
342
+ expect(screen.queryByRole('heading', { level: 2 })).not.toBeInTheDocument();
343
+ expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument();
344
+ expect(screen.getByRole('link', { name: 'Review request' })).toBeVisible();
345
+ expect(screen.getByRole('button', { name: 'Check status' })).toBeVisible();
346
+ expect(screen.getByRole('link', { name: 'Account details' })).toBeVisible();
347
+ },
348
+ );
349
+
350
+ it('renders a meaningful default preview', () => {
351
+ render(<PendingActivationPage />);
352
+ expect(
353
+ screen.getByRole('heading', {
354
+ level: 1,
355
+ name: 'Your account is awaiting activation',
356
+ }),
357
+ ).toBeVisible();
358
+ expect(screen.getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/sign-in');
359
+ });
360
+
361
+ it.each(states)('keeps one consumer h1 and intro in %s', (_, state) => {
362
+ render(
363
+ <PendingActivationPage
364
+ {...state}
365
+ recoveryLinks={recoveryLinks}
366
+ title="Awaiting review"
367
+ intro="Your registration is complete."
368
+ />,
369
+ );
370
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
371
+ expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Awaiting review');
372
+ expect(screen.getByText('Your registration is complete.')).toBeVisible();
373
+ });
374
+
375
+ it.each(['info', 'warning', undefined] as const)('announces status politely with tone %s', (statusTone) => {
376
+ render(
377
+ <PendingActivationPage
378
+ recoveryLinks={recoveryLinks}
379
+ statusTitle="Review pending"
380
+ statusMessage="We will notify you after approval."
381
+ statusTone={statusTone}
382
+ />,
383
+ );
384
+ const status = screen.getByRole('status', { name: 'Review pending' });
385
+ expect(status).toHaveAccessibleDescription('We will notify you after approval.');
386
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
387
+ expect(within(status).queryByRole('button')).not.toBeInTheDocument();
388
+ expectTypeOf<PendingActivationPageProps['statusTone']>().toEqualTypeOf<'info' | 'warning' | undefined>();
389
+ });
390
+ });
@@ -0,0 +1,145 @@
1
+ import { AuthHeader, ErrorPanel, InlineCallout } from '@gntik-ai/blocks';
2
+ import { Lock, SearchX } from '@gntik-ai/icons';
3
+ import { AuthLayout, Button, EmptyState, Link, Skeleton, Stack } from '@gntik-ai/ui';
4
+ import { pendingActivationSample } from './data';
5
+
6
+ /** Navigation targets are passed directly to Link; validation belongs to the consumer. */
7
+ export type PendingActivationLink = { label: string; href: string };
8
+
9
+ /** An allowed navigation link or consumer callback. */
10
+ export type PendingActivationAction =
11
+ (PendingActivationLink & { onClick?: never }) | { label: string; href?: never; onClick: () => void };
12
+
13
+ export interface PendingActivationPageProps {
14
+ /** The only page-level heading, also retained in recovery states. */
15
+ title?: string;
16
+ /** Description directly below the page heading. */
17
+ intro?: string;
18
+ /** Title announced by the polite status callout, retained in recovery states; match copy to the current state. */
19
+ statusTitle?: string;
20
+ /** Current approval or activation status, supplied by the consumer. */
21
+ statusMessage?: string;
22
+ /** Informational or warning presentation; defaults to info, never assertive. */
23
+ statusTone?: 'info' | 'warning';
24
+ /** Optional request identifier; absent references render no callout. */
25
+ reference?: { label: string; value: string };
26
+ /** Allowed navigation links or callback buttons, rendered in input order. */
27
+ actions?: readonly PendingActivationAction[];
28
+ /** Accessible name of the actions region, including while loading. */
29
+ actionsLabel?: string;
30
+ /** Loading takes precedence over the supplied actions. */
31
+ loading?: boolean;
32
+ /** Recovery variant; omitted for the normal pending state. */
33
+ errorState?: 'no-access' | 'not-found' | 'error';
34
+ /** Recovery heading below the h1; defaults to the selected state's sample copy. */
35
+ errorTitle?: string;
36
+ /** Consumer explanation of the failure and how to recover. */
37
+ errorMessage?: string;
38
+ /** Retry handler for the error variant; promises show the kit's retry loading state. */
39
+ onRetry?: () => void | Promise<void>;
40
+ /** Text on the error variant's retry button. */
41
+ retryLabel?: string;
42
+ /** Consumer navigation, retained in every state. Required when configuring the page. */
43
+ recoveryLinks: readonly PendingActivationLink[];
44
+ /** Accessible name of the recovery navigation. */
45
+ recoveryLabel?: string;
46
+ /** AuthLayout skip link text. */
47
+ skipLinkLabel?: string;
48
+ }
49
+
50
+ /** Presentational account approval step; render without props for the catalogue preview. */
51
+ export default function PendingActivationPage({
52
+ title = pendingActivationSample.title,
53
+ intro = pendingActivationSample.intro,
54
+ statusTitle = pendingActivationSample.statusTitle,
55
+ statusMessage = pendingActivationSample.statusMessage,
56
+ statusTone = 'info',
57
+ reference,
58
+ actions = [],
59
+ actionsLabel = 'Available actions',
60
+ loading = false,
61
+ errorState,
62
+ errorTitle,
63
+ errorMessage,
64
+ onRetry,
65
+ retryLabel = 'Retry',
66
+ recoveryLinks = pendingActivationSample.recoveryLinks,
67
+ recoveryLabel = 'Account recovery',
68
+ skipLinkLabel = 'Skip to account status',
69
+ }: PendingActivationPageProps | Record<string, never> = {}) {
70
+ const recovery = errorState ? pendingActivationSample.recovery[errorState] : undefined;
71
+ return (
72
+ <AuthLayout fullScreen variant="card" skipLinkLabel={skipLinkLabel}>
73
+ <AuthHeader headingLevel="h1" title={title} description={intro} />
74
+ <Stack gap={4}>
75
+ <InlineCallout
76
+ tone={statusTone}
77
+ title={statusTitle}
78
+ description={statusMessage}
79
+ actions={[]}
80
+ dismissible={false}
81
+ />
82
+ {reference && (
83
+ <InlineCallout
84
+ tone="info"
85
+ title={reference.label}
86
+ description={reference.value}
87
+ actions={[]}
88
+ dismissible={false}
89
+ />
90
+ )}
91
+ {errorState === 'error' ? (
92
+ <ErrorPanel
93
+ titleAs="h2"
94
+ title={errorTitle ?? recovery?.title}
95
+ message={errorMessage ?? recovery?.message}
96
+ code={null}
97
+ requestId=""
98
+ details=""
99
+ secondaryAction={null}
100
+ onRetry={onRetry}
101
+ retryLabel={retryLabel}
102
+ />
103
+ ) : errorState ? (
104
+ <EmptyState
105
+ titleAs="h2"
106
+ icon={errorState === 'no-access' ? Lock : SearchX}
107
+ bordered
108
+ className="mx-auto"
109
+ title={errorTitle ?? recovery?.title}
110
+ description={errorMessage ?? recovery?.message}
111
+ />
112
+ ) : (
113
+ (loading || actions.length > 0) && (
114
+ <section aria-label={actionsLabel} aria-busy={loading}>
115
+ <Stack gap={2}>
116
+ {loading
117
+ ? Array.from({ length: Math.max(2, actions.length) }, (_, index) => (
118
+ <Skeleton key={index} className="h-10 w-full" />
119
+ ))
120
+ : actions.map((action, index) =>
121
+ action.href !== undefined ? (
122
+ <Link key={index} href={action.href}>
123
+ {action.label}
124
+ </Link>
125
+ ) : (
126
+ <Button key={index} variant="secondary" onClick={action.onClick}>
127
+ {action.label}
128
+ </Button>
129
+ ),
130
+ )}
131
+ </Stack>
132
+ </section>
133
+ )
134
+ )}
135
+ <nav aria-label={recoveryLabel} className="flex flex-wrap justify-center gap-4">
136
+ {recoveryLinks.map((link, index) => (
137
+ <Link key={index} href={link.href}>
138
+ {link.label}
139
+ </Link>
140
+ ))}
141
+ </nav>
142
+ </Stack>
143
+ </AuthLayout>
144
+ );
145
+ }