@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,182 @@
1
+ import { ThemeProvider } from '@gntik-ai/ui';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { createRef } from 'react';
5
+ import type { SignInFormRef } from '@gntik-ai/blocks';
6
+ import { expectNoAxeViolations } from '../test/a11y';
7
+ import SignInCardPage from './Page';
8
+
9
+ describe('SignInCardPage', () => {
10
+ it('suppresses every internal heading beside a page heading and hides or replaces optional regions', () => {
11
+ const { container, rerender } = render(
12
+ <>
13
+ <h1>Account access</h1>
14
+ <SignInCardPage suppressAllHeadings footer={null} signUpPrompt={null} sso={null} />
15
+ </>,
16
+ );
17
+ expect(container.querySelectorAll('h1')).toHaveLength(1);
18
+ expect(screen.getByRole('main').querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]')).toBeNull();
19
+ expect(screen.getByRole('form', { name: 'Sign in' })).toBeInTheDocument();
20
+ expect(screen.queryByRole('link', { name: 'Terms' })).not.toBeInTheDocument();
21
+ expect(screen.queryByRole('link', { name: 'Privacy Policy' })).not.toBeInTheDocument();
22
+ expect(screen.queryByRole('link', { name: 'Create an account' })).not.toBeInTheDocument();
23
+ expect(screen.queryByRole('group', { name: 'Single sign-on' })).not.toBeInTheDocument();
24
+ rerender(<SignInCardPage headingLevel="h2" footer="Account support" />);
25
+ expect(screen.getByRole('heading', { level: 2, name: 'Sign in' })).toBeInTheDocument();
26
+ expect(screen.getByText('Account support')).toBeInTheDocument();
27
+ rerender(<SignInCardPage headingLevel={null} aria-label="Account access" />);
28
+ expect(screen.queryByRole('heading')).not.toBeInTheDocument();
29
+ expect(screen.getByRole('form', { name: 'Account access' })).toBeInTheDocument();
30
+ });
31
+
32
+ it('forwards identifier, validation, field help, focus and server ARIA props unchanged', async () => {
33
+ const onSubmit = vi.fn();
34
+ const ref = createRef<SignInFormRef>();
35
+ const props = {
36
+ ref,
37
+ onSubmit,
38
+ identifier: {
39
+ type: 'text' as const,
40
+ autoComplete: 'username',
41
+ label: 'Username',
42
+ help: 'Account name',
43
+ helpId: 'username-help',
44
+ minLength: 3,
45
+ maxLength: 120,
46
+ },
47
+ passwordHelp: 'Account password',
48
+ passwordHelpId: 'password-help',
49
+ messages: {
50
+ identifierRequired: 'Username is required.',
51
+ identifierInvalid: 'Use three characters.',
52
+ passwordRequired: 'Password is required.',
53
+ },
54
+ 'aria-describedby': 'form-feedback',
55
+ };
56
+ const { rerender } = render(<SignInCardPage {...props} />);
57
+ const input = screen.getByLabelText('Username');
58
+ const password = screen.getByLabelText('Password');
59
+ expect(input).toHaveAttribute('type', 'text');
60
+ expect(input).toHaveAttribute('autocomplete', 'username');
61
+ expect(input).toHaveAttribute('minlength', '3');
62
+ expect(input).toHaveAttribute('maxlength', '120');
63
+ expect(screen.getByRole('form', { name: 'Sign in' })).toHaveAttribute('novalidate');
64
+ await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
65
+ expect(input).toHaveFocus();
66
+ expect(onSubmit).not.toHaveBeenCalled();
67
+ for (const [field, message, helpId] of [
68
+ [input, 'Username is required.', 'username-help'],
69
+ [password, 'Password is required.', 'password-help'],
70
+ ] as const) {
71
+ const error = screen.getByText(message).closest('[role="alert"]');
72
+ expect(field).toHaveAttribute('aria-invalid', 'true');
73
+ expect(field.getAttribute('aria-describedby')?.split(' ')).toEqual(expect.arrayContaining([helpId, error?.id]));
74
+ }
75
+ await userEvent.type(input, 'ab');
76
+ expect(screen.getByText('Use three characters.')).toBeInTheDocument();
77
+ await userEvent.type(input, 'c');
78
+ await userEvent.type(password, 'secret');
79
+ expect(input).not.toHaveAttribute('aria-invalid');
80
+ expect(password).not.toHaveAttribute('aria-invalid');
81
+ rerender(
82
+ <SignInCardPage
83
+ {...props}
84
+ fieldInvalid={{ identifier: true, password: true }}
85
+ feedbackId="credential-feedback"
86
+ error="Credentials were not accepted."
87
+ />,
88
+ );
89
+ for (const [field, helpId] of [
90
+ [input, 'username-help'],
91
+ [password, 'password-help'],
92
+ ] as const) {
93
+ expect(field).toHaveAttribute('aria-invalid', 'true');
94
+ expect(field.getAttribute('aria-describedby')?.split(' ')).toEqual(
95
+ expect.arrayContaining([helpId, 'credential-feedback']),
96
+ );
97
+ }
98
+ expect(screen.getByRole('form', { name: 'Sign in' })).toHaveAttribute('aria-describedby', 'form-feedback');
99
+ input.focus();
100
+ ref.current?.focus('password');
101
+ expect(password).toHaveFocus();
102
+ });
103
+
104
+ it('renders the card form without SSO', async () => {
105
+ const { container } = render(<SignInCardPage />);
106
+ expect(screen.getByRole('main')).toBeInTheDocument();
107
+ expect(screen.getByRole('heading', { level: 1, name: 'Sign in' })).toBeInTheDocument();
108
+ expect(screen.queryByRole('group', { name: 'Single sign-on' })).not.toBeInTheDocument();
109
+ expect(screen.getByRole('link', { name: 'Privacy Policy' })).toBeInTheDocument();
110
+ await expectNoAxeViolations(container);
111
+ });
112
+
113
+ it('shows a server error and runs forgot password', async () => {
114
+ const onForgotPassword = vi.fn();
115
+ render(<SignInCardPage error="Incorrect email or password." onForgotPassword={onForgotPassword} />);
116
+ expect(screen.getByText('Incorrect email or password.')).toBeInTheDocument();
117
+ await userEvent.click(screen.getByRole('link', { name: 'Forgot password?' }));
118
+ expect(onForgotPassword).toHaveBeenCalledOnce();
119
+ });
120
+
121
+ it('takes the heading copy from props', () => {
122
+ render(<SignInCardPage title="Welcome back to the console" description="Sign in with your tenant account." />);
123
+ expect(screen.getByRole('heading', { level: 1, name: 'Welcome back to the console' })).toBeInTheDocument();
124
+ expect(screen.getByText('Sign in with your tenant account.')).toBeInTheDocument();
125
+ });
126
+ });
127
+
128
+ // Public rendering seams: identifier mode, theme, heading policy and validation/recovery state.
129
+ describe('SignInCardPage accessibility matrix', () => {
130
+ for (const theme of ['dark', 'light', 'high_contrast'] as const) {
131
+ for (const mode of ['email', 'username'] as const) {
132
+ for (const suppressed of [false, true]) {
133
+ for (const state of ['initial', 'errors'] as const) {
134
+ it(`${theme} / ${mode} / headings ${!suppressed} / ${state}`, async () => {
135
+ const onSubmit = vi.fn();
136
+ const { container } = render(
137
+ <ThemeProvider defaultMode={theme} storageKey={null}>
138
+ <h1>Account access</h1>
139
+ <SignInCardPage
140
+ identifier={
141
+ mode === 'username'
142
+ ? {
143
+ type: 'text',
144
+ autoComplete: 'username',
145
+ label: 'Username',
146
+ help: 'Use your account name.',
147
+ minLength: 3,
148
+ maxLength: 120,
149
+ }
150
+ : undefined
151
+ }
152
+ suppressAllHeadings={suppressed}
153
+ messages={{
154
+ identifierRequired: 'Account name is required.',
155
+ passwordRequired: 'Password is required.',
156
+ }}
157
+ onSubmit={onSubmit}
158
+ />
159
+ </ThemeProvider>,
160
+ );
161
+ if (state === 'initial' || state === 'errors') {
162
+ const input = screen.getByRole('textbox', { name: mode === 'username' ? 'Username' : 'Email' });
163
+ expect(input).toHaveAttribute('type', mode === 'username' ? 'text' : 'email');
164
+ expect(input).toHaveAttribute('autocomplete', mode === 'username' ? 'username' : 'email');
165
+ expect(screen.getByRole('form', { name: 'Sign in' })).toHaveAttribute('novalidate');
166
+ if (state === 'errors') {
167
+ await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
168
+ expect(input).toHaveFocus();
169
+ expect(input).toHaveAttribute('aria-invalid', 'true');
170
+ expect(onSubmit).not.toHaveBeenCalled();
171
+ expect(screen.getByText('Account name is required.')).toBeInTheDocument();
172
+ expect(screen.getByLabelText('Password')).toHaveAttribute('aria-invalid', 'true');
173
+ }
174
+ }
175
+ if (suppressed) expect(container.querySelectorAll('h1,h2,h3,h4,h5,h6,[role="heading"]')).toHaveLength(1);
176
+ await expectNoAxeViolations(container);
177
+ });
178
+ }
179
+ }
180
+ }
181
+ }
182
+ });
@@ -0,0 +1,82 @@
1
+ import { SignInForm, type SignInValues, type SignInFormProps } from '@gntik-ai/blocks';
2
+ import { AuthLayout, Link } from '@gntik-ai/ui';
3
+ import type { ReactNode } from 'react';
4
+ import { signInCardCopy } from './data';
5
+
6
+ export interface SignInCardPageProps extends SignInFormProps {
7
+ onSubmit: (values: SignInValues) => void | Promise<void>;
8
+ error: string | null;
9
+ defaultEmail: string;
10
+ forgotPasswordHref: string;
11
+ onForgotPassword: () => void;
12
+ signUpHref: string;
13
+ onSignUp: () => void;
14
+ /** Optional SSO area under the form; hidden by default. */
15
+ sso: ReactNode;
16
+ eyebrow: ReactNode;
17
+ title: ReactNode;
18
+ description: ReactNode;
19
+ termsHref: string;
20
+ privacyHref: string;
21
+ logo: ReactNode;
22
+ /** Replaces the legal footer; `null` hides it. */
23
+ footer?: ReactNode;
24
+ /** Suppresses all internal headings, including the embedded form heading. */
25
+ suppressAllHeadings?: boolean;
26
+ }
27
+
28
+ /** Card sign-in: AuthLayout `card` + SignInForm without SSO. */
29
+ export default function SignInCardPage({
30
+ onSubmit,
31
+ error,
32
+ defaultEmail,
33
+ forgotPasswordHref,
34
+ onForgotPassword,
35
+ signUpHref,
36
+ onSignUp,
37
+ sso = null,
38
+ eyebrow = signInCardCopy.eyebrow,
39
+ title = signInCardCopy.title,
40
+ description = signInCardCopy.description,
41
+ termsHref = signInCardCopy.termsHref,
42
+ privacyHref = signInCardCopy.privacyHref,
43
+ logo,
44
+ footer,
45
+ suppressAllHeadings = false,
46
+ headingLevel,
47
+ ...formProps
48
+ }: Partial<SignInCardPageProps>) {
49
+ return (
50
+ <AuthLayout
51
+ fullScreen
52
+ variant="card"
53
+ logo={logo}
54
+ footer={
55
+ footer === undefined ? (
56
+ <>
57
+ By signing in you agree to the <Link href={termsHref}>Terms</Link> and{' '}
58
+ <Link href={privacyHref}>Privacy Policy</Link>.
59
+ </>
60
+ ) : (
61
+ footer
62
+ )
63
+ }
64
+ >
65
+ <SignInForm
66
+ {...formProps}
67
+ headingLevel={suppressAllHeadings ? null : headingLevel}
68
+ onSubmit={onSubmit}
69
+ error={error}
70
+ defaultEmail={defaultEmail}
71
+ forgotPasswordHref={forgotPasswordHref}
72
+ onForgotPassword={onForgotPassword}
73
+ signUpHref={signUpHref}
74
+ onSignUp={onSignUp}
75
+ sso={sso}
76
+ eyebrow={eyebrow}
77
+ title={title}
78
+ description={description}
79
+ />
80
+ </AuthLayout>
81
+ );
82
+ }
@@ -0,0 +1,8 @@
1
+ /** Copy for the card sign-in (no SSO, legal footer). */
2
+ export const signInCardCopy = {
3
+ eyebrow: 'Welcome back',
4
+ title: 'Sign in',
5
+ description: 'Use the email and password for your account.',
6
+ termsHref: '#terms',
7
+ privacyHref: '#privacy',
8
+ };
@@ -0,0 +1,28 @@
1
+ import SignInCardPage, { type SignInCardPageProps } from '../Page';
2
+
3
+ /** /preview.html?kind=template&id=sign-in-card--username */
4
+ export default function Username(props: Pick<SignInCardPageProps, 'headingLevel' | 'suppressAllHeadings'>) {
5
+ return (
6
+ <SignInCardPage
7
+ {...props}
8
+ identifier={{
9
+ type: 'text',
10
+ autoComplete: 'username',
11
+ label: 'Username',
12
+ help: 'Use your account name.',
13
+ placeholder: 'Account name',
14
+ minLength: 3,
15
+ maxLength: 120,
16
+ }}
17
+ messages={{
18
+ identifierRequired: 'Enter your username.',
19
+ identifierInvalid: 'Use at least three characters.',
20
+ passwordRequired: 'Enter your password.',
21
+ }}
22
+ description="Use your account name to continue."
23
+ footer={null}
24
+ signUpPrompt={null}
25
+ sso={null}
26
+ />
27
+ );
28
+ }
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Sign in (card)',
5
+ family: 'Auth',
6
+ priority: 'P1',
7
+ status: 'stable',
8
+ description:
9
+ 'Centred card sign-in with default email and password, legal footer and no SSO. Supports identifier (type, autoComplete, label, help/helpId, placeholder, minLength/maxLength), messages (identifierRequired/identifierInvalid/identifierTooShort/passwordRequired), and React 19 ref.focus. The kit owns inline validation and focuses the first invalid field; consumers supply messages, not a second inline validator. Server errors use error + fieldInvalid + feedbackId; aria-describedby describes the form, while flagged inputs also retain their help and error ids. headingLevel (h1/h2/h3/null) suppresses the heading without losing the form name; aria-label/aria-labelledby override the default name. Forwards SignInForm props unchanged, including passwordHelp/passwordHelpId and ref.focus(identifier/password). Text submissions retain email and add identifier. suppressAllHeadings suppresses every internal heading. footer, sso and signUpPrompt accept replacements or null. Preview: sign-in-card--username.',
10
+ layout: 'AuthLayout',
11
+ blocks: ['sign-in-form'],
12
+ };
@@ -0,0 +1,33 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import SignUpPage from './Page';
5
+
6
+ describe('SignUpPage', () => {
7
+ it('renders the form, SSO options and brand panel', async () => {
8
+ const { container } = render(<SignUpPage />);
9
+ expect(screen.getByRole('main')).toBeInTheDocument();
10
+ expect(screen.getByRole('complementary')).toBeInTheDocument();
11
+ expect(screen.getByRole('heading', { level: 1, name: 'Create your account' })).toBeInTheDocument();
12
+ expect(screen.getByRole('group', { name: 'Single sign-on' })).toBeInTheDocument();
13
+ await expectNoAxeViolations(container);
14
+ });
15
+
16
+ it('requires the terms, then creates the account', async () => {
17
+ const onSubmit = vi.fn();
18
+ render(<SignUpPage onSubmit={onSubmit} />);
19
+ await userEvent.type(screen.getByLabelText('Full name'), 'Avery Collins');
20
+ await userEvent.type(screen.getByLabelText('Work email'), 'avery@example.com');
21
+ await userEvent.type(screen.getByLabelText('Password'), 'long-enough-pass');
22
+ await userEvent.click(screen.getByRole('button', { name: 'Create account' }));
23
+ expect(screen.getByText('Accept the terms to continue.')).toBeInTheDocument();
24
+ expect(onSubmit).not.toHaveBeenCalled();
25
+ await userEvent.click(screen.getByRole('checkbox'));
26
+ await userEvent.click(screen.getByRole('button', { name: 'Create account' }));
27
+ expect(onSubmit).toHaveBeenCalledWith({
28
+ name: 'Avery Collins',
29
+ email: 'avery@example.com',
30
+ password: 'long-enough-pass',
31
+ });
32
+ });
33
+ });
@@ -0,0 +1,70 @@
1
+ import { SignUpForm, SsoButtons, type SignUpValues, type SsoProvider } from '@gntik-ai/blocks';
2
+ import { AuthLayout } from '@gntik-ai/ui';
3
+ import { useState, type ReactNode } from 'react';
4
+ import { BrandPanel, type BrandPanelCopy } from '../sign-in/BrandPanel';
5
+ import { signUpBrand, signUpSsoProviders } from './data';
6
+
7
+ export interface SignUpPageProps {
8
+ /** Creates the account. A returned promise shows the loading state; a rejection shows in the alert. */
9
+ onSubmit: (values: SignUpValues) => void | Promise<void>;
10
+ /** Starts single sign-on with a provider id. */
11
+ onSso: (providerId: string) => void;
12
+ ssoProviders: SsoProvider[];
13
+ error: string | null;
14
+ minPasswordLength: number;
15
+ termsHref: string;
16
+ privacyHref: string;
17
+ signInHref: string;
18
+ onSignIn: () => void;
19
+ brand: BrandPanelCopy;
20
+ brandFooter: ReactNode;
21
+ logo: ReactNode;
22
+ }
23
+
24
+ /** Split sign-up: AuthLayout brand panel + SignUpForm with SsoButtons. */
25
+ export default function SignUpPage({
26
+ onSubmit,
27
+ onSso,
28
+ ssoProviders = signUpSsoProviders,
29
+ error,
30
+ minPasswordLength,
31
+ termsHref,
32
+ privacyHref,
33
+ signInHref,
34
+ onSignIn,
35
+ brand = signUpBrand,
36
+ brandFooter = signUpBrand.footer,
37
+ logo,
38
+ }: Partial<SignUpPageProps>) {
39
+ const [ssoLoading, setSsoLoading] = useState<string | null>(null);
40
+ return (
41
+ <AuthLayout
42
+ fullScreen
43
+ variant="split"
44
+ logo={logo}
45
+ brand={<BrandPanel {...brand} />}
46
+ brandFooter={brandFooter}
47
+ skipLinkLabel="Skip to sign-up form"
48
+ >
49
+ <SignUpForm
50
+ onSubmit={onSubmit}
51
+ error={error}
52
+ minPasswordLength={minPasswordLength}
53
+ termsHref={termsHref}
54
+ privacyHref={privacyHref}
55
+ signInHref={signInHref}
56
+ onSignIn={onSignIn}
57
+ sso={
58
+ <SsoButtons
59
+ providers={ssoProviders}
60
+ loadingId={ssoLoading}
61
+ onSelect={(id) => {
62
+ setSsoLoading(id);
63
+ onSso?.(id);
64
+ }}
65
+ />
66
+ }
67
+ />
68
+ </AuthLayout>
69
+ );
70
+ }
@@ -0,0 +1,15 @@
1
+ import { ssoProviders } from '@gntik-ai/blocks';
2
+
3
+ export { ssoProviders as signUpSsoProviders };
4
+
5
+ /** Brand panel copy for the split layout. */
6
+ export const signUpBrand = {
7
+ eyebrow: 'Free for small teams',
8
+ headline: 'Your whole team, one workspace.',
9
+ features: [
10
+ 'Invite teammates with roles from day one',
11
+ 'Projects, environments and deployments together',
12
+ 'Usage-based plans, no card to start',
13
+ ],
14
+ footer: 'SOC 2 · encrypted at rest · audited access',
15
+ };
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Sign up',
5
+ family: 'Auth',
6
+ priority: 'P1',
7
+ status: 'stable',
8
+ description:
9
+ 'Split sign-up: brand panel with the value pitch, name, work email and password with a strength meter, terms consent and single sign-on.',
10
+ layout: 'AuthLayout',
11
+ blocks: ['sign-up-form', 'sso-buttons'],
12
+ };
@@ -0,0 +1,41 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import SsoLoginPage from './Page';
5
+
6
+ describe('SsoLoginPage', () => {
7
+ it('renders the work email step', async () => {
8
+ const { container } = render(<SsoLoginPage />);
9
+ expect(screen.getByRole('main')).toBeInTheDocument();
10
+ expect(screen.getByRole('heading', { level: 1, name: 'Sign in with SSO' })).toBeInTheDocument();
11
+ await expectNoAxeViolations(container);
12
+ });
13
+
14
+ it('looks up the email and shows the redirecting state', async () => {
15
+ const onSubmit = vi.fn();
16
+ const onCancel = vi.fn();
17
+ const { container } = render(<SsoLoginPage onSubmit={onSubmit} onCancel={onCancel} providerName="Acme Identity" />);
18
+ await userEvent.click(screen.getByRole('button', { name: 'Continue with SSO' }));
19
+ expect(screen.getByText('Enter a valid work email.')).toBeInTheDocument();
20
+ await userEvent.type(screen.getByLabelText('Work email'), 'dana@acme.example');
21
+ await userEvent.click(screen.getByRole('button', { name: 'Continue with SSO' }));
22
+ expect(onSubmit).toHaveBeenCalledWith('dana@acme.example');
23
+ expect(await screen.findByRole('heading', { level: 1, name: 'Redirecting to Acme Identity' })).toBeInTheDocument();
24
+ expect(screen.getByRole('status')).toHaveTextContent('Waiting for Acme Identity');
25
+ await expectNoAxeViolations(container);
26
+ await userEvent.click(screen.getByRole('button', { name: 'Use a different email' }));
27
+ expect(onCancel).toHaveBeenCalledOnce();
28
+ expect(screen.getByRole('heading', { level: 1, name: 'Sign in with SSO' })).toBeInTheDocument();
29
+ });
30
+
31
+ it('shows a lookup failure', async () => {
32
+ render(
33
+ <SsoLoginPage
34
+ defaultEmail="dana@unknown.example"
35
+ onSubmit={() => Promise.reject(new Error('No SSO connection for this domain.'))}
36
+ />,
37
+ );
38
+ await userEvent.click(screen.getByRole('button', { name: 'Continue with SSO' }));
39
+ expect(await screen.findByText('No SSO connection for this domain.')).toBeInTheDocument();
40
+ });
41
+ });
@@ -0,0 +1,151 @@
1
+ import { AuthHeader } from '@gntik-ai/blocks';
2
+ import { Building2 } from '@gntik-ai/icons';
3
+ import {
4
+ Alert,
5
+ AuthLayout,
6
+ Button,
7
+ Field,
8
+ FieldDescription,
9
+ FieldError,
10
+ FieldLabel,
11
+ Input,
12
+ Link,
13
+ Spinner,
14
+ Stack,
15
+ Text,
16
+ } from '@gntik-ai/ui';
17
+ import { useState, type ReactNode } from 'react';
18
+ import { ssoEmailPattern, ssoLoginSample } from './data';
19
+
20
+ export interface SsoLoginPageProps {
21
+ /**
22
+ * Looks up the domain's identity provider and starts the redirect. A rejection's message shows
23
+ * in the alert (e.g. "No SSO connection for this domain").
24
+ */
25
+ onSubmit: (email: string) => void | Promise<void>;
26
+ /** Leaves the redirecting state. */
27
+ onCancel: () => void;
28
+ defaultEmail: string;
29
+ /** Name shown while redirecting. */
30
+ providerName: string;
31
+ signInHref: string;
32
+ error: string | null;
33
+ logo: ReactNode;
34
+ }
35
+
36
+ /** Card SSO entry: work email → "Redirecting to your identity provider". */
37
+ export default function SsoLoginPage({
38
+ onSubmit,
39
+ onCancel,
40
+ defaultEmail = ssoLoginSample.defaultEmail,
41
+ providerName = ssoLoginSample.providerName,
42
+ signInHref = ssoLoginSample.signInHref,
43
+ error: errorProp = null,
44
+ logo,
45
+ }: Partial<SsoLoginPageProps>) {
46
+ const [email, setEmail] = useState(defaultEmail);
47
+ const [submitted, setSubmitted] = useState(false);
48
+ const [loading, setLoading] = useState(false);
49
+ const [redirecting, setRedirecting] = useState(false);
50
+ const [error, setError] = useState<string | null>(null);
51
+ const invalid = submitted && !ssoEmailPattern.test(email.trim());
52
+ const domain = email.trim().split('@')[1] ?? '';
53
+ const alert = errorProp ?? error;
54
+
55
+ const start = async () => {
56
+ setError(null);
57
+ setLoading(true);
58
+ try {
59
+ await onSubmit?.(email.trim());
60
+ setRedirecting(true);
61
+ } catch (e) {
62
+ setError(e instanceof Error ? e.message : 'Single sign-on could not start. Try again.');
63
+ }
64
+ setLoading(false);
65
+ };
66
+
67
+ const footer = (
68
+ <>
69
+ Not using SSO? <Link href={signInHref}>Sign in with a password</Link>
70
+ </>
71
+ );
72
+
73
+ return (
74
+ <AuthLayout fullScreen variant="card" logo={logo} skipLinkLabel="Skip to single sign-on" footer={footer}>
75
+ {redirecting ? (
76
+ <Stack gap={6}>
77
+ <AuthHeader
78
+ className="mb-0"
79
+ eyebrow="Single sign-on"
80
+ title={`Redirecting to ${providerName}`}
81
+ description={
82
+ <>
83
+ <span className="font-medium text-foreground">{domain}</span> signs in through {providerName}. If
84
+ nothing happens in a few seconds, try again.
85
+ </>
86
+ }
87
+ />
88
+ <Stack direction="row" align="center" gap={3} role="status">
89
+ <Spinner size={18} />
90
+ <Text variant="supporting" tone="muted">
91
+ Waiting for {providerName}…
92
+ </Text>
93
+ </Stack>
94
+ <Stack gap={2}>
95
+ <Button variant="secondary" size="lg" className="w-full" onClick={() => void start()} loading={loading}>
96
+ Try again
97
+ </Button>
98
+ <Button
99
+ variant="ghost"
100
+ size="lg"
101
+ className="w-full"
102
+ onClick={() => {
103
+ setRedirecting(false);
104
+ onCancel?.();
105
+ }}
106
+ >
107
+ Use a different email
108
+ </Button>
109
+ </Stack>
110
+ </Stack>
111
+ ) : (
112
+ <>
113
+ <AuthHeader
114
+ eyebrow="Single sign-on"
115
+ title="Sign in with SSO"
116
+ description="Enter your work email and we'll send you to your organization's identity provider."
117
+ />
118
+ <form
119
+ noValidate
120
+ aria-label="Single sign-on"
121
+ className="flex flex-col gap-4"
122
+ onSubmit={(e) => {
123
+ e.preventDefault();
124
+ setSubmitted(true);
125
+ if (ssoEmailPattern.test(email.trim())) void start();
126
+ }}
127
+ >
128
+ {alert && <Alert tone="destructive" description={alert} />}
129
+ <Field invalid={invalid}>
130
+ <FieldLabel>Work email</FieldLabel>
131
+ <Input
132
+ type="email"
133
+ leadingIcon={Building2}
134
+ autoComplete="email"
135
+ placeholder="name@company.com"
136
+ value={email}
137
+ onValueChange={setEmail}
138
+ required
139
+ />
140
+ <FieldDescription>Your domain decides where you sign in.</FieldDescription>
141
+ <FieldError match={invalid}>Enter a valid work email.</FieldError>
142
+ </Field>
143
+ <Button type="submit" size="lg" loading={loading} className="mt-2 w-full">
144
+ Continue with SSO
145
+ </Button>
146
+ </form>
147
+ </>
148
+ )}
149
+ </AuthLayout>
150
+ );
151
+ }
@@ -0,0 +1,10 @@
1
+ /** Sample copy for the single sign-on lookup. */
2
+ export const ssoLoginSample = {
3
+ defaultEmail: '',
4
+ signInHref: '#sign-in',
5
+ /** Identity provider shown while redirecting (resolved from the email domain in a real product). */
6
+ providerName: 'your identity provider',
7
+ };
8
+
9
+ /** Simple shape check before asking the server for the domain's identity provider. */
10
+ export const ssoEmailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Single sign-on',
5
+ family: 'Auth',
6
+ priority: 'P2',
7
+ status: 'beta',
8
+ description:
9
+ 'Centred card SSO entry: work email lookup, then a "Redirecting to your identity provider" state with cancel and a password fallback.',
10
+ layout: 'AuthLayout',
11
+ blocks: [],
12
+ };