@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
@@ -0,0 +1,36 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+ import { cleanup } from '@testing-library/react';
3
+ import { afterEach } from 'vitest';
4
+
5
+ afterEach(() => {
6
+ cleanup();
7
+ document.documentElement.className = '';
8
+ try {
9
+ localStorage.clear();
10
+ } catch {}
11
+ });
12
+
13
+ // jsdom gaps that Base UI touches (positioning, animations, media queries).
14
+ if (!window.matchMedia) {
15
+ window.matchMedia = (query: string) =>
16
+ ({
17
+ matches: false,
18
+ media: query,
19
+ onchange: null,
20
+ addListener: () => {},
21
+ removeListener: () => {},
22
+ addEventListener: () => {},
23
+ removeEventListener: () => {},
24
+ dispatchEvent: () => false,
25
+ }) as MediaQueryList;
26
+ }
27
+ if (!('ResizeObserver' in window)) {
28
+ (window as unknown as { ResizeObserver: unknown }).ResizeObserver = class {
29
+ observe() {}
30
+ unobserve() {}
31
+ disconnect() {}
32
+ };
33
+ }
34
+ if (!Element.prototype.getAnimations) {
35
+ Element.prototype.getAnimations = () => [];
36
+ }
@@ -0,0 +1,28 @@
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 UpgradeRequiredPage from './Page';
5
+
6
+ describe('UpgradeRequiredPage', () => {
7
+ it('renders the gate inside the console with the plan comparison', { timeout: 15000 }, async () => {
8
+ render(<UpgradeRequiredPage />);
9
+ expect(screen.getByRole('main')).toBeInTheDocument();
10
+ expect(screen.getByRole('heading', { level: 1, name: 'Audit log is available on Team' })).toBeInTheDocument();
11
+ expect(screen.getByRole('heading', { level: 2, name: 'Compare plans' })).toBeInTheDocument();
12
+ expect(screen.getByText('Your workspace is on the Starter plan')).toBeInTheDocument();
13
+ await expectNoAxeViolations();
14
+ });
15
+
16
+ it('starts the upgrade from a tier CTA, ignoring the current plan', { timeout: 15000 }, async () => {
17
+ const onUpgrade = vi.fn();
18
+ render(<UpgradeRequiredPage onUpgrade={onUpgrade} />);
19
+ const main = screen.getByRole('main');
20
+ const current = within(main).getByRole('button', { name: /Current plan/ });
21
+ expect(current).toBeDisabled();
22
+ expect(within(main).getByRole('listitem', { name: 'Starter' })).toHaveAttribute('aria-current', 'true');
23
+ await userEvent.click(current);
24
+ expect(onUpgrade).not.toHaveBeenCalled();
25
+ await userEvent.click(within(main).getByRole('button', { name: /Start free trial/ }));
26
+ expect(onUpgrade).toHaveBeenCalledWith(expect.objectContaining({ id: 'team' }), 'monthly');
27
+ });
28
+ });
@@ -0,0 +1,79 @@
1
+ import { InlineCallout, PageHeader, PricingTable, type PricingCycle, type PricingTier } from '@gntik-ai/blocks';
2
+ import { MessageSquare } from '@gntik-ai/icons';
3
+ import { Page, Stack, type BreadcrumbItem } from '@gntik-ai/ui';
4
+ import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
5
+ import { upgradeBreadcrumbs, upgradeContent, upgradeTiers } from './data';
6
+
7
+ export interface UpgradeRequiredProps {
8
+ /** The locked feature, e.g. "Audit log". */
9
+ feature: string;
10
+ /** Lowest plan that includes it. */
11
+ requiredPlan: string;
12
+ currentPlan: string;
13
+ description: string;
14
+ tiers: PricingTier[];
15
+ /** Id of the tier the workspace is on (badge + disabled "Current plan" CTA). */
16
+ currentTierId: string;
17
+ /** Called with the chosen tier and billing cycle. */
18
+ onUpgrade: (tier: PricingTier, cycle: PricingCycle) => void;
19
+ onContactSales: () => void;
20
+ breadcrumbs: BreadcrumbItem[];
21
+ currentHref: string;
22
+ /** ConsoleShell props (app nav, user, workspaces, sidebar footer, topbar…). */
23
+ shell: Omit<ConsoleShellProps, 'children'>;
24
+ }
25
+
26
+ /** Plan gate: ConsoleShell + Page with PageHeader, a current-plan callout and a PricingTable. */
27
+ export default function UpgradeRequiredPage(props: Partial<UpgradeRequiredProps>) {
28
+ const {
29
+ feature = upgradeContent.feature,
30
+ requiredPlan = upgradeContent.requiredPlan,
31
+ currentPlan = upgradeContent.currentPlan,
32
+ description = upgradeContent.description,
33
+ tiers = upgradeTiers,
34
+ currentTierId = 'starter',
35
+ onUpgrade,
36
+ onContactSales,
37
+ breadcrumbs = upgradeBreadcrumbs,
38
+ currentHref = '/settings',
39
+ shell,
40
+ } = props;
41
+ return (
42
+ <ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
43
+ <Page
44
+ width="wide"
45
+ header={
46
+ <PageHeader
47
+ breadcrumbs={null}
48
+ title={`${feature} is available on ${requiredPlan}`}
49
+ description={description}
50
+ status=""
51
+ meta={[]}
52
+ tabs={null}
53
+ actions={[{ label: 'Contact sales', icon: MessageSquare, variant: 'secondary', onClick: onContactSales }]}
54
+ />
55
+ }
56
+ >
57
+ <Stack gap={2}>
58
+ <InlineCallout
59
+ tone="info"
60
+ title={`Your workspace is on the ${currentPlan} plan`}
61
+ description={`Upgrade to ${requiredPlan} or above to turn on ${feature.toLowerCase()}. Your data and settings carry over.`}
62
+ actions={[]}
63
+ />
64
+ <PricingTable
65
+ className="px-0 py-8 sm:px-0 sm:py-10"
66
+ eyebrow={null}
67
+ title="Compare plans"
68
+ description="Switch at any time. Upgrades apply immediately and are prorated."
69
+ tiers={tiers}
70
+ currentTierId={currentTierId}
71
+ onSelectTier={(tier, cycle) => {
72
+ if (tier.id !== currentTierId) onUpgrade?.(tier, cycle);
73
+ }}
74
+ />
75
+ </Stack>
76
+ </Page>
77
+ </ConsoleShell>
78
+ );
79
+ }
@@ -0,0 +1,19 @@
1
+ import { samplePricingTiers, type PricingTier } from '@gntik-ai/blocks';
2
+ import type { BreadcrumbItem } from '@gntik-ai/ui';
3
+
4
+ /** Neutral fixtures for the plan gate. */
5
+ export const upgradeContent = {
6
+ feature: 'Audit log',
7
+ requiredPlan: 'Team',
8
+ currentPlan: 'Starter',
9
+ description: 'Keep a searchable record of every change in your workspace: who did what, when, and what changed.',
10
+ };
11
+
12
+ export const upgradeBreadcrumbs: BreadcrumbItem[] = [
13
+ { label: 'Acme Industries', href: '/overview' },
14
+ { label: 'Settings', href: '/settings' },
15
+ { label: 'Audit log' },
16
+ ];
17
+
18
+ /** The sample tiers; PricingTable marks the current one via `currentTierId`. */
19
+ export const upgradeTiers: PricingTier[] = samplePricingTiers;
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Upgrade required',
5
+ family: 'System',
6
+ priority: 'P2',
7
+ status: 'beta',
8
+ description:
9
+ 'Plan gate inside the console: what the locked feature does, the current plan, and a PricingTable comparison whose CTA starts the upgrade.',
10
+ layout: 'SidebarLayout · Page',
11
+ blocks: ['app-sidebar', 'app-topbar', 'page-header', 'inline-callout', 'pricing-table'],
12
+ };
@@ -0,0 +1,83 @@
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 UsageAnalyticsPage from './Page';
5
+ import { filterUsage, usageRows, type UsageRow } from './data';
6
+ import type { DataTableColumn } from '@gntik-ai/blocks';
7
+
8
+ describe('UsageAnalyticsPage', () => {
9
+ it('renders the header, filters, charts and breakdown', { timeout: 15000 }, async () => {
10
+ const { container } = render(<UsageAnalyticsPage />);
11
+ expect(screen.getByRole('main')).toBeInTheDocument();
12
+ expect(screen.getByRole('heading', { level: 1, name: 'Usage analytics' })).toBeInTheDocument();
13
+ expect(screen.getByRole('search', { name: 'Filters' })).toBeInTheDocument();
14
+ expect(screen.getByRole('heading', { level: 2, name: 'Requests by environment' })).toBeInTheDocument();
15
+ expect(screen.getByRole('heading', { level: 2, name: 'Infrastructure cost' })).toBeInTheDocument();
16
+ expect(screen.getByRole('table', { name: 'Usage by project' })).toBeInTheDocument();
17
+ await expectNoAxeViolations(container);
18
+ });
19
+
20
+ it('filters the breakdown by search and exports what is shown', { timeout: 15000 }, async () => {
21
+ const onExport = vi.fn();
22
+ render(<UsageAnalyticsPage onExport={onExport} />);
23
+ await userEvent.type(screen.getByRole('searchbox', { name: 'Search' }), 'webhooks');
24
+ const table = screen.getByRole('table', { name: 'Usage by project' });
25
+ const expected = usageRows.filter((r) => r.project === 'webhooks').length;
26
+ expect(within(table).getAllByRole('row')).toHaveLength(expected + 1);
27
+ await userEvent.click(screen.getAllByRole('button', { name: 'Export CSV' })[0]!);
28
+ expect(onExport).toHaveBeenCalledWith(expect.objectContaining({ query: 'webhooks' }));
29
+ expect(onExport.mock.calls[0]?.[0].rows).toHaveLength(expected);
30
+ });
31
+
32
+ it('shows the no-results state when nothing matches', { timeout: 15000 }, async () => {
33
+ render(<UsageAnalyticsPage />);
34
+ await userEvent.type(screen.getByRole('searchbox', { name: 'Search' }), 'zzz');
35
+ expect(screen.getByText(/No projects match/)).toBeInTheDocument();
36
+ });
37
+
38
+ it('takes data-driven charts, columns, filter fields and a search placeholder', { timeout: 15000 }, async () => {
39
+ type Point = { week: string; Calls: number };
40
+ const rows: UsageRow[] = [
41
+ { id: 'a', project: 'tenant-a', environment: 'dev', region: 'local', requests: 10, errorRate: 0, cost: 1 },
42
+ { id: 'b', project: 'tenant-b', environment: 'prod', region: 'local', requests: 20, errorRate: 0, cost: 2 },
43
+ ];
44
+ const columns: DataTableColumn<UsageRow>[] = [
45
+ { id: 'project', header: 'Tenant', accessor: (r) => r.project },
46
+ { id: 'environment', header: 'Stage', accessor: (r) => r.environment, variant: 'mono' },
47
+ ];
48
+ render(
49
+ <UsageAnalyticsPage<Point, Point>
50
+ rows={rows}
51
+ columns={columns}
52
+ filterFields={[{ id: 'environment', label: 'Stage', options: ['dev', 'prod'] }]}
53
+ searchPlaceholder="Search tenants…"
54
+ primaryChart={{
55
+ title: 'Function calls',
56
+ ranges: [{ value: 'w', label: 'Weekly' }],
57
+ dataByRange: { w: [{ week: 'W1', Calls: 3 }] },
58
+ index: 'week',
59
+ categories: ['Calls'],
60
+ }}
61
+ trafficRanges={[{ value: 'w', label: 'Weekly' }]}
62
+ trafficByRange={{ w: [{ week: 'W1', Calls: 5 }] }}
63
+ secondaryChart={{ title: 'Calls by stage', index: 'week', categories: ['Calls'] }}
64
+ />,
65
+ );
66
+ expect(screen.getByRole('heading', { level: 2, name: 'Function calls' })).toBeInTheDocument();
67
+ expect(screen.getByRole('heading', { level: 2, name: 'Calls by stage' })).toBeInTheDocument();
68
+ expect(screen.getByPlaceholderText('Search tenants…')).toBeInTheDocument();
69
+ const table = screen.getByRole('table', { name: 'Usage by project' });
70
+ expect(within(table).getByRole('columnheader', { name: /Stage/ })).toBeInTheDocument();
71
+ expect(within(table).getByText('prod')).toBeInTheDocument();
72
+ });
73
+
74
+ it('filters on any row field named by the filter id', () => {
75
+ const rows = usageRows.slice(0, 6);
76
+ const region = rows[0]!.region;
77
+ expect(filterUsage(rows, '', [{ field: 'region', value: region }]).every((r) => r.region === region)).toBe(true);
78
+ expect(filterUsage(rows, '', [{ field: 'project', value: rows[1]!.project }]).map((r) => r.project)).toEqual(
79
+ rows.filter((r) => r.project === rows[1]!.project).map((r) => r.project),
80
+ );
81
+ expect(filterUsage(rows, '', [{ field: 'unknown', value: 'x' }])).toHaveLength(rows.length);
82
+ });
83
+ });
@@ -0,0 +1,184 @@
1
+ import {
2
+ ChartCard,
3
+ DataTable,
4
+ DateRangeFilter,
5
+ FilterBar,
6
+ NoResultsEmpty,
7
+ PageHeader,
8
+ type ActiveFilter,
9
+ type ChartCardProps,
10
+ type ChartCardRange,
11
+ type CostPoint,
12
+ type DataTableColumn,
13
+ type FilterField,
14
+ type SavedView,
15
+ } from '@gntik-ai/blocks';
16
+ import { chartFmt } from '@gntik-ai/charts';
17
+ import { Download } from '@gntik-ai/icons';
18
+ import { Grid, HStack, Page, Section, Stack, type BreadcrumbItem, type DateRange } from '@gntik-ai/ui';
19
+ import { useState } from 'react';
20
+ import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
21
+ import {
22
+ filterUsage,
23
+ trafficByRange,
24
+ trafficRanges,
25
+ usageBreadcrumbs,
26
+ usageColumns,
27
+ usageFilterFields,
28
+ usageRows,
29
+ usageSavedViews,
30
+ type TrafficPoint,
31
+ type UsageRow,
32
+ } from './data';
33
+
34
+ export interface UsageExportRequest {
35
+ range: DateRange | null;
36
+ query: string;
37
+ filters: ActiveFilter[];
38
+ /** Rows currently shown in the breakdown. */
39
+ rows: UsageRow[];
40
+ }
41
+
42
+ /** Second-chart settings (everything but its data and ranges, which have their own props). */
43
+ export type UsageSecondaryChart<T extends object> = Omit<ChartCardProps<T>, 'dataByRange' | 'ranges'>;
44
+
45
+ /**
46
+ * `P` is the row type of the first chart (default: the infrastructure-cost fixture), `T` the row
47
+ * type of the second one (default: requests per environment and day).
48
+ */
49
+ export interface UsageAnalyticsProps<P extends object = CostPoint, T extends object = TrafficPoint> {
50
+ title: string;
51
+ description: string;
52
+ breadcrumbs: BreadcrumbItem[];
53
+ rows: readonly UsageRow[];
54
+ /** Breakdown table columns (default: project, environment, region, requests, error rate, cost). */
55
+ columns: DataTableColumn<UsageRow>[];
56
+ /** FilterBar fields; each id is matched against the row field of the same name. */
57
+ filterFields: FilterField[];
58
+ searchPlaceholder: string;
59
+ savedViews: readonly SavedView[];
60
+ /**
61
+ * First chart: any ChartCard props (title, description, ranges, dataByRange, index, categories,
62
+ * colors, kind, valueFormatter…). Unset fields keep the infrastructure-cost fixture.
63
+ */
64
+ primaryChart: ChartCardProps<P>;
65
+ /** Data of the second chart, per range key. */
66
+ trafficByRange: Readonly<Record<string, readonly T[]>>;
67
+ trafficRanges: readonly ChartCardRange[];
68
+ /** Second chart: title, description, index, categories, colors, kind… (merged over the traffic defaults). */
69
+ secondaryChart: UsageSecondaryChart<T>;
70
+ /** Fires when the date range changes (server-side products refetch here). */
71
+ onRangeChange: (range: DateRange) => void;
72
+ onExport: (request: UsageExportRequest) => void;
73
+ shell: Omit<ConsoleShellProps, 'children'>;
74
+ }
75
+
76
+ const TRAFFIC_CHART: UsageSecondaryChart<TrafficPoint> = {
77
+ title: 'Requests by environment',
78
+ description: 'Daily requests',
79
+ index: 'day',
80
+ categories: ['Production', 'Staging', 'Preview'],
81
+ kind: 'bar',
82
+ valueFormatter: chartFmt.compact,
83
+ };
84
+
85
+ /** Usage analytics: date range + filters, two charts and a sortable breakdown table with export. */
86
+ export default function UsageAnalyticsPage<P extends object = CostPoint, T extends object = TrafficPoint>({
87
+ title = 'Usage analytics',
88
+ description = 'Traffic, errors and spend by project and environment.',
89
+ breadcrumbs = usageBreadcrumbs,
90
+ rows = usageRows,
91
+ columns = usageColumns,
92
+ filterFields = usageFilterFields,
93
+ searchPlaceholder = 'Search projects…',
94
+ savedViews = usageSavedViews,
95
+ primaryChart,
96
+ trafficByRange: traffic = trafficByRange as unknown as Readonly<Record<string, readonly T[]>>,
97
+ trafficRanges: ranges = trafficRanges,
98
+ secondaryChart,
99
+ onRangeChange,
100
+ onExport,
101
+ shell,
102
+ }: Partial<UsageAnalyticsProps<P, T>>) {
103
+ const secondary = { ...(TRAFFIC_CHART as unknown as UsageSecondaryChart<T>), ...secondaryChart };
104
+ const [range, setRange] = useState<DateRange | null>(null);
105
+ const [query, setQuery] = useState('');
106
+ const [filters, setFilters] = useState<ActiveFilter[]>([]);
107
+ const visible = filterUsage(rows, query, filters);
108
+
109
+ const clearFilters = () => setFilters([]);
110
+ const exportAction = {
111
+ label: 'Export CSV',
112
+ icon: Download,
113
+ variant: 'secondary' as const,
114
+ onClick: () => onExport?.({ range, query, filters, rows: visible }),
115
+ };
116
+
117
+ return (
118
+ <ConsoleShell currentHref="/analytics" breadcrumbs={breadcrumbs} {...shell}>
119
+ <Page
120
+ width="wide"
121
+ header={
122
+ <PageHeader
123
+ breadcrumbs={null}
124
+ title={title}
125
+ description={description}
126
+ status=""
127
+ meta={[]}
128
+ tabs={null}
129
+ actions={[exportAction]}
130
+ />
131
+ }
132
+ >
133
+ <Stack gap={6}>
134
+ <HStack align="start" gap={3} wrap>
135
+ <DateRangeFilter
136
+ onValueChange={(next) => {
137
+ setRange(next);
138
+ onRangeChange?.(next);
139
+ }}
140
+ />
141
+ <FilterBar
142
+ className="min-w-0 flex-1"
143
+ fields={filterFields}
144
+ query={query}
145
+ onQueryChange={setQuery}
146
+ filters={filters}
147
+ onFiltersChange={setFilters}
148
+ defaultFilters={[]}
149
+ views={savedViews}
150
+ placeholder={searchPlaceholder}
151
+ resultCount={visible.length}
152
+ />
153
+ </HStack>
154
+ <Grid cols={{ base: 1, lg: 2 }} gap={6}>
155
+ <ChartCard<P> titleAs="h2" {...primaryChart} />
156
+ <ChartCard<T> titleAs="h2" {...secondary} ranges={ranges} dataByRange={traffic} />
157
+ </Grid>
158
+ <Section title="Breakdown" description="Per project and environment. Sort by any column.">
159
+ <DataTable<UsageRow>
160
+ rows={visible}
161
+ columns={columns}
162
+ getRowLabel={(r) => `${r.project} (${r.environment})`}
163
+ caption="Usage by project"
164
+ selectable={false}
165
+ showDensityToggle
166
+ showColumnMenu
167
+ defaultSort={{ columnId: 'cost', direction: 'descending' }}
168
+ emptyState={
169
+ <NoResultsEmpty
170
+ entity="projects"
171
+ query={query}
172
+ filterCount={filters.length}
173
+ onClearFilters={clearFilters}
174
+ onClearSearch={() => setQuery('')}
175
+ titleAs="h3"
176
+ />
177
+ }
178
+ />
179
+ </Section>
180
+ </Stack>
181
+ </Page>
182
+ </ConsoleShell>
183
+ );
184
+ }
@@ -0,0 +1,161 @@
1
+ import type { ActiveFilter, ChartCardRange, DataTableColumn, FilterField, SavedView } from '@gntik-ai/blocks';
2
+ import type { BreadcrumbItem } from '@gntik-ai/ui';
3
+
4
+ /** One row of the usage breakdown table. */
5
+ export interface UsageRow {
6
+ id: string;
7
+ project: string;
8
+ /** Environment name, e.g. "Production". */
9
+ environment: string;
10
+ region: string;
11
+ requests: number;
12
+ errorRate: number;
13
+ cost: number;
14
+ }
15
+
16
+ export interface TrafficPoint {
17
+ day: string;
18
+ Production: number;
19
+ Staging: number;
20
+ Preview: number;
21
+ }
22
+
23
+ export const usageBreadcrumbs: BreadcrumbItem[] = [{ label: 'Overview', href: '/overview' }, { label: 'Analytics' }];
24
+
25
+ const PROJECTS = [
26
+ 'orders-api',
27
+ 'billing-sync',
28
+ 'search-index',
29
+ 'auth-gateway',
30
+ 'media-resizer',
31
+ 'webhooks',
32
+ 'reports',
33
+ 'checkout-web',
34
+ ];
35
+ const ENVS = ['Production', 'Production', 'Staging', 'Preview'];
36
+ const REGIONS = ['eu-west-1', 'us-east-1', 'ap-south-1', 'eu-central-1'];
37
+
38
+ /** Usage per project and environment over the selected range. */
39
+ export const usageRows: UsageRow[] = Array.from({ length: 16 }, (_, i) => ({
40
+ id: `use_${i + 1}`,
41
+ project: PROJECTS[i % PROJECTS.length] ?? 'orders-api',
42
+ environment: ENVS[(i * 3) % ENVS.length] ?? 'Production',
43
+ region: REGIONS[(i * 5) % REGIONS.length] ?? 'eu-west-1',
44
+ requests: ((i * 48_271) % 900_000) + 12_000,
45
+ errorRate: Math.round(((i * 37) % 29) * 0.11 * 100) / 100,
46
+ cost: Math.round((((i * 3571) % 1_400) + 60) * 100) / 100,
47
+ }));
48
+
49
+ export const usageFilterFields: FilterField[] = [
50
+ { id: 'environment', label: 'Environment', options: ['Production', 'Staging', 'Preview'] },
51
+ { id: 'region', label: 'Region', options: REGIONS },
52
+ ];
53
+
54
+ export const usageSavedViews: SavedView[] = [
55
+ { value: 'all', label: 'All usage', filters: [] },
56
+ { value: 'production', label: 'Production only', filters: [{ field: 'environment', value: 'Production' }] },
57
+ {
58
+ value: 'eu',
59
+ label: 'EU regions',
60
+ filters: [
61
+ { field: 'region', value: 'eu-west-1' },
62
+ { field: 'region', value: 'eu-central-1' },
63
+ ],
64
+ },
65
+ ];
66
+
67
+ /**
68
+ * Search by project name; filters match the row field named by the filter id (same field OR,
69
+ * across fields AND). Filters on fields the row does not have are ignored.
70
+ */
71
+ export function filterUsage(rows: readonly UsageRow[], query: string, filters: readonly ActiveFilter[]): UsageRow[] {
72
+ const q = query.trim().toLowerCase();
73
+ const byField = new Map<string, string[]>();
74
+ for (const f of filters) byField.set(f.field, [...(byField.get(f.field) ?? []), f.value]);
75
+ return rows.filter((row) => {
76
+ if (q && !row.project.toLowerCase().includes(q)) return false;
77
+ for (const [field, values] of byField) {
78
+ const value: unknown = (row as unknown as Record<string, unknown>)[field];
79
+ if (value !== undefined && value !== null && !values.includes(String(value))) return false;
80
+ }
81
+ return true;
82
+ });
83
+ }
84
+
85
+ const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
86
+ const count = new Intl.NumberFormat('en-US');
87
+
88
+ export const usageColumns: DataTableColumn<UsageRow>[] = [
89
+ {
90
+ id: 'project',
91
+ header: 'Project',
92
+ accessor: (r) => r.project,
93
+ sortable: true,
94
+ hideable: false,
95
+ className: 'font-semibold',
96
+ },
97
+ { id: 'environment', header: 'Environment', accessor: (r) => r.environment, sortable: true },
98
+ {
99
+ id: 'region',
100
+ header: 'Region',
101
+ accessor: (r) => r.region,
102
+ sortable: true,
103
+ className: 'font-mono text-[12px] text-muted-foreground',
104
+ },
105
+ {
106
+ id: 'requests',
107
+ header: 'Requests',
108
+ accessor: (r) => r.requests,
109
+ cell: (r) => count.format(r.requests),
110
+ sortable: true,
111
+ align: 'right',
112
+ className: 'font-mono text-[12.5px]',
113
+ },
114
+ {
115
+ id: 'errors',
116
+ header: 'Error rate',
117
+ accessor: (r) => r.errorRate,
118
+ cell: (r) => `${r.errorRate.toFixed(2)}%`,
119
+ sortable: true,
120
+ align: 'right',
121
+ className: 'font-mono text-[12.5px]',
122
+ },
123
+ {
124
+ id: 'cost',
125
+ header: 'Cost',
126
+ accessor: (r) => r.cost,
127
+ cell: (r) => money.format(r.cost),
128
+ sortable: true,
129
+ align: 'right',
130
+ className: 'font-mono text-[12.5px]',
131
+ },
132
+ ];
133
+
134
+ const DAYS7 = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
135
+ const DAYS30 = Array.from({ length: 30 }, (_, i) => `Day ${i + 1}`);
136
+ const traffic = (labels: string[], base: number): TrafficPoint[] =>
137
+ labels.map((day, i) => ({
138
+ day,
139
+ Production: Math.round(base + ((i * 37) % 11) * 900),
140
+ Staging: Math.round(base * 0.22 + ((i * 13) % 7) * 210),
141
+ Preview: Math.round(base * 0.08 + ((i * 29) % 5) * 120),
142
+ }));
143
+
144
+ /** Requests per environment, per range. */
145
+ export const trafficByRange: Record<string, TrafficPoint[]> = {
146
+ '7d': traffic(DAYS7, 24_000),
147
+ '30d': traffic(DAYS30, 22_000),
148
+ };
149
+
150
+ export const trafficRanges: ChartCardRange[] = [
151
+ {
152
+ value: '7d',
153
+ label: '7d',
154
+ summary: { value: '241,906', delta: { value: '+8.2%', direction: 'up', sentiment: 'positive' } },
155
+ },
156
+ {
157
+ value: '30d',
158
+ label: '30d',
159
+ summary: { value: '1.02M', delta: { value: '+3.5%', direction: 'up', sentiment: 'positive' } },
160
+ },
161
+ ];
@@ -0,0 +1,21 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Usage analytics',
5
+ family: 'Overview',
6
+ priority: 'P2',
7
+ status: 'beta',
8
+ description:
9
+ 'Analytics page: date range and filter bar, spend and traffic charts, a sortable breakdown table and a CSV export action.',
10
+ layout: 'SidebarLayout + Page',
11
+ blocks: [
12
+ 'app-sidebar',
13
+ 'app-topbar',
14
+ 'page-header',
15
+ 'date-range-filter',
16
+ 'filter-bar',
17
+ 'chart-card',
18
+ 'data-table',
19
+ 'empty-states',
20
+ ],
21
+ };
@@ -0,0 +1,32 @@
1
+ import { act, render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import VerifyEmailPage from './Page';
5
+
6
+ describe('VerifyEmailPage', () => {
7
+ it('renders the inbox step with a resend countdown', async () => {
8
+ const { container } = render(<VerifyEmailPage />);
9
+ expect(screen.getByRole('main')).toBeInTheDocument();
10
+ expect(screen.getByRole('heading', { level: 1, name: 'Check your inbox' })).toBeInTheDocument();
11
+ expect(screen.getByText('dana@example.com')).toBeInTheDocument();
12
+ expect(screen.getByRole('button', { name: 'Resend email in 1:00' })).toBeDisabled();
13
+ await expectNoAxeViolations(container);
14
+ });
15
+
16
+ it('counts down, then resends and restarts the timer', async () => {
17
+ vi.useFakeTimers({ shouldAdvanceTime: true });
18
+ try {
19
+ const onResend = vi.fn();
20
+ render(<VerifyEmailPage onResend={onResend} resendCooldown={5} />);
21
+ expect(screen.getByRole('button', { name: 'Resend email in 0:05' })).toBeDisabled();
22
+ act(() => vi.advanceTimersByTime(5000));
23
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
24
+ await user.click(screen.getByRole('button', { name: 'Resend email' }));
25
+ expect(onResend).toHaveBeenCalledOnce();
26
+ expect(screen.getByRole('status')).toHaveTextContent('A new verification email was sent to dana@example.com.');
27
+ expect(screen.getByRole('button', { name: /Resend email in 0:0[45]/ })).toBeDisabled();
28
+ } finally {
29
+ vi.useRealTimers();
30
+ }
31
+ });
32
+ });