@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,38 @@
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 ScorecardPage from './Page';
5
+ import { attainment, periodDelta, scorecardMetrics } from './data';
6
+
7
+ describe('ScorecardPage', () => {
8
+ it('renders KPIs, targets, radar and commentary', { timeout: 15000 }, async () => {
9
+ render(<ScorecardPage />);
10
+ expect(screen.getByRole('main')).toBeInTheDocument();
11
+ expect(screen.getByRole('heading', { level: 1, name: 'Quarterly scorecard' })).toBeInTheDocument();
12
+ expect(screen.getByText('Compared with Q2 2026')).toBeInTheDocument();
13
+ const table = screen.getByRole('table', { name: 'Metrics for Q3 2026 against target' });
14
+ expect(within(table).getAllByRole('meter')).toHaveLength(scorecardMetrics.length);
15
+ expect(screen.getByRole('complementary', { name: 'Commentary' })).toBeInTheDocument();
16
+ await expectNoAxeViolations();
17
+ });
18
+
19
+ it('switches period and posts a note', { timeout: 15000 }, async () => {
20
+ const onPeriodChange = vi.fn();
21
+ const onComment = vi.fn();
22
+ render(<ScorecardPage defaultPeriod="Q2 2026" onPeriodChange={onPeriodChange} onComment={onComment} />);
23
+ expect(screen.getByRole('table', { name: 'Metrics for Q2 2026 against target' })).toBeInTheDocument();
24
+ await userEvent.click(screen.getByRole('combobox', { name: 'Period' }));
25
+ await userEvent.click(await screen.findByRole('option', { name: 'Q1 2026' }));
26
+ expect(onPeriodChange).toHaveBeenCalledWith('Q1 2026');
27
+ expect(screen.getByText('Compared with Q4 2025')).toBeInTheDocument();
28
+ await userEvent.type(screen.getByRole('textbox'), 'Revenue on track.');
29
+ await userEvent.click(screen.getByRole('button', { name: 'Post note' }));
30
+ expect(onComment).toHaveBeenCalledWith('Revenue on track.');
31
+ });
32
+
33
+ it('flips sentiment for lower-is-better metrics', () => {
34
+ const latency = scorecardMetrics.find((m) => m.id === 'latency')!;
35
+ expect(periodDelta(latency, 176, 205)?.sentiment).toBe('positive');
36
+ expect(attainment(latency, 180)).toBe(100);
37
+ });
38
+ });
@@ -0,0 +1,160 @@
1
+ import { CommentThread, KpiRow, PageHeader, type CommentAuthor, type ThreadComment } from '@gntik-ai/blocks';
2
+ import { RadarChart } from '@gntik-ai/charts';
3
+ import { Download } from '@gntik-ai/icons';
4
+ import {
5
+ Card,
6
+ CardBody,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ Grid,
11
+ HStack,
12
+ Page,
13
+ Section,
14
+ SimpleSelect,
15
+ Stack,
16
+ Text,
17
+ type BreadcrumbItem,
18
+ } from '@gntik-ai/ui';
19
+ import { useState } from 'react';
20
+ import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
21
+ import {
22
+ periodKpis,
23
+ scorecardAuthor,
24
+ scorecardBreadcrumbs,
25
+ scorecardComments,
26
+ scorecardMetrics,
27
+ scorecardPeriods,
28
+ scorecardRadar,
29
+ type RadarPoint,
30
+ type ScorecardMetric,
31
+ } from './data';
32
+ import { TargetsTable } from './TargetsTable';
33
+
34
+ export interface ScorecardProps {
35
+ title: string;
36
+ description: string;
37
+ /** Period labels, oldest first; every metric series is aligned with them. */
38
+ periods: string[];
39
+ /** Initially selected period (defaults to the latest). */
40
+ defaultPeriod: string;
41
+ onPeriodChange: (period: string) => void;
42
+ metrics: ScorecardMetric[];
43
+ /** Dimension scores: `dimension` plus one key per period label. */
44
+ radar: RadarPoint[];
45
+ comments: ThreadComment[];
46
+ currentUser: CommentAuthor;
47
+ onComment: (body: string) => void;
48
+ onExport: (period: string) => void;
49
+ breadcrumbs: BreadcrumbItem[];
50
+ currentHref: string;
51
+ shell: Omit<ConsoleShellProps, 'children'>;
52
+ }
53
+
54
+ /** Scorecard: period-over-period KPIs, targets with attainment and trends, a radar of dimensions and a commentary rail. */
55
+ export default function ScorecardPage(props: Partial<ScorecardProps>) {
56
+ const {
57
+ title = 'Quarterly scorecard',
58
+ description = 'Company metrics against target, compared with the previous quarter.',
59
+ periods = scorecardPeriods,
60
+ defaultPeriod,
61
+ onPeriodChange,
62
+ metrics = scorecardMetrics,
63
+ radar = scorecardRadar,
64
+ comments = scorecardComments,
65
+ currentUser = scorecardAuthor,
66
+ onComment,
67
+ onExport,
68
+ breadcrumbs = scorecardBreadcrumbs,
69
+ currentHref = '/scorecard',
70
+ shell,
71
+ } = props;
72
+ const [period, setPeriod] = useState(defaultPeriod ?? periods[periods.length - 1] ?? '');
73
+ const index = Math.max(0, periods.indexOf(period));
74
+ const previous = periods[index - 1];
75
+ const compared = previous ? [period, previous] : [period];
76
+
77
+ return (
78
+ <ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
79
+ <Page
80
+ width="wide"
81
+ header={
82
+ <PageHeader
83
+ breadcrumbs={null}
84
+ title={title}
85
+ description={description}
86
+ status=""
87
+ meta={[]}
88
+ tabs={null}
89
+ actions={[{ label: 'Export PDF', icon: Download, variant: 'secondary', onClick: () => onExport?.(period) }]}
90
+ />
91
+ }
92
+ >
93
+ <Stack gap={6}>
94
+ <HStack gap={3} wrap>
95
+ <SimpleSelect
96
+ aria-label="Period"
97
+ items={periods.map((p) => ({ value: p, label: p }))}
98
+ value={period}
99
+ onValueChange={(v) => {
100
+ if (!v) return;
101
+ setPeriod(v);
102
+ onPeriodChange?.(v);
103
+ }}
104
+ className="w-40"
105
+ />
106
+ <Text as="span" variant="supporting" tone="muted" aria-live="polite">
107
+ {previous ? `Compared with ${previous}` : 'No earlier period to compare with'}
108
+ </Text>
109
+ </HStack>
110
+ <KpiRow items={periodKpis(metrics, periods, index)} label={`Key metrics, ${period}`} showTrends={false} />
111
+ <Grid cols={{ base: 1, xl: 3 }} gap={6} align="start">
112
+ <Stack gap={6} className="min-w-0 xl:col-span-2">
113
+ <Section
114
+ title="Targets"
115
+ description="Attainment against this period’s targets; trend shows every quarter up to the selected one."
116
+ >
117
+ <TargetsTable metrics={metrics} periods={periods} index={index} />
118
+ </Section>
119
+ <Card>
120
+ <CardHeader>
121
+ <CardTitle as="h2">Health by dimension</CardTitle>
122
+ <CardDescription>Scores out of 100{previous ? `, ${period} vs. ${previous}` : ''}.</CardDescription>
123
+ </CardHeader>
124
+ <CardBody>
125
+ <RadarChart<RadarPoint>
126
+ data={radar}
127
+ index="dimension"
128
+ categories={compared}
129
+ max={100}
130
+ height={300}
131
+ aria-label={`Health by dimension, ${compared.join(' vs. ')}`}
132
+ dataTable
133
+ />
134
+ </CardBody>
135
+ </Card>
136
+ </Stack>
137
+ <aside aria-label="Commentary" className="min-w-0">
138
+ <Card>
139
+ <CardHeader divided>
140
+ <CardTitle as="h2">Commentary</CardTitle>
141
+ <CardDescription>Notes from the review, newest last.</CardDescription>
142
+ </CardHeader>
143
+ <CardBody>
144
+ <CommentThread
145
+ comments={comments}
146
+ currentUser={currentUser}
147
+ onSubmit={onComment}
148
+ label="Scorecard commentary"
149
+ placeholder="Add a note for this period…"
150
+ submitLabel="Post note"
151
+ />
152
+ </CardBody>
153
+ </Card>
154
+ </aside>
155
+ </Grid>
156
+ </Stack>
157
+ </Page>
158
+ </ConsoleShell>
159
+ );
160
+ }
@@ -0,0 +1,92 @@
1
+ import { TrendDelta } from '@gntik-ai/blocks';
2
+ import { MiniBar } from '@gntik-ai/charts';
3
+ import { Meter, Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@gntik-ai/ui';
4
+ import { attainment, periodDelta, type ScorecardMetric } from './data';
5
+
6
+ /** Attainment never turns amber/red: for targets, a full bar is the good outcome. */
7
+ const NO_ALERT = { warning: 1000, destructive: 1000 };
8
+
9
+ export interface TargetsTableProps {
10
+ metrics: readonly ScorecardMetric[];
11
+ periods: readonly string[];
12
+ index: number;
13
+ }
14
+
15
+ /** Period-over-period metrics against their targets, with attainment and a trend per row. */
16
+ export function TargetsTable({ metrics, periods, index }: TargetsTableProps) {
17
+ const current = periods[index] ?? '';
18
+ const previous = periods[index - 1];
19
+ return (
20
+ <Table containerLabel="Targets">
21
+ <TableCaption srOnly>Metrics for {current} against target</TableCaption>
22
+ <TableHeader>
23
+ <TableRow>
24
+ <TableHead scope="col">Metric</TableHead>
25
+ <TableHead scope="col" align="right">
26
+ {current}
27
+ </TableHead>
28
+ <TableHead scope="col" align="right">
29
+ {previous ?? 'Previous'}
30
+ </TableHead>
31
+ <TableHead scope="col" align="right">
32
+ Change
33
+ </TableHead>
34
+ <TableHead scope="col" align="right">
35
+ Target
36
+ </TableHead>
37
+ <TableHead scope="col" className="w-40">
38
+ Attainment
39
+ </TableHead>
40
+ <TableHead scope="col">Trend</TableHead>
41
+ </TableRow>
42
+ </TableHeader>
43
+ <TableBody>
44
+ {metrics.map((m) => {
45
+ const value = m.series[index] ?? 0;
46
+ const prev = m.series[index - 1];
47
+ const delta = periodDelta(m, value, prev);
48
+ const att = attainment(m, value);
49
+ const series = m.series.slice(0, index + 1);
50
+ return (
51
+ <TableRow key={m.id}>
52
+ <TableHead scope="row" className="font-medium text-foreground">
53
+ {m.label}
54
+ </TableHead>
55
+ <TableCell align="right" className="font-mono tabular-nums text-foreground">
56
+ {m.format(value)}
57
+ </TableCell>
58
+ <TableCell align="right" className="font-mono tabular-nums text-muted-foreground">
59
+ {prev === undefined ? '—' : m.format(prev)}
60
+ </TableCell>
61
+ <TableCell align="right">{delta ? <TrendDelta {...delta} /> : '—'}</TableCell>
62
+ <TableCell align="right" className="font-mono tabular-nums text-muted-foreground">
63
+ {m.lowerIsBetter ? '≤ ' : ''}
64
+ {m.format(m.target)}
65
+ </TableCell>
66
+ <TableCell>
67
+ <Meter
68
+ size="sm"
69
+ value={Math.min(att, 100)}
70
+ aria-label={`${m.label} attainment`}
71
+ aria-valuetext={`${att}% of target`}
72
+ valueLabel={`${att}%`}
73
+ thresholds={NO_ALERT}
74
+ />
75
+ </TableCell>
76
+ <TableCell>
77
+ <MiniBar
78
+ data={series}
79
+ labels={periods.slice(0, index + 1)}
80
+ formatValue={m.format}
81
+ label={`${m.label} by quarter`}
82
+ width={88}
83
+ height={24}
84
+ />
85
+ </TableCell>
86
+ </TableRow>
87
+ );
88
+ })}
89
+ </TableBody>
90
+ </Table>
91
+ );
92
+ }
@@ -0,0 +1,118 @@
1
+ import type { CommentAuthor, KpiDelta, KpiItem, ThreadComment } from '@gntik-ai/blocks';
2
+ import type { BreadcrumbItem } from '@gntik-ai/ui';
3
+
4
+ export interface ScorecardMetric {
5
+ id: string;
6
+ label: string;
7
+ /** One value per period, aligned with `periods`. */
8
+ series: number[];
9
+ target: number;
10
+ /** Lower is better (latency, churn): flips the delta sentiment and the attainment. */
11
+ lowerIsBetter?: boolean;
12
+ format: (value: number) => string;
13
+ }
14
+
15
+ export interface RadarPoint {
16
+ dimension: string;
17
+ [period: string]: number | string;
18
+ }
19
+
20
+ const pct = (v: number) => `${v.toFixed(1)}%`;
21
+ const ms = (v: number) => `${Math.round(v)} ms`;
22
+ const money = (v: number) => `$${(v / 1000).toFixed(0)}k`;
23
+ const int = (v: number) => v.toLocaleString('en-US');
24
+
25
+ export const scorecardPeriods = ['Q4 2025', 'Q1 2026', 'Q2 2026', 'Q3 2026'];
26
+
27
+ export const scorecardMetrics: ScorecardMetric[] = [
28
+ {
29
+ id: 'revenue',
30
+ label: 'Recurring revenue',
31
+ series: [412_000, 438_000, 471_000, 503_000],
32
+ target: 520_000,
33
+ format: money,
34
+ },
35
+ { id: 'active', label: 'Active workspaces', series: [1_840, 1_910, 2_064, 2_190], target: 2_100, format: int },
36
+ {
37
+ id: 'uptime',
38
+ label: 'Uptime',
39
+ series: [99.92, 99.95, 99.87, 99.97],
40
+ target: 99.95,
41
+ format: (v) => `${v.toFixed(2)}%`,
42
+ },
43
+ { id: 'latency', label: 'p95 latency', series: [212, 198, 205, 176], target: 180, lowerIsBetter: true, format: ms },
44
+ { id: 'churn', label: 'Logo churn', series: [2.4, 2.1, 2.6, 1.9], target: 2.0, lowerIsBetter: true, format: pct },
45
+ { id: 'nps', label: 'Satisfaction (NPS)', series: [38, 41, 40, 46], target: 45, format: int },
46
+ ];
47
+
48
+ /** Dimension scores (0–100) per period. */
49
+ export const scorecardRadar: RadarPoint[] = [
50
+ { dimension: 'Growth', 'Q4 2025': 62, 'Q1 2026': 66, 'Q2 2026': 71, 'Q3 2026': 78 },
51
+ { dimension: 'Reliability', 'Q4 2025': 80, 'Q1 2026': 84, 'Q2 2026': 76, 'Q3 2026': 88 },
52
+ { dimension: 'Performance', 'Q4 2025': 70, 'Q1 2026': 74, 'Q2 2026': 72, 'Q3 2026': 83 },
53
+ { dimension: 'Retention', 'Q4 2025': 68, 'Q1 2026': 72, 'Q2 2026': 65, 'Q3 2026': 76 },
54
+ { dimension: 'Satisfaction', 'Q4 2025': 60, 'Q1 2026': 64, 'Q2 2026': 63, 'Q3 2026': 71 },
55
+ ];
56
+
57
+ export const scorecardBreadcrumbs: BreadcrumbItem[] = [
58
+ { label: 'Acme Industries', href: '/overview' },
59
+ { label: 'Scorecard' },
60
+ ];
61
+
62
+ export const scorecardAuthor: CommentAuthor = { name: 'Jordan Lee' };
63
+
64
+ const DAY = 86_400_000;
65
+ export const scorecardComments: ThreadComment[] = [
66
+ {
67
+ id: 'c1',
68
+ author: { name: 'Priya Natarajan' },
69
+ body: 'Latency is finally under target after the cache rollout in August. Keep an eye on the EU region.',
70
+ at: Date.now() - 3 * DAY,
71
+ },
72
+ {
73
+ id: 'c2',
74
+ author: { name: 'Marcus Chen' },
75
+ body: 'Revenue is 3% short of target; two enterprise renewals slipped into October.',
76
+ at: Date.now() - 2 * DAY,
77
+ },
78
+ {
79
+ id: 'c3',
80
+ author: { name: 'Ana Souza' },
81
+ body: 'Churn improved with the new onboarding checklist. Proposing we raise the NPS target to 50 for Q4.',
82
+ at: Date.now() - DAY / 2,
83
+ },
84
+ ];
85
+
86
+ /** Change between two values, with sentiment following `lowerIsBetter`. */
87
+ export function periodDelta(m: ScorecardMetric, current: number, previous: number | undefined): KpiDelta | undefined {
88
+ if (previous === undefined || previous === 0) return undefined;
89
+ const change = ((current - previous) / previous) * 100;
90
+ const direction = Math.abs(change) < 0.05 ? 'flat' : change > 0 ? 'up' : 'down';
91
+ const good = m.lowerIsBetter ? change < 0 : change > 0;
92
+ return {
93
+ value: `${change > 0 ? '+' : ''}${change.toFixed(1)}%`,
94
+ direction,
95
+ sentiment: direction === 'flat' ? 'neutral' : good ? 'positive' : 'negative',
96
+ };
97
+ }
98
+
99
+ /** Attainment of the target in percent (capped at 100 for the meter). */
100
+ export function attainment(m: ScorecardMetric, value: number): number {
101
+ const ratio = m.lowerIsBetter ? m.target / value : value / m.target;
102
+ return Math.round(ratio * 1000) / 10;
103
+ }
104
+
105
+ /** KpiRow items for a period, compared with the one before it. */
106
+ export function periodKpis(metrics: readonly ScorecardMetric[], periods: readonly string[], index: number): KpiItem[] {
107
+ const prevLabel = periods[index - 1];
108
+ return metrics.slice(0, 4).map((m) => {
109
+ const current = m.series[index] ?? 0;
110
+ const delta = periodDelta(m, current, m.series[index - 1]);
111
+ return {
112
+ id: m.id,
113
+ label: m.label,
114
+ value: m.format(current),
115
+ delta: delta && prevLabel ? { ...delta, note: `vs. ${prevLabel}` } : undefined,
116
+ };
117
+ });
118
+ }
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Scorecard',
5
+ family: 'Overview',
6
+ priority: 'P3',
7
+ status: 'experimental',
8
+ description:
9
+ 'Period-over-period scorecard: period select, KpiRow with deltas vs. the previous period, a targets table (change, target, attainment Meter, MiniBar trend), a RadarChart of dimension scores and a commentary rail (CommentThread).',
10
+ layout: 'SidebarLayout · Page',
11
+ blocks: ['app-sidebar', 'app-topbar', 'page-header', 'kpi-row', 'comment-thread'],
12
+ };
@@ -0,0 +1,43 @@
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 SearchResultsPage from './Page';
5
+
6
+ describe('SearchResultsPage', () => {
7
+ it('renders facets and grouped results', { timeout: 15000 }, async () => {
8
+ render(<SearchResultsPage />);
9
+ expect(screen.getByRole('main')).toBeInTheDocument();
10
+ expect(screen.getByRole('heading', { level: 1, name: 'Search' })).toBeInTheDocument();
11
+ expect(screen.getByRole('complementary', { name: 'Filters' })).toBeInTheDocument();
12
+ for (const name of ['Projects', 'Deployments', 'Members', 'Documents']) {
13
+ expect(screen.getByRole('region', { name })).toBeInTheDocument();
14
+ }
15
+ await expectNoAxeViolations();
16
+ });
17
+
18
+ it('moves through results with the arrow keys', { timeout: 15000 }, async () => {
19
+ render(<SearchResultsPage />);
20
+ await userEvent.click(screen.getByRole('searchbox', { name: 'Search the workspace' }));
21
+ await userEvent.keyboard('{ArrowDown}');
22
+ expect(screen.getByRole('link', { name: 'billing-api' })).toHaveFocus();
23
+ await userEvent.keyboard('{ArrowDown}{ArrowDown}');
24
+ expect(screen.getByRole('link', { name: 'orders-api' })).toHaveFocus();
25
+ await userEvent.keyboard('{End}');
26
+ expect(screen.getByRole('link', { name: 'Usage-based billing guide' })).toHaveFocus();
27
+ });
28
+
29
+ it('filters with a facet and shows NoResultsEmpty', { timeout: 15000 }, async () => {
30
+ render(<SearchResultsPage />);
31
+ const facets = screen.getByRole('complementary', { name: 'Filters' });
32
+ await userEvent.click(within(facets).getByRole('checkbox', { name: /Members/ }));
33
+ expect(screen.queryByRole('region', { name: 'Projects' })).not.toBeInTheDocument();
34
+ expect(screen.getByRole('link', { name: 'Riley Morgan' })).toBeInTheDocument();
35
+
36
+ const box = screen.getByRole('searchbox', { name: 'Search the workspace' });
37
+ await userEvent.clear(box);
38
+ await userEvent.type(box, 'zzz');
39
+ expect(screen.getByRole('heading', { level: 2, name: /No results match/ })).toBeInTheDocument();
40
+ await userEvent.click(screen.getByRole('button', { name: 'Clear filters' }));
41
+ expect(within(facets).getByRole('checkbox', { name: /Members/ })).not.toBeChecked();
42
+ });
43
+ });