@gntik-ai/templates 0.0.0-stage → 0.5.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
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@gntik-ai/templates",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.5.0",
4
+ "description": "gntik-ui page templates: complete product pages (auth, dashboard, resources, settings, system) on the kit layouts and blocks.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "*.css"
9
+ ],
10
+ "files": [
11
+ "dist",
12
+ "src"
13
+ ],
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json",
22
+ "./styles.css": "./src/styles.css"
23
+ },
24
+ "publishConfig": {
25
+ "registry": "https://registry.npmjs.org/",
26
+ "access": "public"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/gntik-ai/gntik-ui.git",
31
+ "directory": "packages/templates"
32
+ },
33
+ "peerDependencies": {
34
+ "react": "^19.0.0",
35
+ "react-dom": "^19.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@gntik-ai/blocks": "0.5.0",
39
+ "@gntik-ai/charts": "0.5.0",
40
+ "@gntik-ai/chat": "0.5.0",
41
+ "@gntik-ai/editor": "0.5.0",
42
+ "@gntik-ai/flow": "0.5.0",
43
+ "@gntik-ai/icons": "0.5.0",
44
+ "@gntik-ai/tokens": "0.2.1",
45
+ "@gntik-ai/ui": "0.5.0",
46
+ "lucide-react": "^1.48.0"
47
+ },
48
+ "devDependencies": {
49
+ "@testing-library/jest-dom": "^7.0.1",
50
+ "@testing-library/react": "^16.3.3",
51
+ "@testing-library/user-event": "^14.6.7",
52
+ "@types/react": "^19.3.0",
53
+ "@types/react-dom": "^19.3.0",
54
+ "axe-core": "^4.13.0",
55
+ "jsdom": "^30.1.1",
56
+ "react": "^19.3.0",
57
+ "react-dom": "^19.3.0",
58
+ "tsdown": "^0.23.0",
59
+ "typescript": "~6.0.3",
60
+ "vitest": "^5.0.2"
61
+ },
62
+ "scripts": {
63
+ "build": "tsdown",
64
+ "typecheck": "tsc -p tsconfig.json --noEmit",
65
+ "test": "vitest run"
66
+ }
6
67
  }
