@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,68 @@
1
+ import { fireEvent, render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import BoardPage from './Page';
5
+ import { boardColumns, boardState, moveCard } from './data';
6
+
7
+ const lane = (name: string) => screen.getByRole('region', { name: new RegExp(`^${name}`) });
8
+
9
+ describe('BoardPage', () => {
10
+ it('renders every column with its cards', { timeout: 15000 }, async () => {
11
+ render(<BoardPage />);
12
+ expect(screen.getByRole('main')).toBeInTheDocument();
13
+ expect(screen.getByRole('heading', { level: 1, name: 'Sprint board' })).toBeInTheDocument();
14
+ for (const c of boardColumns)
15
+ expect(within(lane(c.title)).getAllByRole('button')).toHaveLength(boardState[c.id]!.length);
16
+ await expectNoAxeViolations();
17
+ });
18
+
19
+ it('moves a card with the keyboard and announces each step', { timeout: 15000 }, async () => {
20
+ const onMove = vi.fn();
21
+ render(<BoardPage onMove={onMove} />);
22
+ const card = within(lane('Backlog')).getByRole('button', { name: /Rotate staging/ });
23
+ card.focus();
24
+ await userEvent.keyboard(' ');
25
+ expect(card).toHaveAttribute('aria-pressed', 'true');
26
+ expect(
27
+ screen.getByText(/Picked up Rotate staging database credentials in Backlog, position 1 of 3/),
28
+ ).toBeInTheDocument();
29
+ await userEvent.keyboard('{ArrowRight}');
30
+ const moved = within(lane('In progress')).getByRole('button', { name: /Rotate staging/ });
31
+ expect(moved).toHaveFocus();
32
+ expect(screen.getByText('Moved to In progress, position 1 of 3.')).toBeInTheDocument();
33
+ await userEvent.keyboard('{ArrowDown} ');
34
+ expect(onMove).toHaveBeenCalledWith('T-101', 'in-progress', 1);
35
+ expect(
36
+ screen.getByText(/Dropped Rotate staging database credentials in In progress, position 2 of 3/),
37
+ ).toBeInTheDocument();
38
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
39
+ });
40
+
41
+ it('Escape returns a picked-up card to where it was', { timeout: 15000 }, async () => {
42
+ render(<BoardPage />);
43
+ within(lane('In review'))
44
+ .getByRole('button', { name: /audit log export/ })
45
+ .focus();
46
+ await userEvent.keyboard(' {ArrowRight}{Escape}');
47
+ expect(within(lane('In review')).getByRole('button', { name: /audit log export/ })).toHaveFocus();
48
+ expect(screen.getByText(/Cancelled\./)).toBeInTheDocument();
49
+ });
50
+
51
+ it('drops a card dragged with the pointer and opens the detail drawer', { timeout: 15000 }, async () => {
52
+ const onMove = vi.fn();
53
+ render(<BoardPage onMove={onMove} />);
54
+ const card = within(lane('Backlog')).getByRole('button', { name: /invoices table/ });
55
+ fireEvent.dragStart(card);
56
+ fireEvent.dragOver(lane('Done'));
57
+ fireEvent.drop(lane('Done'));
58
+ expect(onMove).toHaveBeenCalledWith('T-103', 'done', 1);
59
+ await userEvent.click(within(lane('Done')).getByRole('button', { name: /invoices table/ }));
60
+ const drawer = await screen.findByRole('dialog', { name: 'Empty state for the invoices table' });
61
+ expect(within(drawer).getByRole('combobox', { name: 'Status' })).toHaveTextContent('Done');
62
+ await expectNoAxeViolations();
63
+ });
64
+
65
+ it('moveCard keeps every other card in order', () => {
66
+ expect(moveCard(boardState, 'T-104', 'backlog', 1).backlog).toEqual(['T-101', 'T-104', 'T-102', 'T-103']);
67
+ });
68
+ });
@@ -0,0 +1,178 @@
1
+ import { PageHeader } from '@gntik-ai/blocks';
2
+ import { Plus } from '@gntik-ai/icons';
3
+ import {
4
+ Avatar,
5
+ Badge,
6
+ Drawer,
7
+ DrawerBody,
8
+ DrawerContent,
9
+ DrawerDescription,
10
+ DrawerHeader,
11
+ DrawerTitle,
12
+ HStack,
13
+ Page,
14
+ SimpleSelect,
15
+ Stack,
16
+ Text,
17
+ Timestamp,
18
+ VisuallyHidden,
19
+ type BreadcrumbItem,
20
+ } from '@gntik-ai/ui';
21
+ import { useId, useState } from 'react';
22
+ import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
23
+ import {
24
+ boardBreadcrumbs,
25
+ boardCards,
26
+ boardColumns,
27
+ boardState,
28
+ locate,
29
+ priorityLabels,
30
+ priorityTones,
31
+ type BoardCard,
32
+ type BoardColumn,
33
+ type BoardState,
34
+ } from './data';
35
+ import { Lane } from './Lane';
36
+ import { useBoard } from './useBoard';
37
+
38
+ export interface BoardProps {
39
+ title: string;
40
+ description: string;
41
+ columns: BoardColumn[];
42
+ cards: BoardCard[];
43
+ /** Card ids per column id. */
44
+ defaultState: BoardState;
45
+ /** Fires after a drop (pointer, keyboard or the detail drawer's status select). */
46
+ onMove: (cardId: string, toColumn: string, toIndex: number) => void;
47
+ onCreate: () => void;
48
+ breadcrumbs: BreadcrumbItem[];
49
+ currentHref: string;
50
+ shell: Omit<ConsoleShellProps, 'children'>;
51
+ }
52
+
53
+ /** Kanban board: columns with pointer and keyboard drag-and-drop, live announcements and a card detail Drawer. */
54
+ export default function BoardPage(props: Partial<BoardProps>) {
55
+ const {
56
+ title = 'Sprint board',
57
+ description = 'Drag cards between columns, or focus a card and press Space to pick it up.',
58
+ columns = boardColumns,
59
+ cards = boardCards,
60
+ defaultState = boardState,
61
+ onMove,
62
+ onCreate,
63
+ breadcrumbs = boardBreadcrumbs,
64
+ currentHref = '/projects',
65
+ shell,
66
+ } = props;
67
+ const board = useBoard({ columns, cards, initial: defaultState, onMove });
68
+ const [openId, setOpenId] = useState<string | null>(null);
69
+ const ids = useId();
70
+ const byId = new Map(cards.map((c) => [c.id, c]));
71
+ const open = openId ? byId.get(openId) : undefined;
72
+ const openAt = openId ? locate(board.state, openId) : null;
73
+
74
+ return (
75
+ <ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
76
+ <Page
77
+ width="full"
78
+ header={
79
+ <PageHeader
80
+ breadcrumbs={null}
81
+ title={title}
82
+ description={description}
83
+ status=""
84
+ meta={[]}
85
+ tabs={null}
86
+ actions={[{ label: 'New card', icon: Plus, onClick: onCreate }]}
87
+ />
88
+ }
89
+ >
90
+ <VisuallyHidden id={`${ids}-help`}>
91
+ Press Space to pick up the card, arrow keys to move it, Space to drop and Escape to cancel. Enter opens the
92
+ details.
93
+ </VisuallyHidden>
94
+ <VisuallyHidden aria-live="assertive" aria-atomic="true">
95
+ {board.announcement}
96
+ </VisuallyHidden>
97
+ <div role="group" aria-label="Board" className="flex gap-4 overflow-x-auto pb-2">
98
+ {columns.map((col) => (
99
+ <Lane
100
+ key={col.id}
101
+ column={col}
102
+ cards={(board.state[col.id] ?? []).flatMap((id) => byId.get(id) ?? [])}
103
+ headingId={`${ids}-${col.id}`}
104
+ instructionsId={`${ids}-help`}
105
+ grabbedId={board.grabbedId}
106
+ draggingId={board.dragging}
107
+ isDropTarget={board.dropColumn === col.id}
108
+ register={board.register}
109
+ onOpen={setOpenId}
110
+ onCardKeyDown={board.onCardKeyDown}
111
+ onCardKeyUp={board.onCardKeyUp}
112
+ onDragStart={board.onDragStart}
113
+ onDragEnd={board.onDragEnd}
114
+ onDragOver={board.onColumnDragOver(col.id)}
115
+ onDrop={board.onColumnDrop(col.id)}
116
+ />
117
+ ))}
118
+ </div>
119
+ </Page>
120
+ <Drawer open={open != null} onOpenChange={(next) => !next && setOpenId(null)}>
121
+ <DrawerContent size="md">
122
+ {open && (
123
+ <>
124
+ <DrawerHeader>
125
+ <DrawerTitle>{open.title}</DrawerTitle>
126
+ <DrawerDescription>{open.id}</DrawerDescription>
127
+ </DrawerHeader>
128
+ <DrawerBody>
129
+ <Stack gap={5}>
130
+ <HStack gap={2} wrap>
131
+ <Badge tone={priorityTones[open.priority]}>{priorityLabels[open.priority]} priority</Badge>
132
+ {open.labels.map((l) => (
133
+ <Badge key={l} variant="outline">
134
+ {l}
135
+ </Badge>
136
+ ))}
137
+ </HStack>
138
+ <Text variant="supporting">{open.description}</Text>
139
+ <dl className="grid grid-cols-[7rem_1fr] items-center gap-x-4 gap-y-3 text-[13px]">
140
+ <dt className="text-muted-foreground">Status</dt>
141
+ <dd>
142
+ <SimpleSelect
143
+ aria-label="Status"
144
+ size="sm"
145
+ items={columns.map((c) => ({ value: c.id, label: c.title }))}
146
+ value={openAt?.column ?? null}
147
+ onValueChange={(v) => v && board.moveTo(open.id, v, board.state[v]?.length ?? 0)}
148
+ className="w-44"
149
+ />
150
+ </dd>
151
+ <dt className="text-muted-foreground">Assignee</dt>
152
+ <dd className="flex items-center gap-2 text-foreground">
153
+ <Avatar size="xs" name={open.assignee} aria-hidden />
154
+ {open.assignee}
155
+ </dd>
156
+ <dt className="text-muted-foreground">Due</dt>
157
+ <dd className="text-foreground">
158
+ {open.due ? (
159
+ <Timestamp
160
+ value={open.due}
161
+ format="absolute"
162
+ tooltip={false}
163
+ dateOptions={{ dateStyle: 'medium' }}
164
+ />
165
+ ) : (
166
+ 'No due date'
167
+ )}
168
+ </dd>
169
+ </dl>
170
+ </Stack>
171
+ </DrawerBody>
172
+ </>
173
+ )}
174
+ </DrawerContent>
175
+ </Drawer>
176
+ </ConsoleShell>
177
+ );
178
+ }
@@ -0,0 +1,127 @@
1
+ import type { BadgeProps, BreadcrumbItem } from '@gntik-ai/ui';
2
+
3
+ export interface BoardColumn {
4
+ id: string;
5
+ title: string;
6
+ }
7
+
8
+ export interface BoardCard {
9
+ id: string;
10
+ title: string;
11
+ description: string;
12
+ assignee: string;
13
+ labels: string[];
14
+ priority: 'low' | 'medium' | 'high';
15
+ /** ISO date. */
16
+ due?: string;
17
+ }
18
+
19
+ /** Card ids per column id, in display order. */
20
+ export type BoardState = Record<string, string[]>;
21
+
22
+ export const priorityTones: Record<BoardCard['priority'], NonNullable<BadgeProps['tone']>> = {
23
+ low: 'neutral',
24
+ medium: 'info',
25
+ high: 'warning',
26
+ };
27
+ export const priorityLabels: Record<BoardCard['priority'], string> = { low: 'Low', medium: 'Medium', high: 'High' };
28
+
29
+ export const boardBreadcrumbs: BreadcrumbItem[] = [
30
+ { label: 'Acme Industries', href: '/overview' },
31
+ { label: 'Projects', href: '/projects' },
32
+ { label: 'Board' },
33
+ ];
34
+
35
+ export const boardColumns: BoardColumn[] = [
36
+ { id: 'backlog', title: 'Backlog' },
37
+ { id: 'in-progress', title: 'In progress' },
38
+ { id: 'review', title: 'In review' },
39
+ { id: 'done', title: 'Done' },
40
+ ];
41
+
42
+ export const boardCards: BoardCard[] = [
43
+ {
44
+ id: 'T-101',
45
+ title: 'Rotate staging database credentials',
46
+ description: 'Move the staging credentials to the secrets store and rotate them.',
47
+ assignee: 'Ada Example',
48
+ labels: ['security'],
49
+ priority: 'high',
50
+ due: '2026-10-08',
51
+ },
52
+ {
53
+ id: 'T-102',
54
+ title: 'Add retry budget to webhooks',
55
+ description: 'Cap retries per endpoint per hour so one bad endpoint cannot flood the queue.',
56
+ assignee: 'Linus Park',
57
+ labels: ['backend'],
58
+ priority: 'medium',
59
+ },
60
+ {
61
+ id: 'T-103',
62
+ title: 'Empty state for the invoices table',
63
+ description: 'Explain why the table is empty on new workspaces and link to billing settings.',
64
+ assignee: 'Grace Hall',
65
+ labels: ['frontend', 'design'],
66
+ priority: 'low',
67
+ },
68
+ {
69
+ id: 'T-104',
70
+ title: 'Migrate usage exports to the new queue',
71
+ description: 'Large exports should run as background jobs and notify when ready.',
72
+ assignee: 'Ada Example',
73
+ labels: ['backend'],
74
+ priority: 'high',
75
+ due: '2026-10-15',
76
+ },
77
+ {
78
+ id: 'T-105',
79
+ title: 'Keyboard shortcuts for the deployments table',
80
+ description: 'J / K to move, Enter to open, X to select.',
81
+ assignee: 'Grace Hall',
82
+ labels: ['frontend', 'a11y'],
83
+ priority: 'medium',
84
+ },
85
+ {
86
+ id: 'T-106',
87
+ title: 'Document the audit log export format',
88
+ description: 'Fields, types and an example line for the JSON export.',
89
+ assignee: 'Linus Park',
90
+ labels: ['docs'],
91
+ priority: 'low',
92
+ },
93
+ {
94
+ id: 'T-107',
95
+ title: 'Region picker on project creation',
96
+ description: 'Default to the workspace region, warn about data residency.',
97
+ assignee: 'Ada Example',
98
+ labels: ['frontend'],
99
+ priority: 'medium',
100
+ },
101
+ ];
102
+
103
+ export const boardState: BoardState = {
104
+ backlog: ['T-101', 'T-102', 'T-103'],
105
+ 'in-progress': ['T-104', 'T-105'],
106
+ review: ['T-106'],
107
+ done: ['T-107'],
108
+ };
109
+
110
+ /** Moves a card to `toColumn` at `toIndex` (clamped); returns a new state. */
111
+ export function moveCard(state: BoardState, cardId: string, toColumn: string, toIndex: number): BoardState {
112
+ const next: BoardState = {};
113
+ for (const [col, ids] of Object.entries(state)) next[col] = ids.filter((id) => id !== cardId);
114
+ const target = next[toColumn] ?? [];
115
+ const index = Math.max(0, Math.min(toIndex, target.length));
116
+ next[toColumn] = [...target.slice(0, index), cardId, ...target.slice(index)];
117
+ return next;
118
+ }
119
+
120
+ /** Column id and index of a card. */
121
+ export function locate(state: BoardState, cardId: string): { column: string; index: number } | null {
122
+ for (const [column, ids] of Object.entries(state)) {
123
+ const index = ids.indexOf(cardId);
124
+ if (index >= 0) return { column, index };
125
+ }
126
+ return null;
127
+ }
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Board',
5
+ family: 'Resources',
6
+ priority: 'P3',
7
+ status: 'experimental',
8
+ description:
9
+ 'Kanban board in the console: columns with counts, cards with priority, labels, due date and assignee; pointer drag-and-drop between and within columns, keyboard moves (Space to pick up, arrows to move, Space to drop, Escape to cancel) with live announcements, and a card detail Drawer whose status select also moves the card.',
10
+ layout: 'SidebarLayout · Page',
11
+ blocks: ['app-sidebar', 'app-topbar', 'page-header'],
12
+ };
@@ -0,0 +1,146 @@
1
+ import { useRef, useState, type DragEvent, type KeyboardEvent } from 'react';
2
+ import { flushSync } from 'react-dom';
3
+ import { locate, moveCard, type BoardCard, type BoardColumn, type BoardState } from './data';
4
+
5
+ export interface UseBoardOptions {
6
+ columns: readonly BoardColumn[];
7
+ cards: readonly BoardCard[];
8
+ initial: BoardState;
9
+ onMove?: (cardId: string, toColumn: string, toIndex: number) => void;
10
+ }
11
+
12
+ interface Grab {
13
+ id: string;
14
+ origin: { column: string; index: number };
15
+ }
16
+
17
+ /**
18
+ * Board state with both drag paths: pointer (HTML drag and drop onto a column or before a card)
19
+ * and keyboard (Space picks up, arrows move, Space or Enter drops, Escape cancels). Every step is
20
+ * announced through `announcement`, and focus follows the moved card.
21
+ */
22
+ export function useBoard({ columns, cards, initial, onMove }: UseBoardOptions) {
23
+ const [state, setState] = useState<BoardState>(initial);
24
+ const [grab, setGrab] = useState<Grab | null>(null);
25
+ const [dragging, setDragging] = useState<string | null>(null);
26
+ const [dropColumn, setDropColumn] = useState<string | null>(null);
27
+ const [announcement, setAnnouncement] = useState('');
28
+ const buttons = useRef(new Map<string, HTMLButtonElement>());
29
+
30
+ const titleOf = (id: string) => cards.find((c) => c.id === id)?.title ?? id;
31
+ const columnTitle = (id: string) => columns.find((c) => c.id === id)?.title ?? id;
32
+ const where = (s: BoardState, column: string, index: number) =>
33
+ `${columnTitle(column)}, position ${index + 1} of ${s[column]?.length ?? 0}`;
34
+
35
+ const register = (id: string) => (el: HTMLButtonElement | null) => {
36
+ if (el) buttons.current.set(id, el);
37
+ else buttons.current.delete(id);
38
+ };
39
+
40
+ /** Moves synchronously so the remounted card can take focus right away. */
41
+ const commit = (id: string, column: string, index: number) => {
42
+ const next = moveCard(state, id, column, index);
43
+ flushSync(() => setState(next));
44
+ buttons.current.get(id)?.focus();
45
+ return next;
46
+ };
47
+
48
+ const moveTo = (id: string, column: string, index: number, announce = true) => {
49
+ const next = commit(id, column, index);
50
+ const at = locate(next, id);
51
+ if (at) {
52
+ if (announce) setAnnouncement(`Dropped ${titleOf(id)} in ${where(next, at.column, at.index)}.`);
53
+ onMove?.(id, at.column, at.index);
54
+ }
55
+ };
56
+
57
+ const onCardKeyDown = (id: string) => (e: KeyboardEvent<HTMLButtonElement>) => {
58
+ const at = locate(state, id);
59
+ if (!at) return;
60
+ const held = grab?.id === id;
61
+ if (e.key === ' ' || (held && e.key === 'Enter')) {
62
+ e.preventDefault();
63
+ if (held) {
64
+ setGrab(null);
65
+ setAnnouncement(`Dropped ${titleOf(id)} in ${where(state, at.column, at.index)}.`);
66
+ onMove?.(id, at.column, at.index);
67
+ } else {
68
+ setGrab({ id, origin: at });
69
+ setAnnouncement(
70
+ `Picked up ${titleOf(id)} in ${where(state, at.column, at.index)}. Use the arrow keys to move, Space to drop, Escape to cancel.`,
71
+ );
72
+ }
73
+ return;
74
+ }
75
+ if (!held) return;
76
+ const colIndex = columns.findIndex((c) => c.id === at.column);
77
+ let target: { column: string; index: number } | null = null;
78
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
79
+ const nextCol = columns[colIndex + (e.key === 'ArrowRight' ? 1 : -1)];
80
+ if (nextCol) target = { column: nextCol.id, index: Math.min(at.index, state[nextCol.id]?.length ?? 0) };
81
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
82
+ const index = at.index + (e.key === 'ArrowDown' ? 1 : -1);
83
+ if (index >= 0 && index < (state[at.column]?.length ?? 0)) target = { column: at.column, index };
84
+ } else if (e.key === 'Escape' && grab) {
85
+ e.preventDefault();
86
+ const next = commit(id, grab.origin.column, grab.origin.index);
87
+ setGrab(null);
88
+ setAnnouncement(`Cancelled. ${titleOf(id)} returned to ${where(next, grab.origin.column, grab.origin.index)}.`);
89
+ return;
90
+ }
91
+ if (!target) return;
92
+ e.preventDefault();
93
+ const next = commit(id, target.column, target.index);
94
+ const now = locate(next, id);
95
+ if (now) setAnnouncement(`Moved to ${where(next, now.column, now.index)}.`);
96
+ };
97
+
98
+ /** Space activates buttons on keyup; cancel it so picking up does not open the card. */
99
+ const onCardKeyUp = (e: KeyboardEvent<HTMLButtonElement>) => {
100
+ if (e.key === ' ') e.preventDefault();
101
+ };
102
+
103
+ const onDragStart = (id: string) => (e: DragEvent<HTMLElement>) => {
104
+ e.dataTransfer?.setData('text/plain', id);
105
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
106
+ setDragging(id);
107
+ };
108
+ const onDragEnd = () => {
109
+ setDragging(null);
110
+ setDropColumn(null);
111
+ };
112
+ const onColumnDragOver = (column: string) => (e: DragEvent<HTMLElement>) => {
113
+ if (!dragging) return;
114
+ e.preventDefault();
115
+ if (dropColumn !== column) setDropColumn(column);
116
+ };
117
+ const onColumnDrop = (column: string) => (e: DragEvent<HTMLElement>) => {
118
+ if (!dragging) return;
119
+ e.preventDefault();
120
+ const before = (e.target as HTMLElement).closest<HTMLElement>('[data-card-index]');
121
+ const from = locate(state, dragging);
122
+ let index = before ? Number(before.dataset.cardIndex) : (state[column]?.length ?? 0);
123
+ // Dropping before a later card of the same column: the card leaves its slot first.
124
+ if (from && from.column === column && from.index < index) index -= 1;
125
+ const id = dragging;
126
+ setDragging(null);
127
+ setDropColumn(null);
128
+ moveTo(id, column, index);
129
+ };
130
+
131
+ return {
132
+ state,
133
+ grabbedId: grab?.id ?? null,
134
+ dragging,
135
+ dropColumn,
136
+ announcement,
137
+ register,
138
+ moveTo,
139
+ onCardKeyDown,
140
+ onCardKeyUp,
141
+ onDragStart,
142
+ onDragEnd,
143
+ onColumnDragOver,
144
+ onColumnDrop,
145
+ };
146
+ }
@@ -0,0 +1,31 @@
1
+ import { render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import ChangelogPage from './Page';
5
+ import { changelogEntries } from './data';
6
+
7
+ describe('ChangelogPage', () => {
8
+ it('renders every release with its badges', { timeout: 15000 }, async () => {
9
+ render(<ChangelogPage />);
10
+ expect(screen.getByRole('main')).toBeInTheDocument();
11
+ expect(screen.getByRole('heading', { level: 1, name: 'Changelog' })).toBeInTheDocument();
12
+ const list = screen.getByRole('list', { name: 'Releases' });
13
+ expect(within(list).getAllByRole('article')).toHaveLength(changelogEntries.length);
14
+ expect(within(list).getAllByText('New')).toHaveLength(changelogEntries.filter((e) => e.isNew).length);
15
+ await expectNoAxeViolations();
16
+ });
17
+
18
+ it('filters by tag and resets', { timeout: 15000 }, async () => {
19
+ const onTagsChange = vi.fn();
20
+ render(<ChangelogPage onTagsChange={onTagsChange} />);
21
+ await userEvent.click(screen.getByRole('button', { name: 'Security' }));
22
+ expect(onTagsChange).toHaveBeenLastCalledWith(['security']);
23
+ const list = screen.getByRole('list', { name: 'Releases' });
24
+ expect(within(list).getAllByRole('article')).toHaveLength(
25
+ changelogEntries.filter((e) => e.tags.includes('security')).length,
26
+ );
27
+ expect(screen.getByText('2 releases')).toBeInTheDocument();
28
+ await userEvent.click(screen.getByRole('button', { name: 'Show all' }));
29
+ expect(screen.getByText(`${changelogEntries.length} releases`)).toBeInTheDocument();
30
+ });
31
+ });