@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,130 @@
1
+ import type { CodeFile, CodeProblem } from '@gntik-ai/blocks';
2
+ import { FileCode, FileJson, FileText, Folder, FolderOpen } from '@gntik-ai/icons';
3
+ import type { BreadcrumbItem, TreeNode } from '@gntik-ai/ui';
4
+
5
+ export const editorProject = {
6
+ title: 'projects-api',
7
+ status: 'draft',
8
+ branch: 'feature/pagination',
9
+ };
10
+
11
+ export const editorBreadcrumbs: BreadcrumbItem[] = [
12
+ { label: 'Services', href: '/services' },
13
+ { label: 'projects-api', mono: true },
14
+ ];
15
+
16
+ const handlerBefore = `import { db } from './db';
17
+
18
+ export async function handler(req: Request) {
19
+ const projects = await db.projects.list();
20
+ return Response.json(projects);
21
+ }
22
+ `;
23
+
24
+ const handlerAfter = `import { db } from './db';
25
+ import { parseQuery } from './query';
26
+
27
+ export async function handler(req: Request) {
28
+ const { page, pageSize } = parseQuery(req.url);
29
+ const projects = await db.projects.list({ page, pageSize });
30
+ return Response.json({ items: projects, page });
31
+ }
32
+ `;
33
+
34
+ export const editorFiles: CodeFile[] = [
35
+ { path: 'src/handler.ts', language: 'typescript', content: handlerAfter, original: handlerBefore },
36
+ {
37
+ path: 'src/query.ts',
38
+ language: 'typescript',
39
+ content: `export function parseQuery(url: string) {
40
+ const params = new URL(url).searchParams;
41
+ return { page: Number(params.get('page') ?? 1), pageSize: Number(params.get('pageSize') ?? 20) };
42
+ }
43
+ `,
44
+ },
45
+ {
46
+ path: 'src/db.ts',
47
+ language: 'typescript',
48
+ content: `export const db = { projects: { list: async (_opts?: unknown) => [] as unknown[] } };\n`,
49
+ },
50
+ {
51
+ path: 'config/deploy.yaml',
52
+ language: 'yaml',
53
+ content: `name: projects-api
54
+ region: eu-west
55
+ replicas: 2
56
+ `,
57
+ },
58
+ { path: 'config/settings.json', language: 'json', content: `{\n "timeoutMs": 30000,\n "retries": 3\n}\n` },
59
+ {
60
+ path: 'README.md',
61
+ language: 'markdown',
62
+ content: `# projects-api\n\nLists the projects of a workspace, paginated.\n`,
63
+ },
64
+ ];
65
+
66
+ export const editorProblems: CodeProblem[] = [
67
+ {
68
+ id: 'p1',
69
+ severity: 'error',
70
+ message: "Property 'page' does not exist on the list options type.",
71
+ file: 'src/handler.ts',
72
+ line: 6,
73
+ column: 45,
74
+ source: 'tsc',
75
+ },
76
+ {
77
+ id: 'p2',
78
+ severity: 'warning',
79
+ message: 'pageSize is not clamped; very large pages are possible.',
80
+ file: 'src/query.ts',
81
+ line: 3,
82
+ column: 42,
83
+ source: 'lint',
84
+ },
85
+ {
86
+ id: 'p3',
87
+ severity: 'info',
88
+ message: 'replicas: 2 is below the recommended minimum of 3 for production.',
89
+ file: 'config/deploy.yaml',
90
+ line: 3,
91
+ column: 1,
92
+ source: 'schema',
93
+ },
94
+ ];
95
+
96
+ export const editorDefaultOpen = ['src/handler.ts', 'src/query.ts'];
97
+
98
+ const iconFor = (path: string) => (path.endsWith('.json') ? FileJson : path.endsWith('.md') ? FileText : FileCode);
99
+
100
+ /** Folder tree of the file paths: folders first, then files, each alphabetical. Folder ids end in "/". */
101
+ export function filesToTree(paths: readonly string[]): TreeNode[] {
102
+ type Dir = { dirs: Map<string, Dir>; files: string[] };
103
+ const root: Dir = { dirs: new Map(), files: [] };
104
+ for (const path of paths) {
105
+ const parts = path.split('/');
106
+ let dir = root;
107
+ for (const part of parts.slice(0, -1)) {
108
+ let next = dir.dirs.get(part);
109
+ if (!next) {
110
+ next = { dirs: new Map(), files: [] };
111
+ dir.dirs.set(part, next);
112
+ }
113
+ dir = next;
114
+ }
115
+ dir.files.push(path);
116
+ }
117
+ const build = (dir: Dir, prefix: string): TreeNode[] => [
118
+ ...[...dir.dirs.entries()]
119
+ .sort(([a], [b]) => a.localeCompare(b))
120
+ .map(([name, sub]) => ({
121
+ id: `${prefix}${name}/`,
122
+ label: name,
123
+ icon: Folder,
124
+ openIcon: FolderOpen,
125
+ children: build(sub, `${prefix}${name}/`),
126
+ })),
127
+ ...[...dir.files].sort().map((path) => ({ id: path, label: path.split('/').pop() ?? path, icon: iconFor(path) })),
128
+ ];
129
+ return build(root, '');
130
+ }
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Code editor',
5
+ family: 'Builders',
6
+ priority: 'P3',
7
+ status: 'experimental',
8
+ description:
9
+ 'Full-screen code editor: CanvasLayout top bar (breadcrumb, title, status, branch, Save), a file TreeList with a context menu (open, copy path, close tab, delete) in the palette, and the CodePanel block (file tabs, Monaco editor or diff, problems list) as the canvas.',
10
+ layout: 'CanvasLayout',
11
+ blocks: ['code-panel'],
12
+ };
@@ -0,0 +1,33 @@
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 ContactPage from './Page';
5
+ import { offices } from './data';
6
+
7
+ describe('ContactPage', () => {
8
+ it('renders the form and office cards', { timeout: 15000 }, async () => {
9
+ render(<ContactPage />);
10
+ expect(screen.getByRole('main')).toBeInTheDocument();
11
+ expect(screen.getByRole('heading', { level: 1, name: 'Talk to us' })).toBeInTheDocument();
12
+ expect(screen.getByRole('form', { name: 'Contact form' })).toBeInTheDocument();
13
+ const officesRegion = screen.getByRole('region', { name: 'Offices' });
14
+ expect(within(officesRegion).getAllByRole('heading', { level: 3 })).toHaveLength(offices.length);
15
+ await expectNoAxeViolations();
16
+ });
17
+
18
+ it('validates, then sends and shows the success state', { timeout: 15000 }, async () => {
19
+ const onSubmit = vi.fn();
20
+ render(<ContactPage onSubmit={onSubmit} defaultValues={{ topic: 'sales' }} />);
21
+ await userEvent.click(screen.getByRole('button', { name: 'Send message' }));
22
+ expect(onSubmit).not.toHaveBeenCalled();
23
+ expect(screen.getAllByText('Enter your name.').length).toBeGreaterThan(0);
24
+ await userEvent.type(screen.getByRole('textbox', { name: /^Name/ }), 'Ada Example');
25
+ await userEvent.type(screen.getByRole('textbox', { name: /Work email/ }), 'ada@example.com');
26
+ await userEvent.type(screen.getByRole('textbox', { name: /Message/ }), 'We would like a demo for 40 seats.');
27
+ await userEvent.click(screen.getByRole('button', { name: 'Send message' }));
28
+ expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ name: 'Ada Example', topic: 'sales' }));
29
+ expect(await screen.findByRole('heading', { name: 'Message sent' })).toBeInTheDocument();
30
+ expect(screen.getByRole('status')).toHaveFocus();
31
+ await expectNoAxeViolations();
32
+ });
33
+ });
@@ -0,0 +1,272 @@
1
+ import { ValidationSummary, type ValidationIssue } from '@gntik-ai/blocks';
2
+ import { CircleCheck, Clock, Mail, MapPin, Phone, Send } from '@gntik-ai/icons';
3
+ import {
4
+ Button,
5
+ Card,
6
+ CardBody,
7
+ CardHeader,
8
+ CardTitle,
9
+ Field,
10
+ FieldError,
11
+ FieldLabel,
12
+ Grid,
13
+ Heading,
14
+ Input,
15
+ Link,
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ Stack,
21
+ Text,
22
+ Textarea,
23
+ type MegaMenuItem,
24
+ } from '@gntik-ai/ui';
25
+ import { useId, useState, type ReactNode } from 'react';
26
+ import {
27
+ contactTopics,
28
+ emptyContact,
29
+ offices,
30
+ publicCopyright,
31
+ publicFooterLinks,
32
+ publicMenu,
33
+ validateContact,
34
+ type ContactTopic,
35
+ type ContactValues,
36
+ type Office,
37
+ } from './data';
38
+ import { PublicFrame } from './PublicFrame';
39
+
40
+ export interface ContactProps {
41
+ brand: ReactNode;
42
+ menu: MegaMenuItem[];
43
+ currentHref: string;
44
+ title: string;
45
+ description: string;
46
+ topics: Array<{ value: ContactTopic; label: string }>;
47
+ offices: Office[];
48
+ defaultValues: Partial<ContactValues>;
49
+ /** Called with valid values; the page shows the success state once it resolves. */
50
+ onSubmit: (values: ContactValues) => void | Promise<void>;
51
+ footerLinks: Array<{ label: string; href: string }>;
52
+ copyright: ReactNode;
53
+ }
54
+
55
+ const LABELS: Record<keyof ContactValues, string> = {
56
+ name: 'Name',
57
+ email: 'Work email',
58
+ company: 'Company',
59
+ topic: 'Topic',
60
+ message: 'Message',
61
+ };
62
+
63
+ /** Public contact page: StackedLayout + MegaMenu, a validated form with a success state, and office cards. */
64
+ export default function ContactPage(props: Partial<ContactProps>) {
65
+ const {
66
+ brand,
67
+ menu = publicMenu,
68
+ currentHref = '/contact',
69
+ title = 'Talk to us',
70
+ description = 'Questions about plans, billing or a technical issue? We answer within one business day.',
71
+ topics = contactTopics,
72
+ offices: officeList = offices,
73
+ defaultValues,
74
+ onSubmit,
75
+ footerLinks = publicFooterLinks,
76
+ copyright = publicCopyright,
77
+ } = props;
78
+ const [values, setValues] = useState<ContactValues>({ ...emptyContact, ...defaultValues });
79
+ const [submitCount, setSubmitCount] = useState(0);
80
+ const [sending, setSending] = useState(false);
81
+ const [sent, setSent] = useState(false);
82
+ const ids = useId();
83
+ const fieldId = (k: keyof ContactValues) => `${ids}-${k}`;
84
+
85
+ const errors = submitCount > 0 ? validateContact(values) : {};
86
+ const issues: ValidationIssue[] = (Object.keys(errors) as Array<keyof ContactValues>).map((k) => ({
87
+ fieldId: fieldId(k),
88
+ label: LABELS[k],
89
+ message: errors[k] ?? '',
90
+ }));
91
+ const set = <K extends keyof ContactValues>(key: K, value: ContactValues[K]) =>
92
+ setValues((v) => ({ ...v, [key]: value }));
93
+
94
+ const submit = async () => {
95
+ setSubmitCount((n) => n + 1);
96
+ if (Object.keys(validateContact(values)).length > 0) return;
97
+ setSending(true);
98
+ try {
99
+ await onSubmit?.(values);
100
+ setSent(true);
101
+ } finally {
102
+ setSending(false);
103
+ }
104
+ };
105
+ const reset = () => {
106
+ setValues({ ...emptyContact, ...defaultValues });
107
+ setSubmitCount(0);
108
+ setSent(false);
109
+ };
110
+
111
+ return (
112
+ <PublicFrame brand={brand} menu={menu} currentHref={currentHref} footerLinks={footerLinks} copyright={copyright}>
113
+ <Stack gap={10}>
114
+ <Stack gap={3} className="max-w-2xl">
115
+ <Heading level={1} size="xl">
116
+ {title}
117
+ </Heading>
118
+ <Text tone="muted">{description}</Text>
119
+ </Stack>
120
+ <Grid cols={{ base: 1, lg: 5 }} gap={8} align="start">
121
+ <Card className="lg:col-span-3">
122
+ <CardBody className="p-6">
123
+ {sent ? (
124
+ <div
125
+ ref={(el) => el?.focus()}
126
+ tabIndex={-1}
127
+ role="status"
128
+ className="flex flex-col items-start gap-3 py-6 outline-none"
129
+ >
130
+ <CircleCheck size={28} aria-hidden className="text-success-text" />
131
+ <Heading level={2} size="sm">
132
+ Message sent
133
+ </Heading>
134
+ <Text tone="muted">
135
+ Thanks, {values.name.trim()}. We’ll reply to {values.email.trim()} within one business day.
136
+ </Text>
137
+ <Button variant="secondary" onClick={reset}>
138
+ Send another message
139
+ </Button>
140
+ </div>
141
+ ) : (
142
+ <form
143
+ noValidate
144
+ aria-label="Contact form"
145
+ onSubmit={(e) => {
146
+ e.preventDefault();
147
+ void submit();
148
+ }}
149
+ >
150
+ <Stack gap={5}>
151
+ <ValidationSummary errors={issues} submitCount={submitCount} />
152
+ <Grid cols={{ base: 1, sm: 2 }} gap={4}>
153
+ <Field invalid={errors.name != null}>
154
+ <FieldLabel required>{LABELS.name}</FieldLabel>
155
+ <Input
156
+ id={fieldId('name')}
157
+ autoComplete="name"
158
+ required
159
+ value={values.name}
160
+ onValueChange={(v) => set('name', v)}
161
+ />
162
+ <FieldError match={errors.name != null}>{errors.name}</FieldError>
163
+ </Field>
164
+ <Field invalid={errors.email != null}>
165
+ <FieldLabel required>{LABELS.email}</FieldLabel>
166
+ <Input
167
+ id={fieldId('email')}
168
+ type="email"
169
+ autoComplete="email"
170
+ required
171
+ value={values.email}
172
+ onValueChange={(v) => set('email', v)}
173
+ />
174
+ <FieldError match={errors.email != null}>{errors.email}</FieldError>
175
+ </Field>
176
+ <Field>
177
+ <FieldLabel>{LABELS.company}</FieldLabel>
178
+ <Input
179
+ id={fieldId('company')}
180
+ autoComplete="organization"
181
+ value={values.company}
182
+ onValueChange={(v) => set('company', v)}
183
+ />
184
+ </Field>
185
+ <Field invalid={errors.topic != null}>
186
+ <FieldLabel required>{LABELS.topic}</FieldLabel>
187
+ <Select
188
+ value={values.topic || null}
189
+ onValueChange={(v) => set('topic', v ?? '')}
190
+ items={topics.map(({ value, label }) => ({ value, label }))}
191
+ >
192
+ <SelectTrigger id={fieldId('topic')} className="w-full" placeholder="Choose a topic" />
193
+ <SelectContent>
194
+ {topics.map((t) => (
195
+ <SelectItem key={t.value} value={t.value}>
196
+ {t.label}
197
+ </SelectItem>
198
+ ))}
199
+ </SelectContent>
200
+ </Select>
201
+ <FieldError match={errors.topic != null}>{errors.topic}</FieldError>
202
+ </Field>
203
+ </Grid>
204
+ <Field invalid={errors.message != null}>
205
+ <FieldLabel required>{LABELS.message}</FieldLabel>
206
+ <Textarea
207
+ id={fieldId('message')}
208
+ rows={5}
209
+ required
210
+ value={values.message}
211
+ onValueChange={(v) => set('message', v)}
212
+ />
213
+ <FieldError match={errors.message != null}>{errors.message}</FieldError>
214
+ </Field>
215
+ <Text variant="caption" tone="muted">
216
+ By sending this form you agree to our <Link href="/privacy">privacy policy</Link>.
217
+ </Text>
218
+ <div>
219
+ <Button type="submit" icon={Send} loading={sending}>
220
+ Send message
221
+ </Button>
222
+ </div>
223
+ </Stack>
224
+ </form>
225
+ )}
226
+ </CardBody>
227
+ </Card>
228
+ <section aria-labelledby={`${ids}-offices`} className="lg:col-span-2">
229
+ <Heading id={`${ids}-offices`} level={2} size="xs" className="mb-3">
230
+ Offices
231
+ </Heading>
232
+ <Stack gap={3}>
233
+ {officeList.map((o) => (
234
+ <Card key={o.id} variant="outline">
235
+ <CardHeader>
236
+ <CardTitle as="h3">{o.city}</CardTitle>
237
+ </CardHeader>
238
+ <CardBody className="pt-0">
239
+ <ul className="flex flex-col gap-1.5">
240
+ <li className="flex gap-2">
241
+ <MapPin size={14} aria-hidden className="mt-1 shrink-0" />
242
+ <address className="not-italic">
243
+ {o.address.map((line) => (
244
+ <span key={line} className="block">
245
+ {line}
246
+ </span>
247
+ ))}
248
+ </address>
249
+ </li>
250
+ <li className="flex items-center gap-2">
251
+ <Mail size={14} aria-hidden className="shrink-0" />
252
+ <Link href={`mailto:${o.email}`}>{o.email}</Link>
253
+ </li>
254
+ <li className="flex items-center gap-2">
255
+ <Phone size={14} aria-hidden className="shrink-0" />
256
+ <Link href={`tel:${o.phone.replace(/\s/g, '')}`}>{o.phone}</Link>
257
+ </li>
258
+ <li className="flex items-center gap-2">
259
+ <Clock size={14} aria-hidden className="shrink-0" />
260
+ {o.hours}
261
+ </li>
262
+ </ul>
263
+ </CardBody>
264
+ </Card>
265
+ ))}
266
+ </Stack>
267
+ </section>
268
+ </Grid>
269
+ </Stack>
270
+ </PublicFrame>
271
+ );
272
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ Button,
3
+ Link,
4
+ Logo,
5
+ MegaMenu,
6
+ MobileNav,
7
+ StackedLayout,
8
+ ThemeSwitcher,
9
+ megaMenuNavItems,
10
+ type MegaMenuItem,
11
+ } from '@gntik-ai/ui';
12
+ import type { ReactNode } from 'react';
13
+
14
+ export interface PublicFrameProps {
15
+ brand?: ReactNode;
16
+ menu: MegaMenuItem[];
17
+ currentHref?: string;
18
+ signInHref?: string;
19
+ getStartedHref?: string;
20
+ footerLinks: Array<{ label: string; href: string }>;
21
+ copyright: ReactNode;
22
+ children: ReactNode;
23
+ }
24
+
25
+ /**
26
+ * Public-site chrome: StackedLayout with the MegaMenu as the sub-nav row on large screens and
27
+ * a MobileNav (fed by megaMenuNavItems) below lg.
28
+ */
29
+ export function PublicFrame({
30
+ brand = <Logo size={26} wordmark />,
31
+ menu,
32
+ currentHref,
33
+ signInHref = '/sign-in',
34
+ getStartedHref = '/sign-up',
35
+ footerLinks,
36
+ copyright,
37
+ children,
38
+ }: PublicFrameProps) {
39
+ return (
40
+ <StackedLayout
41
+ fullScreen
42
+ width="wide"
43
+ brand={brand}
44
+ actions={
45
+ <>
46
+ <ThemeSwitcher size="sm" className="hidden md:inline-flex" />
47
+ <Button variant="ghost" size="sm" render={<a href={signInHref} />} nativeButton={false}>
48
+ Sign in
49
+ </Button>
50
+ <Button size="sm" render={<a href={getStartedHref} />} nativeButton={false}>
51
+ Get started
52
+ </Button>
53
+ <MobileNav
54
+ className="lg:hidden"
55
+ groups={[{ items: megaMenuNavItems(menu) }]}
56
+ currentHref={currentHref}
57
+ title="Menu"
58
+ label="Primary"
59
+ side="right"
60
+ />
61
+ </>
62
+ }
63
+ subnav={<MegaMenu items={menu} currentHref={currentHref} label="Primary" className="hidden lg:block" />}
64
+ footer={
65
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
66
+ <span>{copyright}</span>
67
+ <nav aria-label="Footer">
68
+ <ul className="flex flex-wrap gap-x-4 gap-y-1">
69
+ {footerLinks.map((l) => (
70
+ <li key={l.href}>
71
+ <Link href={l.href} tone="muted" underline="hover">
72
+ {l.label}
73
+ </Link>
74
+ </li>
75
+ ))}
76
+ </ul>
77
+ </nav>
78
+ </div>
79
+ }
80
+ >
81
+ {children}
82
+ </StackedLayout>
83
+ );
84
+ }
@@ -0,0 +1,129 @@
1
+ import { BarChart3, BookOpen, FileText, FolderKanban, LifeBuoy, Rocket, Shield } from '@gntik-ai/icons';
2
+ import type { MegaMenuItem } from '@gntik-ai/ui';
3
+
4
+ /** Public navigation: product panels plus plain links. */
5
+ export const publicMenu: MegaMenuItem[] = [
6
+ {
7
+ label: 'Product',
8
+ sections: [
9
+ {
10
+ title: 'Build',
11
+ links: [
12
+ {
13
+ label: 'Projects',
14
+ href: '/product/projects',
15
+ icon: FolderKanban,
16
+ description: 'Every project, its environments and owners.',
17
+ },
18
+ {
19
+ label: 'Deployments',
20
+ href: '/product/deployments',
21
+ icon: Rocket,
22
+ description: 'Previews, rollbacks and deploy gates.',
23
+ },
24
+ ],
25
+ },
26
+ {
27
+ title: 'Operate',
28
+ links: [
29
+ {
30
+ label: 'Analytics',
31
+ href: '/product/analytics',
32
+ icon: BarChart3,
33
+ description: 'Usage, latency and error budgets.',
34
+ },
35
+ { label: 'Security', href: '/product/security', icon: Shield, description: 'SSO, roles and audit logs.' },
36
+ ],
37
+ },
38
+ ],
39
+ },
40
+ {
41
+ label: 'Resources',
42
+ sections: [
43
+ {
44
+ links: [
45
+ { label: 'Documentation', href: '/docs', icon: BookOpen, description: 'Guides and API reference.' },
46
+ { label: 'Changelog', href: '/changelog', icon: FileText, description: 'What shipped this month.' },
47
+ { label: 'Support', href: '/contact', icon: LifeBuoy, description: 'Talk to a person.' },
48
+ ],
49
+ },
50
+ ],
51
+ },
52
+ { label: 'Pricing', href: '/pricing' },
53
+ { label: 'Contact', href: '/contact' },
54
+ ];
55
+
56
+ export const publicFooterLinks = [
57
+ { label: 'Status', href: '/status' },
58
+ { label: 'Privacy', href: '/privacy' },
59
+ { label: 'Terms', href: '/terms' },
60
+ ];
61
+
62
+ export const publicCopyright = '© Example, Inc.';
63
+
64
+ export type ContactTopic = 'sales' | 'support' | 'billing' | 'partnerships';
65
+
66
+ export const contactTopics: Array<{ value: ContactTopic; label: string }> = [
67
+ { value: 'sales', label: 'Sales and plans' },
68
+ { value: 'support', label: 'Technical support' },
69
+ { value: 'billing', label: 'Billing' },
70
+ { value: 'partnerships', label: 'Partnerships' },
71
+ ];
72
+
73
+ export interface ContactValues {
74
+ name: string;
75
+ email: string;
76
+ company: string;
77
+ topic: ContactTopic | '';
78
+ message: string;
79
+ }
80
+
81
+ export const emptyContact: ContactValues = { name: '', email: '', company: '', topic: '', message: '' };
82
+
83
+ export interface Office {
84
+ id: string;
85
+ city: string;
86
+ /** Address lines. */
87
+ address: string[];
88
+ email: string;
89
+ phone: string;
90
+ hours: string;
91
+ }
92
+
93
+ export const offices: Office[] = [
94
+ {
95
+ id: 'lisbon',
96
+ city: 'Lisbon',
97
+ address: ['Rua do Exemplo 120', '1200-000 Lisbon, Portugal'],
98
+ email: 'lisbon@example.com',
99
+ phone: '+351 210 000 000',
100
+ hours: 'Mon–Fri, 9:00–18:00 WET',
101
+ },
102
+ {
103
+ id: 'toronto',
104
+ city: 'Toronto',
105
+ address: ['200 Example Street, Suite 400', 'Toronto, ON M5V 0A0, Canada'],
106
+ email: 'toronto@example.com',
107
+ phone: '+1 416 000 0000',
108
+ hours: 'Mon–Fri, 9:00–17:00 ET',
109
+ },
110
+ {
111
+ id: 'singapore',
112
+ city: 'Singapore',
113
+ address: ['10 Example Road, #08-01', 'Singapore 018000'],
114
+ email: 'singapore@example.com',
115
+ phone: '+65 6000 0000',
116
+ hours: 'Mon–Fri, 9:00–18:00 SGT',
117
+ },
118
+ ];
119
+
120
+ export type ContactErrors = Partial<Record<keyof ContactValues, string>>;
121
+
122
+ export function validateContact(v: ContactValues): ContactErrors {
123
+ const errors: ContactErrors = {};
124
+ if (!v.name.trim()) errors.name = 'Enter your name.';
125
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.email.trim())) errors.email = 'Enter a valid email address.';
126
+ if (!v.topic) errors.topic = 'Choose a topic.';
127
+ if (v.message.trim().length < 10) errors.message = 'Tell us a little more (at least 10 characters).';
128
+ return errors;
129
+ }
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Contact',
5
+ family: 'Overview',
6
+ priority: 'P3',
7
+ status: 'experimental',
8
+ description:
9
+ 'Public contact page: StackedLayout with a MegaMenu (MobileNav below lg), a validated contact form (Field, Input, Select, Textarea, ValidationSummary) with a focused success state, and office cards with address, email, phone and hours.',
10
+ layout: 'StackedLayout',
11
+ blocks: ['validation-summary'],
12
+ };