@@ -0,0 +1,40 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import AcceptInvitationPage from './Page';
5
+
6
+ describe('AcceptInvitationPage', () => {
7
+ it('renders the inviter, workspace and role', async () => {
8
+ const { container } = render(<AcceptInvitationPage />);
9
+ expect(screen.getByRole('main')).toBeInTheDocument();
10
+ expect(screen.getByRole('heading', { level: 1, name: 'Join Acme Industries' })).toBeInTheDocument();
11
+ expect(screen.getByText('Priya Raman')).toBeInTheDocument();
12
+ expect(screen.getByText('Developer')).toBeInTheDocument();
13
+ await expectNoAxeViolations(container);
14
+ });
15
+
16
+ it('joins the workspace', async () => {
17
+ const onAccept = vi.fn();
18
+ const onOpenWorkspace = vi.fn();
19
+ const { container } = render(<AcceptInvitationPage onAccept={onAccept} onOpenWorkspace={onOpenWorkspace} />);
20
+ await userEvent.click(screen.getByRole('button', { name: 'Join Acme Industries' }));
21
+ expect(onAccept).toHaveBeenCalledOnce();
22
+ expect(await screen.findByRole('heading', { level: 1, name: 'Welcome to Acme Industries' })).toBeInTheDocument();
23
+ expect(screen.getByRole('status')).toHaveTextContent('You joined Acme Industries.');
24
+ await userEvent.click(screen.getByRole('button', { name: 'Open workspace' }));
25
+ expect(onOpenWorkspace).toHaveBeenCalledOnce();
26
+ await expectNoAxeViolations(container);
27
+ });
28
+
29
+ it('declines, and keeps the card on failure', async () => {
30
+ const onDecline = vi
31
+ .fn()
32
+ .mockRejectedValueOnce(new Error('The invitation has expired.'))
33
+ .mockResolvedValueOnce(undefined);
34
+ render(<AcceptInvitationPage onDecline={onDecline} />);
35
+ await userEvent.click(screen.getByRole('button', { name: 'Decline' }));
36
+ expect(await screen.findByText('The invitation has expired.')).toBeInTheDocument();
37
+ await userEvent.click(screen.getByRole('button', { name: 'Decline' }));
38
+ expect(await screen.findByRole('heading', { level: 1, name: 'No problem' })).toBeInTheDocument();
39
+ });
40
+ });
@@ -0,0 +1,153 @@
1
+ import { AuthHeader } from '@gntik-ai/blocks';
2
+ import { Alert, AuthLayout, Avatar, Badge, Button, Link, Separator, Stack, Text } from '@gntik-ai/ui';
3
+ import { useState, type ReactNode } from 'react';
4
+ import { sampleInvitation, type Invitation } from './data';
5
+
6
+ export interface AcceptInvitationPageProps {
7
+ invitation: Invitation;
8
+ /** Joins the workspace. A returned promise shows the loading state; a rejection shows in the alert. */
9
+ onAccept: () => void | Promise<void>;
10
+ /** Declines the invitation. */
11
+ onDecline: () => void | Promise<void>;
12
+ /** From the joined state. */
13
+ onOpenWorkspace: () => void;
14
+ switchAccountHref: string;
15
+ logo: ReactNode;
16
+ }
17
+
18
+ type Outcome = 'pending' | 'accepted' | 'declined';
19
+
20
+ /** Card invitation: inviter, workspace, role → Join / Decline. */
21
+ export default function AcceptInvitationPage({
22
+ invitation = sampleInvitation,
23
+ onAccept,
24
+ onDecline,
25
+ onOpenWorkspace,
26
+ switchAccountHref = '#switch-account',
27
+ logo,
28
+ }: Partial<AcceptInvitationPageProps>) {
29
+ const [outcome, setOutcome] = useState<Outcome>('pending');
30
+ const [busy, setBusy] = useState<'accept' | 'decline' | null>(null);
31
+ const [error, setError] = useState<string | null>(null);
32
+ const { workspace, role, roleDescription, memberCount, inviter, inviteeEmail, expiresIn } = invitation;
33
+
34
+ const act = async (kind: 'accept' | 'decline') => {
35
+ setError(null);
36
+ setBusy(kind);
37
+ try {
38
+ await (kind === 'accept' ? onAccept?.() : onDecline?.());
39
+ setOutcome(kind === 'accept' ? 'accepted' : 'declined');
40
+ } catch (e) {
41
+ setError(e instanceof Error ? e.message : 'Something went wrong. Try again.');
42
+ }
43
+ setBusy(null);
44
+ };
45
+
46
+ const body =
47
+ outcome === 'accepted' ? (
48
+ <Stack gap={2}>
49
+ <AuthHeader
50
+ eyebrow="Invitation accepted"
51
+ title={`Welcome to ${workspace}`}
52
+ description={`You joined as ${role}. ${inviter.name} has been notified.`}
53
+ />
54
+ <Button size="lg" className="w-full" onClick={onOpenWorkspace}>
55
+ Open workspace
56
+ </Button>
57
+ </Stack>
58
+ ) : outcome === 'declined' ? (
59
+ <AuthHeader
60
+ className="mb-0"
61
+ eyebrow="Invitation declined"
62
+ title="No problem"
63
+ description={`You declined the invitation to ${workspace}. ${inviter.name} can invite you again at any time.`}
64
+ />
65
+ ) : (
66
+ <>
67
+ <AuthHeader
68
+ eyebrow="Invitation"
69
+ title={`Join ${workspace}`}
70
+ description={`${inviter.name} invited you to collaborate. This invitation expires in ${expiresIn}.`}
71
+ />
72
+ <Stack gap={5}>
73
+ {error && <Alert tone="destructive" description={error} />}
74
+ <Stack direction="row" align="center" gap={3}>
75
+ <Avatar name={inviter.name} src={inviter.avatarUrl} size="md" aria-hidden />
76
+ <Stack gap={0}>
77
+ <Text variant="label">{inviter.name}</Text>
78
+ <Text variant="caption">{inviter.email}</Text>
79
+ </Stack>
80
+ </Stack>
81
+ <Separator />
82
+ <Stack as="dl" gap={3}>
83
+ <Stack direction="row" justify="between" align="center" gap={4}>
84
+ <Text as="dt" variant="supporting" tone="muted">
85
+ Workspace
86
+ </Text>
87
+ <Text as="dd" variant="label">
88
+ {workspace}
89
+ </Text>
90
+ </Stack>
91
+ <Stack direction="row" justify="between" align="center" gap={4}>
92
+ <Text as="dt" variant="supporting" tone="muted">
93
+ Role
94
+ </Text>
95
+ <Text as="dd" variant="label">
96
+ <Badge tone="primary">{role}</Badge>
97
+ </Text>
98
+ </Stack>
99
+ <Stack direction="row" justify="between" align="center" gap={4}>
100
+ <Text as="dt" variant="supporting" tone="muted">
101
+ Members
102
+ </Text>
103
+ <Text as="dd" variant="label">
104
+ {memberCount}
105
+ </Text>
106
+ </Stack>
107
+ </Stack>
108
+ <Text variant="caption">{roleDescription}</Text>
109
+ <Stack gap={2}>
110
+ <Button
111
+ size="lg"
112
+ className="w-full"
113
+ loading={busy === 'accept'}
114
+ disabled={busy === 'decline'}
115
+ onClick={() => void act('accept')}
116
+ >
117
+ Join {workspace}
118
+ </Button>
119
+ <Button
120
+ variant="ghost"
121
+ size="lg"
122
+ className="w-full"
123
+ loading={busy === 'decline'}
124
+ disabled={busy === 'accept'}
125
+ onClick={() => void act('decline')}
126
+ >
127
+ Decline
128
+ </Button>
129
+ </Stack>
130
+ </Stack>
131
+ </>
132
+ );
133
+
134
+ return (
135
+ <AuthLayout
136
+ fullScreen
137
+ variant="card"
138
+ logo={logo}
139
+ skipLinkLabel="Skip to invitation"
140
+ footer={
141
+ <>
142
+ Signed in as <span className="font-medium text-foreground">{inviteeEmail}</span>.{' '}
143
+ <Link href={switchAccountHref}>Not you?</Link>
144
+ </>
145
+ }
146
+ >
147
+ {body}
148
+ <span role="status" className="sr-only">
149
+ {outcome === 'accepted' ? `You joined ${workspace}.` : outcome === 'declined' ? 'Invitation declined.' : ''}
150
+ </span>
151
+ </AuthLayout>
152
+ );
153
+ }
@@ -0,0 +1,22 @@
1
+ /** Sample invitation: who invited whom, to which workspace and with what role. */
2
+ export interface Invitation {
3
+ workspace: string;
4
+ role: string;
5
+ /** What the role can do, in one line. */
6
+ roleDescription: string;
7
+ memberCount: number;
8
+ inviter: { name: string; email: string; avatarUrl?: string };
9
+ /** Signed-in (or invited) email. */
10
+ inviteeEmail: string;
11
+ expiresIn: string;
12
+ }
13
+
14
+ export const sampleInvitation: Invitation = {
15
+ workspace: 'Acme Industries',
16
+ role: 'Developer',
17
+ roleDescription: 'Create projects, deploy to staging and view usage.',
18
+ memberCount: 24,
19
+ inviter: { name: 'Priya Raman', email: 'priya@acme.example' },
20
+ inviteeEmail: 'dana@example.com',
21
+ expiresIn: '6 days',
22
+ };
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Accept invitation',
5
+ family: 'Auth',
6
+ priority: 'P2',
7
+ status: 'beta',
8
+ description:
9
+ 'Centred card invitation: inviter, workspace, role and member count, the invited email with a switch-account link, and Join / Decline with result states.',
10
+ layout: 'AuthLayout',
11
+ blocks: [],
12
+ };
@@ -0,0 +1,51 @@
1
+ import { act, fireEvent, render, screen } from '@testing-library/react';
2
+ import { expectNoAxeViolations } from '../test/a11y';
3
+ import AssistantChatPage from './Page';
4
+ import { assistantReply, assistantTool, chunkReply } from './data';
5
+
6
+ const total = assistantTool.durationMs + (chunkReply(assistantReply).length + 2) * 30;
7
+
8
+ describe('AssistantChatPage', () => {
9
+ it('renders the console with the empty conversation', { timeout: 15000 }, async () => {
10
+ render(<AssistantChatPage />);
11
+ expect(screen.getByRole('main')).toBeInTheDocument();
12
+ expect(screen.getByRole('heading', { level: 1, name: 'Assistant' })).toBeInTheDocument();
13
+ expect(screen.getByRole('heading', { name: 'How can I help?' })).toBeInTheDocument();
14
+ expect(screen.getByRole('textbox', { name: 'Message' })).toBeInTheDocument();
15
+ await expectNoAxeViolations();
16
+ });
17
+
18
+ describe('with fake timers', () => {
19
+ beforeEach(() => vi.useFakeTimers());
20
+ afterEach(() => vi.useRealTimers());
21
+
22
+ it('streams a reply with a tool call and sources after a suggestion', { timeout: 15000 }, () => {
23
+ const onSend = vi.fn();
24
+ render(<AssistantChatPage onSend={onSend} />);
25
+ fireEvent.click(screen.getByRole('button', { name: /Summarise this week/ }));
26
+ expect(onSend).toHaveBeenCalledWith('Summarise this week’s deployments');
27
+ expect(screen.getByRole('button', { name: /list_deployments/ })).toHaveTextContent('Running');
28
+ act(() => vi.advanceTimersByTime(total));
29
+ expect(screen.getByRole('button', { name: /list_deployments/ })).toHaveTextContent('Succeeded');
30
+ expect(screen.getByRole('table')).toBeInTheDocument();
31
+ expect(screen.getAllByRole('region', { name: 'Sources' }).length).toBeGreaterThan(0);
32
+ });
33
+
34
+ it('Stop ends the stream and unmounting clears pending timers', { timeout: 15000 }, () => {
35
+ const { unmount } = render(<AssistantChatPage />);
36
+ const box = screen.getByRole('textbox', { name: 'Message' });
37
+ fireEvent.change(box, { target: { value: 'How are deployments going?' } });
38
+ fireEvent.keyDown(box, { key: 'Enter' });
39
+ act(() => vi.advanceTimersByTime(assistantTool.durationMs + 90));
40
+ fireEvent.click(screen.getByRole('button', { name: 'Stop generating' }));
41
+ expect(screen.getByRole('button', { name: 'Send message' })).toBeInTheDocument();
42
+ expect(screen.getByRole('button', { name: /list_deployments/ })).toHaveTextContent('Succeeded');
43
+
44
+ fireEvent.change(box, { target: { value: 'And invoices?' } });
45
+ fireEvent.keyDown(box, { key: 'Enter' });
46
+ const pending = vi.getTimerCount();
47
+ unmount();
48
+ expect(vi.getTimerCount()).toBeLessThanOrEqual(pending - chunkReply(assistantReply).length);
49
+ });
50
+ });
51
+ });
@@ -0,0 +1,273 @@
1
+ import { PageHeader } from '@gntik-ai/blocks';
2
+ import {
3
+ ChatAnnouncer,
4
+ ChatComposer,
5
+ ChatLayout,
6
+ ChatMessage,
7
+ CitationList,
8
+ Markdown,
9
+ SuggestionChips,
10
+ ToolCallCard,
11
+ type ChatFeedback,
12
+ type ChatRole,
13
+ type CitationSource,
14
+ type Suggestion,
15
+ type ToolCallStatus,
16
+ } from '@gntik-ai/chat';
17
+ import { SquarePen } from '@gntik-ai/icons';
18
+ import { Page, TooltipProvider, type BreadcrumbItem } from '@gntik-ai/ui';
19
+ import { useEffect, useRef, useState } from 'react';
20
+ import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
21
+ import {
22
+ assistantBreadcrumbs,
23
+ assistantReply,
24
+ assistantSources,
25
+ assistantSuggestions,
26
+ assistantTool,
27
+ chunkReply,
28
+ type AssistantTool,
29
+ } from './data';
30
+
31
+ interface Turn {
32
+ id: number;
33
+ role: ChatRole;
34
+ text: string;
35
+ time: Date;
36
+ streaming?: boolean;
37
+ tool?: { status: ToolCallStatus; durationMs?: number };
38
+ sources?: boolean;
39
+ feedback?: ChatFeedback;
40
+ }
41
+
42
+ export interface AssistantChatProps {
43
+ title: string;
44
+ suggestions: Suggestion[];
45
+ /** Sources cited under the reply and listed in the details panel. */
46
+ sources: CitationSource[];
47
+ /** Mock tool call run before each reply. */
48
+ tool: AssistantTool;
49
+ /** Mock reply (markdown), streamed in chunks. */
50
+ reply: string;
51
+ /** Milliseconds between streamed chunks. */
52
+ tickMs: number;
53
+ /** Show the details panel. */
54
+ withPanel: boolean;
55
+ /** Called with each prompt the user sends. */
56
+ onSend: (text: string) => void;
57
+ onFeedback: (feedback: ChatFeedback) => void;
58
+ breadcrumbs: BreadcrumbItem[];
59
+ currentHref: string;
60
+ /** ConsoleShell props (app nav, user, workspaces, sidebar footer, topbar…). */
61
+ shell: Omit<ConsoleShellProps, 'children'>;
62
+ }
63
+
64
+ /** Assistant chat: ConsoleShell + Page + ChatLayout with a mock-streamed reply (timers cleared on Stop and unmount). */
65
+ export default function AssistantChatPage(props: Partial<AssistantChatProps>) {
66
+ const {
67
+ title = 'Assistant',
68
+ suggestions = assistantSuggestions,
69
+ sources = assistantSources,
70
+ tool = assistantTool,
71
+ reply = assistantReply,
72
+ tickMs = 30,
73
+ withPanel = true,
74
+ onSend,
75
+ onFeedback,
76
+ breadcrumbs = assistantBreadcrumbs,
77
+ currentHref = '/assistant',
78
+ shell,
79
+ } = props;
80
+ const [turns, setTurns] = useState<Turn[]>([]);
81
+ const [streaming, setStreaming] = useState(false);
82
+ const nextId = useRef(1);
83
+ const [timers] = useState(() => new Set<ReturnType<typeof setTimeout>>());
84
+
85
+ const clearTimers = () => {
86
+ timers.forEach((t) => clearTimeout(t));
87
+ timers.clear();
88
+ };
89
+ useEffect(
90
+ () => () => {
91
+ timers.forEach((t) => clearTimeout(t));
92
+ timers.clear();
93
+ },
94
+ [timers],
95
+ );
96
+ const later = (fn: () => void, ms: number) => {
97
+ const t = setTimeout(() => {
98
+ timers.delete(t);
99
+ fn();
100
+ }, ms);
101
+ timers.add(t);
102
+ };
103
+
104
+ const patch = (id: number, fn: (t: Turn) => Turn) => setTurns((ts) => ts.map((t) => (t.id === id ? fn(t) : t)));
105
+
106
+ const respond = () => {
107
+ clearTimers();
108
+ const toolId = nextId.current++;
109
+ const replyId = nextId.current++;
110
+ const now = new Date();
111
+ setStreaming(true);
112
+ setTurns((ts) => [
113
+ ...ts,
114
+ { id: toolId, role: 'tool', text: '', time: now, tool: { status: 'running' } },
115
+ { id: replyId, role: 'assistant', text: '', time: now, streaming: true },
116
+ ]);
117
+ const pieces = chunkReply(reply);
118
+ const startAt = tool.durationMs;
119
+ later(() => patch(toolId, (t) => ({ ...t, tool: { status: 'succeeded', durationMs: tool.durationMs } })), startAt);
120
+ pieces.forEach((piece, i) =>
121
+ later(() => patch(replyId, (t) => ({ ...t, text: t.text + piece })), startAt + (i + 1) * tickMs),
122
+ );
123
+ later(
124
+ () => {
125
+ patch(replyId, (t) => ({ ...t, streaming: false, sources: true }));
126
+ setStreaming(false);
127
+ },
128
+ startAt + (pieces.length + 1) * tickMs,
129
+ );
130
+ };
131
+
132
+ const send = (text: string) => {
133
+ setTurns((ts) => [...ts, { id: nextId.current++, role: 'user', text, time: new Date() }]);
134
+ onSend?.(text);
135
+ respond();
136
+ };
137
+
138
+ const stop = () => {
139
+ clearTimers();
140
+ setTurns((ts) =>
141
+ ts.map((t) =>
142
+ t.streaming
143
+ ? { ...t, streaming: false }
144
+ : t.tool?.status === 'running'
145
+ ? { ...t, tool: { status: 'failed', durationMs: 0 } }
146
+ : t,
147
+ ),
148
+ );
149
+ setStreaming(false);
150
+ };
151
+
152
+ const retry = (id: number) => {
153
+ setTurns((ts) => {
154
+ const at = ts.findIndex((t) => t.id === id);
155
+ // Drop the reply and the tool call that preceded it.
156
+ const cut = at > 0 && ts[at - 1]?.role === 'tool' ? at - 1 : at;
157
+ return ts.slice(0, cut);
158
+ });
159
+ respond();
160
+ };
161
+
162
+ const newConversation = () => {
163
+ clearTimers();
164
+ setTurns([]);
165
+ setStreaming(false);
166
+ };
167
+
168
+ const lastReply = [...turns].reverse().find((t) => t.role === 'assistant' && !t.streaming)?.text;
169
+
170
+ return (
171
+ <ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
172
+ <Page
173
+ width="full"
174
+ header={
175
+ <PageHeader
176
+ breadcrumbs={null}
177
+ title={title}
178
+ description="Ask about projects, deployments, invoices and members. Answers cite their sources."
179
+ status=""
180
+ meta={[]}
181
+ tabs={null}
182
+ actions={[
183
+ {
184
+ label: 'New conversation',
185
+ icon: SquarePen,
186
+ variant: 'secondary',
187
+ disabled: turns.length === 0,
188
+ onClick: newConversation,
189
+ },
190
+ ]}
191
+ />
192
+ }
193
+ >
194
+ <TooltipProvider>
195
+ <div className="h-[680px] min-h-0 overflow-hidden rounded-xl border border-border">
196
+ <ChatLayout
197
+ header={
198
+ <h2 className="px-4 py-3 text-[13px] font-semibold text-foreground">
199
+ {turns.length === 0 ? 'New conversation' : 'Conversation'}
200
+ </h2>
201
+ }
202
+ panel={
203
+ withPanel ? (
204
+ <div className="flex flex-col gap-4 p-4 text-[13px]">
205
+ <div>
206
+ <h2 className="font-semibold text-foreground">Details</h2>
207
+ <p className="mt-1 text-muted-foreground">
208
+ Read access to projects, deployments, invoices and members in this workspace.
209
+ </p>
210
+ </div>
211
+ <CitationList sources={sources} heading="Context sources" />
212
+ </div>
213
+ ) : undefined
214
+ }
215
+ composer={<ChatComposer onSubmit={send} streaming={streaming} onStop={stop} maxLength={4000} />}
216
+ >
217
+ {turns.length === 0 && (
218
+ <div className="flex flex-col gap-5 pt-10">
219
+ <h3 className="text-center text-[20px] font-semibold tracking-tight text-foreground">
220
+ How can I help?
221
+ </h3>
222
+ <SuggestionChips suggestions={suggestions} onSelect={send} />
223
+ </div>
224
+ )}
225
+ {turns.map((t) => {
226
+ if (t.role === 'tool' && t.tool) {
227
+ return (
228
+ <ChatMessage key={t.id} role="tool" author="Tool" timestamp={t.time}>
229
+ <ToolCallCard
230
+ name={tool.name}
231
+ status={t.tool.status}
232
+ durationMs={t.tool.durationMs}
233
+ args={tool.args}
234
+ result={t.tool.status === 'succeeded' ? tool.result : undefined}
235
+ error={t.tool.status === 'failed' ? 'Stopped by the user.' : undefined}
236
+ />
237
+ </ChatMessage>
238
+ );
239
+ }
240
+ if (t.role === 'assistant') {
241
+ return (
242
+ <ChatMessage
243
+ key={t.id}
244
+ role="assistant"
245
+ timestamp={t.time}
246
+ streaming={t.streaming}
247
+ copyText={t.text}
248
+ onRetry={streaming ? undefined : () => retry(t.id)}
249
+ feedback={t.feedback ?? null}
250
+ onFeedback={(f) => {
251
+ patch(t.id, (x) => ({ ...x, feedback: f }));
252
+ onFeedback?.(f);
253
+ }}
254
+ >
255
+ {t.text && <Markdown streaming={t.streaming}>{t.text}</Markdown>}
256
+ {t.sources && <CitationList sources={sources} className="mt-3" />}
257
+ </ChatMessage>
258
+ );
259
+ }
260
+ return (
261
+ <ChatMessage key={t.id} role={t.role} timestamp={t.time}>
262
+ {t.text}
263
+ </ChatMessage>
264
+ );
265
+ })}
266
+ <ChatAnnouncer streaming={streaming} lastReply={lastReply} />
267
+ </ChatLayout>
268
+ </div>
269
+ </TooltipProvider>
270
+ </Page>
271
+ </ConsoleShell>
272
+ );
273
+ }