@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,227 @@
1
+ import { PricingTable, type PricingTier, type RoleOption } from '@gntik-ai/blocks';
2
+ import { Globe } from '@gntik-ai/icons';
3
+ import {
4
+ Field,
5
+ FieldDescription,
6
+ FieldError,
7
+ FieldLabel,
8
+ Heading,
9
+ Input,
10
+ Logo,
11
+ SelectableCard,
12
+ SelectableCardGroup,
13
+ SimpleSelect,
14
+ Stack,
15
+ Text,
16
+ TokenInput,
17
+ WizardLayout,
18
+ type StepItem,
19
+ } from '@gntik-ai/ui';
20
+ import { useId, useState, type ReactNode } from 'react';
21
+ import {
22
+ EMAIL_RE,
23
+ slugify,
24
+ workspaceInitialValues,
25
+ workspacePlans,
26
+ workspaceRegions,
27
+ workspaceRoles,
28
+ workspaceSteps,
29
+ type WorkspaceRegion,
30
+ type WorkspaceValues,
31
+ } from './data';
32
+
33
+ export interface CreateWorkspaceProps {
34
+ steps: StepItem[];
35
+ initialValues: WorkspaceValues;
36
+ regions: readonly WorkspaceRegion[];
37
+ plans: readonly PricingTier[];
38
+ roles: readonly RoleOption[];
39
+ /** Host shown before the slug, e.g. "app.example.com/". */
40
+ addressPrefix: string;
41
+ maxInvites: number;
42
+ /** Called on the last step with the slug; return a promise to show the loading state. */
43
+ onFinish: (values: WorkspaceValues & { slug: string }) => void | Promise<void>;
44
+ onExit: () => void;
45
+ onStepChange: (index: number) => void;
46
+ }
47
+
48
+ function StepIntro({ id, title, children }: { id: string; title: string; children?: ReactNode }) {
49
+ return (
50
+ <Stack gap={1}>
51
+ <Heading level={1} id={id} size="lg">
52
+ {title}
53
+ </Heading>
54
+ {children && <Text variant="supporting">{children}</Text>}
55
+ </Stack>
56
+ );
57
+ }
58
+
59
+ const emailError = (token: string) => (EMAIL_RE.test(token) ? null : `${token} is not a valid email address`);
60
+
61
+ /** Workspace onboarding: name and region as selectable cards, the plan as a selectable PricingTable, then inline invitations. */
62
+ export default function CreateWorkspacePage({
63
+ steps = workspaceSteps,
64
+ initialValues = workspaceInitialValues,
65
+ regions = workspaceRegions,
66
+ plans = workspacePlans,
67
+ roles = workspaceRoles,
68
+ addressPrefix = 'app.example.com/',
69
+ maxInvites = 20,
70
+ onFinish,
71
+ onExit,
72
+ onStepChange,
73
+ }: Partial<CreateWorkspaceProps>) {
74
+ const id = useId();
75
+ const [step, setStep] = useState(0);
76
+ const [values, setValues] = useState(initialValues);
77
+ const [touched, setTouched] = useState(false);
78
+ const [finishing, setFinishing] = useState(false);
79
+ const [status, setStatus] = useState('');
80
+ const set = <K extends keyof WorkspaceValues>(key: K, value: WorkspaceValues[K]) =>
81
+ setValues((v) => ({ ...v, [key]: value }));
82
+
83
+ const slug = slugify(values.name);
84
+ const nameValid = values.name.trim().length >= 2 && slug.length > 0;
85
+ const invalidInvites = values.invites.filter((e) => emailError(e)).length;
86
+ const go = (index: number) => {
87
+ setStep(index);
88
+ onStepChange?.(index);
89
+ };
90
+ const finish = async () => {
91
+ setFinishing(true);
92
+ try {
93
+ await onFinish?.({ ...values, slug });
94
+ setStatus(`Workspace “${values.name}” created`);
95
+ } finally {
96
+ setFinishing(false);
97
+ }
98
+ };
99
+
100
+ return (
101
+ <WizardLayout
102
+ fullScreen
103
+ logo={<Logo size={22} />}
104
+ title="Create a workspace"
105
+ steps={steps}
106
+ current={step}
107
+ onStepChange={go}
108
+ nextDisabled={(step === 0 && !nameValid) || (step === 3 && invalidInvites > 0)}
109
+ nextLoading={finishing}
110
+ finishLabel={values.invites.length > 0 ? 'Create and invite' : 'Create workspace'}
111
+ onFinish={() => void finish()}
112
+ onExit={onExit}
113
+ exitTitle="Leave workspace setup?"
114
+ exitDescription="The workspace has not been created yet. Your answers will be discarded."
115
+ footerStart={<span aria-live="polite">{status}</span>}
116
+ >
117
+ {step === 0 && (
118
+ <section aria-labelledby={`${id}-name`}>
119
+ <Stack gap={6}>
120
+ <StepIntro id={`${id}-name`} title="Name your workspace">
121
+ A workspace holds your projects, members and billing.
122
+ </StepIntro>
123
+ <Field invalid={touched && !nameValid}>
124
+ <FieldLabel required>Workspace name</FieldLabel>
125
+ <Input
126
+ required
127
+ value={values.name}
128
+ onValueChange={(v) => set('name', v)}
129
+ onBlur={() => setTouched(true)}
130
+ placeholder="Acme Inc."
131
+ />
132
+ <FieldDescription>
133
+ Address:{' '}
134
+ <span className="font-mono text-foreground">
135
+ {addressPrefix}
136
+ {slug || 'your-workspace'}
137
+ </span>
138
+ </FieldDescription>
139
+ <FieldError match={touched && !nameValid}>Enter at least two characters.</FieldError>
140
+ </Field>
141
+ </Stack>
142
+ </section>
143
+ )}
144
+ {step === 1 && (
145
+ <section aria-labelledby={`${id}-region`}>
146
+ <Stack gap={6}>
147
+ <StepIntro id={`${id}-region`} title="Where should your data live?">
148
+ Projects in this workspace run and store data in this region.
149
+ </StepIntro>
150
+ <SelectableCardGroup
151
+ aria-labelledby={`${id}-region`}
152
+ value={values.region}
153
+ onValueChange={(v) => set('region', v)}
154
+ columns={1}
155
+ >
156
+ {regions.map((r) => (
157
+ <SelectableCard
158
+ key={r.value}
159
+ value={r.value}
160
+ title={r.title}
161
+ description={r.description}
162
+ meta={r.meta}
163
+ icon={<Globe size={18} aria-hidden />}
164
+ />
165
+ ))}
166
+ </SelectableCardGroup>
167
+ </Stack>
168
+ </section>
169
+ )}
170
+ {step === 2 && (
171
+ <section aria-labelledby={`${id}-plan`}>
172
+ <Stack gap={6}>
173
+ <StepIntro id={`${id}-plan`} title="Pick a plan">
174
+ Change or cancel at any time from Billing.
175
+ </StepIntro>
176
+ <PricingTable
177
+ className="p-0 sm:p-0"
178
+ tiers={[...plans]}
179
+ title={null}
180
+ eyebrow={null}
181
+ description={null}
182
+ aria-labelledby={`${id}-plan`}
183
+ headingLevel="h2"
184
+ showCycleToggle={false}
185
+ columns={1}
186
+ selectable
187
+ value={values.plan}
188
+ onValueChange={(v) => set('plan', v)}
189
+ />
190
+ </Stack>
191
+ </section>
192
+ )}
193
+ {step === 3 && (
194
+ <section aria-labelledby={`${id}-invite`}>
195
+ <Stack gap={6}>
196
+ <StepIntro id={`${id}-invite`} title="Invite your team">
197
+ Optional: you can invite people later from Members.
198
+ </StepIntro>
199
+ <Field invalid={invalidInvites > 0}>
200
+ <FieldLabel>Email addresses</FieldLabel>
201
+ <TokenInput
202
+ value={values.invites}
203
+ onValueChange={(v) => set('invites', v)}
204
+ validate={emailError}
205
+ maxItems={maxInvites}
206
+ placeholder="name@company.com"
207
+ listLabel="Invited addresses"
208
+ invalid={invalidInvites > 0}
209
+ />
210
+ <FieldDescription>Separate addresses with commas or Enter.</FieldDescription>
211
+ <FieldError match={invalidInvites > 0}>
212
+ Fix {invalidInvites} invalid {invalidInvites === 1 ? 'address' : 'addresses'}.
213
+ </FieldError>
214
+ </Field>
215
+ <SimpleSelect
216
+ label="Role"
217
+ items={roles.map(({ value, label }) => ({ value, label }))}
218
+ value={values.role}
219
+ onValueChange={(v) => v && set('role', v)}
220
+ className="w-full sm:max-w-xs"
221
+ />
222
+ </Stack>
223
+ </section>
224
+ )}
225
+ </WizardLayout>
226
+ );
227
+ }
@@ -0,0 +1,61 @@
1
+ import { memberRoles, samplePricingTiers, type PricingTier, type RoleOption } from '@gntik-ai/blocks';
2
+ import type { StepItem } from '@gntik-ai/ui';
3
+
4
+ export interface WorkspaceValues {
5
+ name: string;
6
+ region: string;
7
+ plan: string;
8
+ invites: string[];
9
+ role: string;
10
+ }
11
+
12
+ export interface WorkspaceRegion {
13
+ value: string;
14
+ title: string;
15
+ description: string;
16
+ meta: string;
17
+ }
18
+
19
+ export const workspaceSteps: StepItem[] = [
20
+ { id: 'name', label: 'Workspace' },
21
+ { id: 'region', label: 'Region' },
22
+ { id: 'plan', label: 'Plan' },
23
+ { id: 'invite', label: 'Invite' },
24
+ ];
25
+
26
+ export const workspaceRegions: WorkspaceRegion[] = [
27
+ { value: 'eu', title: 'Europe', description: 'Frankfurt and Ireland', meta: 'GDPR data residency' },
28
+ { value: 'us', title: 'United States', description: 'Virginia and Oregon', meta: 'Lowest latency in the Americas' },
29
+ { value: 'ap', title: 'Asia Pacific', description: 'Mumbai and Singapore', meta: 'Lowest latency in Asia' },
30
+ ];
31
+
32
+ export const workspacePlans: PricingTier[] = samplePricingTiers;
33
+ export const workspaceRoles: RoleOption[] = memberRoles;
34
+
35
+ export const workspaceInitialValues: WorkspaceValues = {
36
+ name: '',
37
+ region: 'eu',
38
+ plan: 'team',
39
+ invites: [],
40
+ role: 'member',
41
+ };
42
+
43
+ export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
44
+
45
+ /** URL-safe slug for the workspace address. */
46
+ export const slugify = (name: string) =>
47
+ name
48
+ .toLowerCase()
49
+ .trim()
50
+ .replace(/[^a-z0-9]+/g, '-')
51
+ .replace(/^-+|-+$/g, '');
52
+
53
+ const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
54
+
55
+ /** "$49 / month", "Free" or "Custom pricing". */
56
+ export const planPrice = (tier: PricingTier) =>
57
+ tier.priceMonthly == null
58
+ ? 'Custom pricing'
59
+ : tier.priceMonthly === 0
60
+ ? 'Free'
61
+ : `${usd.format(tier.priceMonthly)} / month`;
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Create workspace',
5
+ family: 'Onboarding',
6
+ priority: 'P2',
7
+ status: 'beta',
8
+ description:
9
+ 'Workspace onboarding wizard: name with address preview, data region and plan as selectable cards, and inline team invitations with email chips and a role.',
10
+ layout: 'WizardLayout',
11
+ blocks: [],
12
+ };
@@ -0,0 +1,45 @@
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 DataExplorerPage from './Page';
5
+ import { explorerRows, generateExplorerRows } from './data';
6
+
7
+ const small = generateExplorerRows(300);
8
+
9
+ describe('DataExplorerPage', () => {
10
+ it('renders the query bar and the table over 10k rows', { timeout: 20000 }, async () => {
11
+ render(<DataExplorerPage />);
12
+ expect(explorerRows).toHaveLength(10_000);
13
+ expect(screen.getByRole('main')).toBeInTheDocument();
14
+ expect(screen.getByRole('heading', { level: 1, name: 'Data explorer' })).toBeInTheDocument();
15
+ expect(screen.getByText('10,000 of 10,000 rows')).toBeInTheDocument();
16
+ const table = screen.getByRole('table', { name: 'request_logs rows' });
17
+ expect(within(table).getAllByRole('row')).toHaveLength(51);
18
+ await expectNoAxeViolations();
19
+ });
20
+
21
+ it('filters with a typed PowerSearch term', { timeout: 15000 }, async () => {
22
+ render(<DataExplorerPage rows={small} />);
23
+ await userEvent.click(screen.getByRole('combobox', { name: 'Filter request_logs' }));
24
+ await userEvent.keyboard('status>=500{Enter}');
25
+ const expected = small.filter((r) => r.status >= 500).length;
26
+ expect(screen.getByText(`${expected} of 300 rows`)).toBeInTheDocument();
27
+ });
28
+
29
+ it('chooses columns and inspects a row', { timeout: 15000 }, async () => {
30
+ render(<DataExplorerPage rows={small} />);
31
+ await userEvent.click(screen.getAllByRole('button', { name: 'Columns' })[0]!);
32
+ const dialog = await screen.findByRole('dialog');
33
+ await userEvent.click(within(dialog).getByRole('option', { name: /Region/ }));
34
+ await userEvent.click(within(dialog).getByRole('button', { name: 'Add checked' }));
35
+ await userEvent.click(within(dialog).getByRole('button', { name: 'Apply' }));
36
+ const table = screen.getByRole('table', { name: 'request_logs rows' });
37
+ expect(within(table).getByRole('columnheader', { name: /Region/ })).toBeInTheDocument();
38
+
39
+ const first = small[0]!;
40
+ await userEvent.click(within(table).getByRole('button', { name: new RegExp(`${first.id}`) }));
41
+ await userEvent.click(await screen.findByRole('menuitem', { name: 'Inspect row' }));
42
+ const drawer = await screen.findByRole('dialog');
43
+ expect(within(drawer).getByRole('tree', { name: `Row ${first.id}` })).toHaveTextContent(first.userAgent);
44
+ });
45
+ });
@@ -0,0 +1,247 @@
1
+ import { DataTable, NoResultsEmpty, PageHeader, type DataTableColumn } from '@gntik-ai/blocks';
2
+ import { Braces, Columns3, Download } from '@gntik-ai/icons';
3
+ import {
4
+ Button,
5
+ Dialog,
6
+ DialogBody,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ Drawer,
13
+ DrawerBody,
14
+ DrawerContent,
15
+ DrawerDescription,
16
+ DrawerHeader,
17
+ DrawerTitle,
18
+ JsonViewer,
19
+ Page,
20
+ PowerSearch,
21
+ StatusTag,
22
+ TransferList,
23
+ emptyPowerSearchQuery,
24
+ formatPowerSearch,
25
+ matchPowerSearch,
26
+ type BreadcrumbItem,
27
+ type PowerSearchField,
28
+ type PowerSearchQuery,
29
+ type TransferListItem,
30
+ } from '@gntik-ai/ui';
31
+ import { useMemo, useState } from 'react';
32
+ import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
33
+ import {
34
+ explorerBreadcrumbs,
35
+ explorerColumns,
36
+ explorerDefaultColumns,
37
+ explorerFields,
38
+ explorerRows,
39
+ type ExplorerField,
40
+ type ExplorerRow,
41
+ } from './data';
42
+
43
+ export interface DataExplorerProps {
44
+ title: string;
45
+ /** Dataset name shown in the description, e.g. "request_logs". */
46
+ dataset: string;
47
+ rows: ExplorerRow[];
48
+ /** PowerSearch fields (structured filters) of the dataset. */
49
+ fields: PowerSearchField<ExplorerField>[];
50
+ /** Columns the chooser offers, in default order. */
51
+ columns: Array<TransferListItem & { value: ExplorerField }>;
52
+ /** Visible columns initially, in order. */
53
+ defaultColumns: ExplorerField[];
54
+ /** Initial query (e.g. from the URL). */
55
+ defaultQuery: PowerSearchQuery<ExplorerField>;
56
+ /** Receives the filtered rows and visible columns; hidden when omitted. */
57
+ onExport: (rows: ExplorerRow[], columns: ExplorerField[]) => void;
58
+ breadcrumbs: BreadcrumbItem[];
59
+ currentHref: string;
60
+ /** ConsoleShell props (app nav, user, workspaces, sidebar footer, topbar…). */
61
+ shell: Omit<ConsoleShellProps, 'children'>;
62
+ }
63
+
64
+ const statusKey = (code: number) => (code >= 500 ? 'failed' : code >= 400 ? 'degraded' : 'succeeded');
65
+
66
+ function cellFor(field: ExplorerField): DataTableColumn<ExplorerRow>['cell'] {
67
+ if (field === 'status') return (r) => <StatusTag size="sm" status={statusKey(r.status)} label={String(r.status)} />;
68
+ if (field === 'durationMs') return (r) => `${r.durationMs} ms`;
69
+ if (field === 'bytes') return (r) => `${(r.bytes / 1024).toFixed(1)} KB`;
70
+ return undefined;
71
+ }
72
+
73
+ /**
74
+ * Data explorer: ConsoleShell + Page with a PowerSearch over a 10k-row DataTable (client
75
+ * filtering, sorting and pagination), a column chooser (TransferList in a Dialog: show, hide
76
+ * and reorder) and a row inspector (Drawer + JsonViewer).
77
+ */
78
+ export default function DataExplorerPage(props: Partial<DataExplorerProps>) {
79
+ const {
80
+ title = 'Data explorer',
81
+ dataset = 'request_logs',
82
+ rows = explorerRows,
83
+ fields = explorerFields,
84
+ columns: columnItems = explorerColumns,
85
+ defaultColumns = explorerDefaultColumns,
86
+ defaultQuery,
87
+ onExport,
88
+ breadcrumbs = explorerBreadcrumbs,
89
+ currentHref = '/data',
90
+ shell,
91
+ } = props;
92
+ const [query, setQuery] = useState<PowerSearchQuery<ExplorerField>>(
93
+ () => defaultQuery ?? emptyPowerSearchQuery<ExplorerField>(),
94
+ );
95
+ const [visible, setVisible] = useState<string[]>(defaultColumns);
96
+ const [draftColumns, setDraftColumns] = useState<string[]>(defaultColumns);
97
+ const [chooserOpen, setChooserOpen] = useState(false);
98
+ const [inspected, setInspected] = useState<ExplorerRow | null>(null);
99
+ const filtered = useMemo(
100
+ () => (query.terms.length ? rows.filter((r) => matchPowerSearch(query, r)) : rows),
101
+ [rows, query],
102
+ );
103
+
104
+ const columns: DataTableColumn<ExplorerRow>[] = visible.flatMap((id) => {
105
+ const item = columnItems.find((c) => c.value === id);
106
+ if (!item) return [];
107
+ const field = item.value;
108
+ const numeric = field === 'durationMs' || field === 'bytes';
109
+ return [
110
+ {
111
+ id: field,
112
+ header: item.label,
113
+ accessor: (r: ExplorerRow) => r[field],
114
+ cell: cellFor(field),
115
+ sortable: true,
116
+ variant: numeric ? 'numeric' : field === 'timestamp' || field === 'path' ? 'mono' : undefined,
117
+ } satisfies DataTableColumn<ExplorerRow>,
118
+ ];
119
+ });
120
+
121
+ const actions = [
122
+ {
123
+ label: 'Columns',
124
+ icon: Columns3,
125
+ variant: 'secondary' as const,
126
+ onClick: () => {
127
+ setDraftColumns(visible);
128
+ setChooserOpen(true);
129
+ },
130
+ },
131
+ ...(onExport
132
+ ? [
133
+ {
134
+ label: 'Export',
135
+ icon: Download,
136
+ variant: 'primary' as const,
137
+ onClick: () => onExport(filtered, visible as ExplorerField[]),
138
+ },
139
+ ]
140
+ : []),
141
+ ];
142
+
143
+ return (
144
+ <ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
145
+ <Page
146
+ width="full"
147
+ header={
148
+ <PageHeader
149
+ breadcrumbs={null}
150
+ title={title}
151
+ description={
152
+ <>
153
+ Query <span className="font-mono">{dataset}</span>: type <span className="font-mono">field=value</span>,
154
+ comparisons like <span className="font-mono">duration&gt;500</span> or free text.
155
+ </>
156
+ }
157
+ status=""
158
+ meta={[]}
159
+ tabs={null}
160
+ actions={actions}
161
+ />
162
+ }
163
+ >
164
+ <PowerSearch<ExplorerField>
165
+ aria-label={`Filter ${dataset}`}
166
+ fields={fields}
167
+ value={query}
168
+ onValueChange={setQuery}
169
+ placeholder="Filter rows…"
170
+ />
171
+ <DataTable<ExplorerRow>
172
+ caption={`${dataset} rows`}
173
+ rows={filtered}
174
+ columns={columns}
175
+ getRowLabel={(r) => r.id}
176
+ defaultSort={{ columnId: 'timestamp', direction: 'descending' }}
177
+ defaultDensity="compact"
178
+ defaultPageSize={50}
179
+ pageSizeOptions={[25, 50, 100]}
180
+ showColumnMenu={false}
181
+ toolbar={
182
+ <p aria-live="polite" className="text-[12.5px] text-muted-foreground">
183
+ {filtered.length.toLocaleString('en-US')} of {rows.length.toLocaleString('en-US')} rows
184
+ {query.terms.length > 0 && <span className="ms-2 font-mono">{formatPowerSearch(query)}</span>}
185
+ </p>
186
+ }
187
+ rowActions={(r) => [{ label: 'Inspect row', icon: Braces, onSelect: () => setInspected(r) }]}
188
+ emptyState={
189
+ <NoResultsEmpty
190
+ entity="rows"
191
+ query={formatPowerSearch(query)}
192
+ filterCount={0}
193
+ onClearSearch={() => setQuery(emptyPowerSearchQuery<ExplorerField>())}
194
+ />
195
+ }
196
+ />
197
+ </Page>
198
+ <Dialog open={chooserOpen} onOpenChange={setChooserOpen}>
199
+ <DialogContent size="xl">
200
+ <DialogHeader>
201
+ <DialogTitle>Columns</DialogTitle>
202
+ <DialogDescription>Choose the visible columns and their order (Alt+↑/↓ reorders).</DialogDescription>
203
+ </DialogHeader>
204
+ <DialogBody>
205
+ <TransferList
206
+ items={columnItems}
207
+ value={draftColumns}
208
+ onValueChange={setDraftColumns}
209
+ reorderable
210
+ titles={{ available: 'Hidden', selected: 'Visible' }}
211
+ height={280}
212
+ />
213
+ </DialogBody>
214
+ <DialogFooter>
215
+ <Button variant="secondary" onClick={() => setChooserOpen(false)}>
216
+ Cancel
217
+ </Button>
218
+ <Button
219
+ disabled={draftColumns.length === 0}
220
+ onClick={() => {
221
+ setVisible(draftColumns);
222
+ setChooserOpen(false);
223
+ }}
224
+ >
225
+ Apply
226
+ </Button>
227
+ </DialogFooter>
228
+ </DialogContent>
229
+ </Dialog>
230
+ <Drawer open={inspected !== null} onOpenChange={(open) => !open && setInspected(null)}>
231
+ <DrawerContent size="md">
232
+ {inspected && (
233
+ <>
234
+ <DrawerHeader>
235
+ <DrawerTitle className="font-mono">{inspected.id}</DrawerTitle>
236
+ <DrawerDescription>Every field of the row, including hidden columns.</DrawerDescription>
237
+ </DrawerHeader>
238
+ <DrawerBody>
239
+ <JsonViewer key={inspected.id} data={inspected} label={`Row ${inspected.id}`} />
240
+ </DrawerBody>
241
+ </>
242
+ )}
243
+ </DrawerContent>
244
+ </Drawer>
245
+ </ConsoleShell>
246
+ );
247
+ }
@@ -0,0 +1,94 @@
1
+ import type { BreadcrumbItem, PowerSearchField, TransferListItem } from '@gntik-ai/ui';
2
+
3
+ /** One request log row of the sample dataset. */
4
+ export interface ExplorerRow {
5
+ id: string;
6
+ timestamp: string;
7
+ project: string;
8
+ region: string;
9
+ method: string;
10
+ path: string;
11
+ status: number;
12
+ durationMs: number;
13
+ bytes: number;
14
+ userAgent: string;
15
+ }
16
+
17
+ export type ExplorerField = Exclude<keyof ExplorerRow, 'id'>;
18
+
19
+ const PROJECTS = ['web-app', 'billing-api', 'auth-service', 'search-indexer', 'reports'];
20
+ const REGIONS = ['eu-west', 'us-east', 'ap-south'];
21
+ const METHODS = ['GET', 'GET', 'GET', 'POST', 'PUT', 'DELETE'];
22
+ const PATHS = ['/v1/projects', '/v1/projects/:id', '/v1/invoices', '/v1/search', '/v1/sessions', '/v1/exports'];
23
+ const STATUSES = [200, 200, 200, 200, 201, 204, 304, 400, 404, 429, 500, 503];
24
+ const AGENTS = ['web/4.2', 'cli/1.8', 'sdk-js/3.1', 'sdk-py/2.0'];
25
+
26
+ /** Deterministic pseudo-random sample of `count` request rows (newest first). */
27
+ export function generateExplorerRows(count = 10_000, end = Date.UTC(2026, 4, 4, 12)): ExplorerRow[] {
28
+ let seed = 7;
29
+ const rand = () => {
30
+ seed = (seed * 1_103_515_245 + 12_345) % 2_147_483_648;
31
+ return seed / 2_147_483_648;
32
+ };
33
+ const pick = <T>(list: readonly T[]) => list[Math.floor(rand() * list.length)] as T;
34
+ return Array.from({ length: count }, (_, i) => {
35
+ const status = pick(STATUSES);
36
+ return {
37
+ id: `req_${String(count - i).padStart(5, '0')}`,
38
+ timestamp: new Date(end - i * 37_000).toISOString(),
39
+ project: pick(PROJECTS),
40
+ region: pick(REGIONS),
41
+ method: pick(METHODS),
42
+ path: pick(PATHS),
43
+ status,
44
+ durationMs: Math.round((status >= 500 ? 900 : 40) + rand() * 600),
45
+ bytes: Math.round(200 + rand() * 48_000),
46
+ userAgent: pick(AGENTS),
47
+ };
48
+ });
49
+ }
50
+
51
+ export const explorerRows: ExplorerRow[] = generateExplorerRows();
52
+
53
+ export const explorerFields: PowerSearchField<ExplorerField>[] = [
54
+ {
55
+ key: 'status',
56
+ label: 'status',
57
+ type: 'number',
58
+ values: ['200', '201', '204', '304', '400', '404', '429', '500', '503'],
59
+ description: 'HTTP status',
60
+ },
61
+ { key: 'project', label: 'project', type: 'enum', values: PROJECTS },
62
+ { key: 'region', label: 'region', type: 'enum', values: REGIONS },
63
+ { key: 'method', label: 'method', type: 'enum', values: ['GET', 'POST', 'PUT', 'DELETE'] },
64
+ { key: 'path', label: 'path', values: PATHS },
65
+ { key: 'durationMs', label: 'duration', type: 'number', description: 'Milliseconds' },
66
+ { key: 'bytes', label: 'bytes', type: 'number', description: 'Response size' },
67
+ { key: 'userAgent', label: 'agent', values: AGENTS },
68
+ ];
69
+
70
+ export const explorerColumns: Array<TransferListItem & { value: ExplorerField }> = [
71
+ { value: 'timestamp', label: 'Time', description: 'When the request arrived (UTC)' },
72
+ { value: 'method', label: 'Method' },
73
+ { value: 'path', label: 'Path', description: 'Route template' },
74
+ { value: 'status', label: 'Status', description: 'HTTP status code' },
75
+ { value: 'durationMs', label: 'Duration', description: 'Milliseconds' },
76
+ { value: 'project', label: 'Project' },
77
+ { value: 'region', label: 'Region' },
78
+ { value: 'bytes', label: 'Size', description: 'Response bytes' },
79
+ { value: 'userAgent', label: 'Client', description: 'User agent' },
80
+ ];
81
+
82
+ export const explorerDefaultColumns: ExplorerField[] = [
83
+ 'timestamp',
84
+ 'method',
85
+ 'path',
86
+ 'status',
87
+ 'durationMs',
88
+ 'project',
89
+ ];
90
+
91
+ export const explorerBreadcrumbs: BreadcrumbItem[] = [
92
+ { label: 'Data', href: '/data' },
93
+ { label: 'request_logs', mono: true },
94
+ ];