@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,23 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import Status403Page from './Page';
5
+
6
+ describe('Status403Page', () => {
7
+ it('renders the forbidden message with landmarks', async () => {
8
+ render(<Status403Page />);
9
+ expect(screen.getByRole('main')).toBeInTheDocument();
10
+ expect(screen.getByRole('heading', { level: 1, name: 'You don’t have access to Billing' })).toBeInTheDocument();
11
+ expect(screen.getByRole('button', { name: 'Go to dashboard' })).toHaveAttribute('href', '/');
12
+ await expectNoAxeViolations();
13
+ });
14
+
15
+ it('requests access and moves to the requested state', async () => {
16
+ const onRequestAccess = vi.fn(() => Promise.resolve());
17
+ render(<Status403Page resource="Audit log" onRequestAccess={onRequestAccess} />);
18
+ await userEvent.click(screen.getByRole('button', { name: 'Request access' }));
19
+ expect(onRequestAccess).toHaveBeenCalledOnce();
20
+ expect(await screen.findByRole('button', { name: 'Access requested' })).toBeDisabled();
21
+ expect(screen.getByRole('status')).toHaveTextContent('Access request sent');
22
+ });
23
+ });
@@ -0,0 +1,92 @@
1
+ import { Check, Home, Lock } from '@gntik-ai/icons';
2
+ import { Button, Link, Logo, StatusLayout } from '@gntik-ai/ui';
3
+ import { useState, type ReactNode } from 'react';
4
+ import { forbiddenContent } from './data';
5
+
6
+ export interface Status403Props {
7
+ /** What the viewer cannot see. */
8
+ resource: string;
9
+ /** Who can grant access. */
10
+ owner: string;
11
+ /** Called on "Request access"; may return a promise (the button shows a spinner). */
12
+ onRequestAccess: () => void | Promise<void>;
13
+ /** Start in the "requested" state (a pending request exists). */
14
+ defaultRequested: boolean;
15
+ homeHref: string;
16
+ homeLabel: string;
17
+ /** Account shown in the footer; `null` hides the line. */
18
+ signedInAs: string | null;
19
+ switchAccountHref: string;
20
+ header: ReactNode;
21
+ fullScreen: boolean;
22
+ }
23
+
24
+ type RequestState = 'idle' | 'pending' | 'requested';
25
+
26
+ /** 403 page: StatusLayout with Request access (NoAccessEmpty behaviour) and a way home. */
27
+ export default function Status403Page(props: Partial<Status403Props>) {
28
+ const {
29
+ resource = forbiddenContent.resource,
30
+ owner = forbiddenContent.owner,
31
+ onRequestAccess,
32
+ defaultRequested = false,
33
+ homeHref = forbiddenContent.homeHref,
34
+ homeLabel = forbiddenContent.homeLabel,
35
+ signedInAs = forbiddenContent.signedInAs,
36
+ switchAccountHref = forbiddenContent.switchAccountHref,
37
+ header = <Logo size={24} wordmark />,
38
+ fullScreen = true,
39
+ } = props;
40
+ const [state, setState] = useState<RequestState>(defaultRequested ? 'requested' : 'idle');
41
+
42
+ const request = async () => {
43
+ setState('pending');
44
+ try {
45
+ await onRequestAccess?.();
46
+ setState('requested');
47
+ } catch {
48
+ setState('idle');
49
+ }
50
+ };
51
+
52
+ const requested = state === 'requested';
53
+ return (
54
+ <StatusLayout
55
+ fullScreen={fullScreen}
56
+ tone="warning"
57
+ header={header}
58
+ icon={Lock}
59
+ code={forbiddenContent.code}
60
+ title={`You don’t have access to ${resource}`}
61
+ description={`Ask ${owner} to grant you a role that includes ${resource.toLowerCase()} permissions.`}
62
+ primaryAction={
63
+ <Button
64
+ variant={requested ? 'secondary' : 'primary'}
65
+ icon={requested ? Check : undefined}
66
+ loading={state === 'pending'}
67
+ disabled={requested}
68
+ onClick={() => void request()}
69
+ >
70
+ {requested ? 'Access requested' : 'Request access'}
71
+ </Button>
72
+ }
73
+ secondaryAction={
74
+ <Button variant="secondary" icon={Home} render={<a href={homeHref} />} nativeButton={false}>
75
+ {homeLabel}
76
+ </Button>
77
+ }
78
+ footer={
79
+ signedInAs ? (
80
+ <span>
81
+ Signed in as <span className="font-mono">{signedInAs}</span> ·{' '}
82
+ <Link href={switchAccountHref}>{forbiddenContent.switchAccountLabel}</Link>
83
+ </span>
84
+ ) : undefined
85
+ }
86
+ >
87
+ <p role="status" className="sr-only">
88
+ {requested ? 'Access request sent' : ''}
89
+ </p>
90
+ </StatusLayout>
91
+ );
92
+ }
@@ -0,0 +1,13 @@
1
+ import { noAccessSample } from '@gntik-ai/blocks';
2
+
3
+ /** Neutral fixtures for the 403 page (resource and owner reuse the NoAccessEmpty sample). */
4
+ export const forbiddenContent = {
5
+ code: '403 · Forbidden',
6
+ resource: noAccessSample.resource,
7
+ owner: noAccessSample.owner,
8
+ homeLabel: 'Go to dashboard',
9
+ homeHref: '/',
10
+ signedInAs: 'dana@example.com',
11
+ switchAccountLabel: 'Switch account',
12
+ switchAccountHref: '/sign-in',
13
+ };
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: '403 Forbidden',
5
+ family: 'System',
6
+ priority: 'P1',
7
+ status: 'beta',
8
+ description:
9
+ 'Full-page permission state: explains who can grant access, with a Request access action (idle · pending · requested, like NoAccessEmpty) and a way home.',
10
+ layout: 'StatusLayout',
11
+ blocks: [],
12
+ };
@@ -0,0 +1,23 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import Status404Page from './Page';
5
+
6
+ describe('Status404Page', () => {
7
+ it('renders the not-found message with landmarks and a way home', async () => {
8
+ render(<Status404Page />);
9
+ expect(screen.getByRole('main')).toBeInTheDocument();
10
+ expect(screen.getByRole('heading', { level: 1, name: 'We can’t find that page' })).toBeInTheDocument();
11
+ expect(screen.getByRole('button', { name: 'Go to dashboard' })).toHaveAttribute('href', '/');
12
+ expect(screen.getByRole('link', { name: 'Contact support' })).toBeInTheDocument();
13
+ await expectNoAxeViolations();
14
+ });
15
+
16
+ it('calls onBack from Go back', async () => {
17
+ const onBack = vi.fn();
18
+ render(<Status404Page onBack={onBack} homeHref="/projects" />);
19
+ await userEvent.click(screen.getByRole('button', { name: 'Go back' }));
20
+ expect(onBack).toHaveBeenCalledOnce();
21
+ expect(screen.getByRole('button', { name: 'Go to dashboard' })).toHaveAttribute('href', '/projects');
22
+ });
23
+ });
@@ -0,0 +1,60 @@
1
+ import { ArrowLeft, FileQuestion, Home } from '@gntik-ai/icons';
2
+ import { Button, Link, Logo, StatusLayout } from '@gntik-ai/ui';
3
+ import type { ReactNode } from 'react';
4
+ import { notFoundContent } from './data';
5
+
6
+ export interface Status404Props {
7
+ title: ReactNode;
8
+ /** Replaces the built-in title; supply exactly one h1 and use its own ref for route focus. */
9
+ heading?: ReactNode;
10
+ description: ReactNode;
11
+ /** Where the primary action leads. */
12
+ homeHref: string;
13
+ homeLabel: string;
14
+ /** Go back handler; defaults to `history.back()`. */
15
+ onBack: () => void;
16
+ backLabel: string;
17
+ supportHref: string;
18
+ supportLabel: string;
19
+ /** Minimal top bar; defaults to the Logo with wordmark. */
20
+ header: ReactNode;
21
+ fullScreen: boolean;
22
+ }
23
+
24
+ /** 404 page: StatusLayout with a way home, Go back and a support link in the footer. */
25
+ export default function Status404Page(props: Partial<Status404Props>) {
26
+ const {
27
+ title = notFoundContent.title,
28
+ heading,
29
+ description = notFoundContent.description,
30
+ homeHref = notFoundContent.homeHref,
31
+ homeLabel = notFoundContent.homeLabel,
32
+ onBack = () => window.history.back(),
33
+ backLabel = notFoundContent.backLabel,
34
+ supportHref = notFoundContent.supportHref,
35
+ supportLabel = notFoundContent.supportLabel,
36
+ header = <Logo size={24} wordmark />,
37
+ fullScreen = true,
38
+ } = props;
39
+ return (
40
+ <StatusLayout
41
+ fullScreen={fullScreen}
42
+ header={header}
43
+ icon={FileQuestion}
44
+ code={notFoundContent.code}
45
+ {...(heading != null ? { heading } : { title })}
46
+ description={description}
47
+ primaryAction={
48
+ <Button icon={Home} render={<a href={homeHref} />} nativeButton={false}>
49
+ {homeLabel}
50
+ </Button>
51
+ }
52
+ secondaryAction={
53
+ <Button variant="secondary" icon={ArrowLeft} onClick={onBack}>
54
+ {backLabel}
55
+ </Button>
56
+ }
57
+ footer={<Link href={supportHref}>{supportLabel}</Link>}
58
+ />
59
+ );
60
+ }
@@ -0,0 +1,52 @@
1
+ import { PageHeader } from '@gntik-ai/blocks';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { createRef } from 'react';
5
+ import { expectNoAxeViolations } from '../test/a11y';
6
+ import Status404Page from './Page';
7
+
8
+ it('uses PageHeader as the sole h1 while preserving the 404 content, actions and focus contract', async () => {
9
+ const user = userEvent.setup();
10
+ const onBack = vi.fn();
11
+ const titleRef = createRef<HTMLHeadingElement>();
12
+ render(
13
+ <Status404Page
14
+ homeHref="/dashboard"
15
+ onBack={onBack}
16
+ heading={
17
+ <PageHeader
18
+ as="div"
19
+ title="Page not found"
20
+ titleRef={titleRef}
21
+ breadcrumbs={null}
22
+ description={null}
23
+ status=""
24
+ meta={[]}
25
+ tabs={null}
26
+ actions={[]}
27
+ />
28
+ }
29
+ />,
30
+ );
31
+ expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
32
+ const heading = screen.getByRole('heading', { level: 1, name: 'Page not found' });
33
+ expect(screen.queryByText('We can’t find that page')).not.toBeInTheDocument();
34
+ expect(screen.getByText('404 · Not found')).toBeInTheDocument();
35
+ expect(
36
+ screen.getByText(
37
+ 'The page may have been moved or deleted, or the link is out of date. Check the address or head back to the dashboard.',
38
+ ),
39
+ ).toBeInTheDocument();
40
+ const primary = screen.getByRole('button', { name: 'Go to dashboard' });
41
+ expect(primary).toHaveAttribute('href', '/dashboard');
42
+ expect(screen.getByRole('main').querySelector('a[href], button, [tabindex="0"]')).toBe(primary);
43
+ expect(screen.getByRole('link', { name: 'Contact support' })).toHaveAttribute('href', '/support');
44
+ await user.tab();
45
+ await user.tab();
46
+ expect(primary).toHaveFocus();
47
+ titleRef.current?.focus();
48
+ expect(heading).toHaveFocus();
49
+ await user.click(screen.getByRole('button', { name: 'Go back' }));
50
+ expect(onBack).toHaveBeenCalledOnce();
51
+ await expectNoAxeViolations();
52
+ });
@@ -0,0 +1,12 @@
1
+ /** Neutral fixtures for the 404 page. */
2
+ export const notFoundContent = {
3
+ code: '404 · Not found',
4
+ title: 'We can’t find that page',
5
+ description:
6
+ 'The page may have been moved or deleted, or the link is out of date. Check the address or head back to the dashboard.',
7
+ homeLabel: 'Go to dashboard',
8
+ homeHref: '/',
9
+ backLabel: 'Go back',
10
+ supportLabel: 'Contact support',
11
+ supportHref: '/support',
12
+ };
@@ -0,0 +1,28 @@
1
+ import { PageHeader } from '@gntik-ai/blocks';
2
+ import { useEffect, useRef } from 'react';
3
+ import Status404Page from '../Page';
4
+
5
+ export default function Status404PageHeader() {
6
+ const titleRef = useRef<HTMLHeadingElement>(null);
7
+ useEffect(() => {
8
+ titleRef.current?.focus();
9
+ }, []);
10
+
11
+ return (
12
+ <Status404Page
13
+ heading={
14
+ <PageHeader
15
+ as="div"
16
+ title="Page not found"
17
+ titleRef={titleRef}
18
+ breadcrumbs={null}
19
+ description={null}
20
+ status=""
21
+ meta={[]}
22
+ tabs={null}
23
+ actions={[]}
24
+ />
25
+ }
26
+ />
27
+ );
28
+ }
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: '404 Not found',
5
+ family: 'System',
6
+ priority: 'P1',
7
+ status: 'beta',
8
+ description:
9
+ 'Full-page not-found state: code, message, a primary way home, Go back and a support link. The optional heading slot replaces the built-in title so PageHeader supplies the only h1; code stays above it and actions remain unchanged. In the PageHeader example set as="div", breadcrumbs={null}, meta={[]}, tabs={null}, actions={[]}, status="" and description={null}; use StatusLayout’s description once. PageHeader titleRef adds tabIndex=-1 for ref.current.focus() on route load without a Tab stop; leave slot links and actions empty to keep the primary action first in main.',
10
+ layout: 'StatusLayout',
11
+ blocks: ['page-header'],
12
+ };
@@ -0,0 +1,24 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import Status500Page from './Page';
5
+
6
+ describe('Status500Page', () => {
7
+ it('renders the error page with the request id and landmarks', async () => {
8
+ render(<Status500Page />);
9
+ expect(screen.getByRole('main')).toBeInTheDocument();
10
+ expect(screen.getByRole('heading', { level: 1, name: 'Something went wrong on our side' })).toBeInTheDocument();
11
+ expect(screen.getByRole('heading', { level: 2, name: 'The request failed' })).toBeInTheDocument();
12
+ expect(screen.getByText('req_3b91e6c2f04d')).toBeInTheDocument();
13
+ expect(screen.getByRole('link', { name: 'System status' })).toHaveAttribute('href', '/status');
14
+ await expectNoAxeViolations();
15
+ });
16
+
17
+ it('retries through onRetry', async () => {
18
+ const onRetry = vi.fn(() => Promise.resolve());
19
+ render(<Status500Page onRetry={onRetry} requestId="req_custom" />);
20
+ await userEvent.click(screen.getByRole('button', { name: 'Try again' }));
21
+ expect(onRetry).toHaveBeenCalledOnce();
22
+ expect(screen.getByText('req_custom')).toBeInTheDocument();
23
+ });
24
+ });
@@ -0,0 +1,70 @@
1
+ import { ErrorPanel, type FeedbackAction } from '@gntik-ai/blocks';
2
+ import { ServerCrash } from '@gntik-ai/icons';
3
+ import { Link, Logo, StatusLayout } from '@gntik-ai/ui';
4
+ import type { ReactNode } from 'react';
5
+ import { serverErrorContent, simulateRetry } from './data';
6
+
7
+ export interface Status500Props {
8
+ title: ReactNode;
9
+ description: ReactNode;
10
+ /** Status or error code on the ErrorPanel chip. */
11
+ errorCode: string | number;
12
+ /** Correlation id for support (copyable). */
13
+ requestId: string;
14
+ /** Raw details behind the disclosure. */
15
+ details: string;
16
+ /** Retry handler; a returned promise shows the loading state. */
17
+ onRetry: () => void | Promise<void>;
18
+ /** Extra action next to Retry; `null` hides it. */
19
+ supportAction: FeedbackAction | null;
20
+ homeHref: string;
21
+ statusHref: string;
22
+ header: ReactNode;
23
+ fullScreen: boolean;
24
+ }
25
+
26
+ /** 500 page: StatusLayout + ErrorPanel with request id, retry and technical details. */
27
+ export default function Status500Page(props: Partial<Status500Props>) {
28
+ const {
29
+ title = serverErrorContent.title,
30
+ description = serverErrorContent.description,
31
+ errorCode = serverErrorContent.errorCode,
32
+ requestId = serverErrorContent.requestId,
33
+ details = serverErrorContent.details,
34
+ onRetry = simulateRetry,
35
+ supportAction = { label: 'Contact support', variant: 'ghost' },
36
+ homeHref = serverErrorContent.homeHref,
37
+ statusHref = serverErrorContent.statusHref,
38
+ header = <Logo size={24} wordmark />,
39
+ fullScreen = true,
40
+ } = props;
41
+ return (
42
+ <StatusLayout
43
+ fullScreen={fullScreen}
44
+ tone="destructive"
45
+ header={header}
46
+ icon={ServerCrash}
47
+ code={serverErrorContent.code}
48
+ title={title}
49
+ description={description}
50
+ footer={
51
+ <>
52
+ <Link href={homeHref}>{serverErrorContent.homeLabel}</Link>
53
+ <Link href={statusHref}>{serverErrorContent.statusLabel}</Link>
54
+ </>
55
+ }
56
+ >
57
+ <ErrorPanel
58
+ titleAs="h2"
59
+ className="text-start"
60
+ title={serverErrorContent.panelTitle}
61
+ message={serverErrorContent.panelMessage}
62
+ code={errorCode}
63
+ requestId={requestId}
64
+ details={details}
65
+ onRetry={onRetry}
66
+ secondaryAction={supportAction}
67
+ />
68
+ </StatusLayout>
69
+ );
70
+ }
@@ -0,0 +1,20 @@
1
+ /** Neutral fixtures for the 500 page. */
2
+ export const serverErrorContent = {
3
+ code: '500 · Server error',
4
+ title: 'Something went wrong on our side',
5
+ description: 'The page failed to load. Nothing you did caused this, and your data is safe.',
6
+ panelTitle: 'The request failed',
7
+ panelMessage:
8
+ 'An unexpected error interrupted the request. Try again; if it keeps happening, share the request ID with support.',
9
+ errorCode: 500,
10
+ requestId: 'req_3b91e6c2f04d',
11
+ details:
12
+ 'GET /v1/projects/web-app/overview\n500 Internal Server Error\nunhandled exception in handler (trace: 7c1e9a42)',
13
+ homeLabel: 'Go to dashboard',
14
+ homeHref: '/',
15
+ statusLabel: 'System status',
16
+ statusHref: '/status',
17
+ };
18
+
19
+ /** Simulated retry for the standalone page. */
20
+ export const simulateRetry = () => new Promise<void>((resolve) => setTimeout(resolve, 1200));
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: '500 Server error',
5
+ family: 'System',
6
+ priority: 'P1',
7
+ status: 'beta',
8
+ description:
9
+ 'Full-page server error: StatusLayout message with an ErrorPanel (status code, copyable request id, retry, technical details) and links home and to the status page.',
10
+ layout: 'StatusLayout',
11
+ blocks: ['error-panel'],
12
+ };
@@ -0,0 +1,24 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import StatusMaintenancePage from './Page';
5
+
6
+ describe('StatusMaintenancePage', () => {
7
+ it('renders the maintenance message, banner and status link', async () => {
8
+ render(<StatusMaintenancePage />);
9
+ expect(screen.getByRole('main')).toBeInTheDocument();
10
+ expect(screen.getByRole('heading', { level: 1, name: 'We’ll be back shortly' })).toBeInTheDocument();
11
+ expect(screen.getByRole('status')).toHaveTextContent('Maintenance in progress.');
12
+ expect(screen.getByRole('link', { name: 'Subscribe to updates' })).toHaveAttribute('href', '/status/subscribe');
13
+ expect(screen.getByRole('button', { name: 'View status page' })).toHaveAttribute('href', '/status');
14
+ await expectNoAxeViolations();
15
+ });
16
+
17
+ it('shows a note when Check again finds the service still down', async () => {
18
+ const onCheckAgain = vi.fn(() => false);
19
+ render(<StatusMaintenancePage onCheckAgain={onCheckAgain} />);
20
+ await userEvent.click(screen.getByRole('button', { name: 'Check again' }));
21
+ expect(onCheckAgain).toHaveBeenCalledOnce();
22
+ expect(await screen.findByText('Still in maintenance. Try again in a few minutes.')).toBeInTheDocument();
23
+ });
24
+ });
@@ -0,0 +1,84 @@
1
+ import { SystemBanner } from '@gntik-ai/blocks';
2
+ import { ExternalLink, RefreshCw, Wrench } from '@gntik-ai/icons';
3
+ import { Button, Logo, StatusLayout } from '@gntik-ai/ui';
4
+ import { useState, type ReactNode } from 'react';
5
+ import { maintenanceContent } from './data';
6
+
7
+ export interface StatusMaintenanceProps {
8
+ title: ReactNode;
9
+ description: ReactNode;
10
+ /** Bold lead-in and message of the maintenance banner. */
11
+ bannerTitle: ReactNode;
12
+ bannerMessage: ReactNode;
13
+ statusHref: string;
14
+ subscribeHref: string;
15
+ /**
16
+ * "Check again" handler. Resolve `true` when the service is back (the product then navigates);
17
+ * anything else shows the "still in maintenance" note.
18
+ */
19
+ onCheckAgain: () => boolean | void | Promise<boolean | void>;
20
+ header: ReactNode;
21
+ fullScreen: boolean;
22
+ }
23
+
24
+ /** Maintenance page: StatusLayout with Check again, a status page link and a maintenance SystemBanner. */
25
+ export default function StatusMaintenancePage(props: Partial<StatusMaintenanceProps>) {
26
+ const {
27
+ title = maintenanceContent.title,
28
+ description = maintenanceContent.description,
29
+ bannerTitle = maintenanceContent.bannerTitle,
30
+ bannerMessage = maintenanceContent.bannerMessage,
31
+ statusHref = maintenanceContent.statusHref,
32
+ subscribeHref = maintenanceContent.subscribeHref,
33
+ onCheckAgain,
34
+ header = <Logo size={24} wordmark />,
35
+ fullScreen = true,
36
+ } = props;
37
+ const [checking, setChecking] = useState(false);
38
+ const [note, setNote] = useState('');
39
+
40
+ const check = async () => {
41
+ setChecking(true);
42
+ setNote('');
43
+ try {
44
+ const back = await onCheckAgain?.();
45
+ if (back !== true) setNote(maintenanceContent.stillDown);
46
+ } finally {
47
+ setChecking(false);
48
+ }
49
+ };
50
+
51
+ return (
52
+ <StatusLayout
53
+ fullScreen={fullScreen}
54
+ tone="warning"
55
+ header={header}
56
+ icon={Wrench}
57
+ code={maintenanceContent.code}
58
+ title={title}
59
+ description={description}
60
+ primaryAction={
61
+ <Button icon={RefreshCw} loading={checking} onClick={() => void check()}>
62
+ Check again
63
+ </Button>
64
+ }
65
+ secondaryAction={
66
+ <Button variant="secondary" trailingIcon={ExternalLink} render={<a href={statusHref} />} nativeButton={false}>
67
+ {maintenanceContent.statusLabel}
68
+ </Button>
69
+ }
70
+ >
71
+ <SystemBanner
72
+ kind="maintenance"
73
+ dismissible={false}
74
+ title={bannerTitle}
75
+ message={bannerMessage}
76
+ action={{ label: maintenanceContent.subscribeLabel, href: subscribeHref }}
77
+ className="rounded-lg border text-start"
78
+ />
79
+ <p aria-live="polite" className="mt-3 min-h-4 text-[12.5px] text-muted-foreground">
80
+ {note}
81
+ </p>
82
+ </StatusLayout>
83
+ );
84
+ }
@@ -0,0 +1,14 @@
1
+ /** Neutral fixtures for the maintenance page. */
2
+ export const maintenanceContent = {
3
+ code: 'Scheduled maintenance',
4
+ title: 'We’ll be back shortly',
5
+ description:
6
+ 'We are upgrading the database. Running services are not affected; the dashboard is read-only until 14:30 UTC.',
7
+ bannerTitle: 'Maintenance in progress.',
8
+ bannerMessage: 'Started 13:00 UTC · expected to end 14:30 UTC.',
9
+ statusLabel: 'View status page',
10
+ statusHref: '/status',
11
+ subscribeLabel: 'Subscribe to updates',
12
+ subscribeHref: '/status/subscribe',
13
+ stillDown: 'Still in maintenance. Try again in a few minutes.',
14
+ };
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Maintenance',
5
+ family: 'System',
6
+ priority: 'P1',
7
+ status: 'beta',
8
+ description:
9
+ 'Full-page maintenance state: message, Check again and a status page link, with a maintenance SystemBanner carrying the window and a subscribe link.',
10
+ layout: 'StatusLayout',
11
+ blocks: ['system-banner'],
12
+ };
package/src/styles.css ADDED
@@ -0,0 +1,11 @@
1
+ /* @gntik-ai/templates — everything a product built on the templates needs, in one import:
2
+ @import "tailwindcss";
3
+ @import "@gntik-ai/templates/styles.css";
4
+ Tokens + the Tailwind bridge (via ui), and the sources of every kit package the templates use. */
5
+ @import "@gntik-ai/ui/styles.css";
6
+ @import "@gntik-ai/blocks/styles.css";
7
+ @import "@gntik-ai/charts/styles.css";
8
+ @import "@gntik-ai/chat/styles.css";
9
+ @import "@gntik-ai/flow/styles.css";
10
+ @import "@gntik-ai/editor/styles.css";
11
+ @source "../dist";
@@ -0,0 +1,18 @@
1
+ import axe from 'axe-core';
2
+
3
+ /**
4
+ * Runs axe on a container and returns the violations. Colour contrast is checked in the
5
+ * browser suite (jsdom has no layout or computed colours), so it is disabled here.
6
+ */
7
+ export async function axeViolations(container: Element = document.body) {
8
+ const result = await axe.run(container, {
9
+ rules: { 'color-contrast': { enabled: false }, region: { enabled: false } },
10
+ });
11
+ return result.violations.map((v) => `${v.id}: ${v.help} (${v.nodes.map((n) => n.target.join(' ')).join(', ')})`);
12
+ }
13
+
14
+ /** Asserts that a container has no axe violations. */
15
+ export async function expectNoAxeViolations(container: Element = document.body) {
16
+ const violations = await axeViolations(container);
17
+ if (violations.length) throw new Error('axe violations:\n' + violations.join('\n'));
18
+ }