@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,32 @@
1
+ import { render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { expectNoAxeViolations } from '../test/a11y';
5
+ import SettingsMembersPage from './Page';
6
+
7
+ describe('SettingsMembersPage', () => {
8
+ it('renders members, invitations and the role matrix', () => {
9
+ render(<SettingsMembersPage />);
10
+ expect(screen.getByRole('heading', { level: 1, name: 'Members' })).toBeInTheDocument();
11
+ expect(screen.getByRole('table', { name: 'Workspace members' })).toBeInTheDocument();
12
+ expect(screen.getByRole('heading', { name: 'Pending invitations' })).toBeInTheDocument();
13
+ const matrix = screen.getByRole('table', { name: 'Permissions by role' });
14
+ expect(within(matrix).getByRole('rowheader', { name: 'Delete the workspace' })).toBeInTheDocument();
15
+ }, 15000);
16
+
17
+ it('opens the invite dialog from the header and sends invitations', async () => {
18
+ const onInvite = vi.fn();
19
+ render(<SettingsMembersPage onInvite={onInvite} />);
20
+ await userEvent.click(screen.getAllByRole('button', { name: 'Invite members' })[0]!);
21
+ const dialog = await screen.findByRole('dialog', { name: 'Invite members' });
22
+ await userEvent.type(within(dialog).getByLabelText(/Email addresses/), 'new.person@example.com,');
23
+ await userEvent.click(within(dialog).getByRole('button', { name: 'Send invitation' }));
24
+ expect(onInvite).toHaveBeenCalledWith(expect.objectContaining({ emails: ['new.person@example.com'] }));
25
+ expect(await screen.findByText('new.person@example.com')).toBeInTheDocument();
26
+ }, 15000);
27
+
28
+ it('has no axe violations', async () => {
29
+ const { container } = render(<SettingsMembersPage />);
30
+ await expectNoAxeViolations(container);
31
+ }, 15000);
32
+ });
@@ -0,0 +1,140 @@
1
+ import {
2
+ InviteMembersDialog,
3
+ MembersTable,
4
+ PendingInvitations,
5
+ type Invitation,
6
+ type InvitePayload,
7
+ type Member,
8
+ type RoleOption,
9
+ } from '@gntik-ai/blocks';
10
+ import { Check, Minus, UserPlus } from '@gntik-ai/icons';
11
+ import { Section, Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@gntik-ai/ui';
12
+ import { useState } from 'react';
13
+ import { SettingsFrame, type SettingsFrameOptions } from '../settings-profile/SettingsFrame';
14
+ import {
15
+ invitations as defaultInvitations,
16
+ members as defaultMembers,
17
+ rolePermissions as defaultPermissions,
18
+ roles as defaultRoles,
19
+ type RolePermission,
20
+ } from './data';
21
+
22
+ export interface SettingsMembersProps extends SettingsFrameOptions {
23
+ members: Member[];
24
+ invitations: Invitation[];
25
+ roles: RoleOption[];
26
+ /** Rows of the role matrix. */
27
+ permissions: RolePermission[];
28
+ /** Id of the signed-in member. */
29
+ currentUserId: string;
30
+ /** Sends invitations. A rejection keeps the dialog open with its message. */
31
+ onInvite: (payload: InvitePayload) => void | Promise<void>;
32
+ onRoleChange: (memberId: string, role: string) => void;
33
+ onRemove: (member: Member) => void;
34
+ onResendInvitation: (invitation: Invitation) => void | Promise<void>;
35
+ onRevokeInvitation: (invitation: Invitation) => void;
36
+ }
37
+
38
+ /** Members settings: MembersTable, PendingInvitations, InviteMembersDialog (header action), role matrix. */
39
+ export default function SettingsMembersPage({
40
+ members = defaultMembers,
41
+ invitations = defaultInvitations,
42
+ roles = defaultRoles,
43
+ permissions = defaultPermissions,
44
+ currentUserId = 'u1',
45
+ onInvite,
46
+ onRoleChange,
47
+ onRemove,
48
+ onResendInvitation,
49
+ onRevokeInvitation,
50
+ ...frame
51
+ }: Partial<SettingsMembersProps>) {
52
+ const [inviteOpen, setInviteOpen] = useState(false);
53
+ const [sent, setSent] = useState<Invitation[]>([]);
54
+ const roleLabel = (value: string) => roles.find((r) => r.value === value)?.label ?? value;
55
+
56
+ const invite = async (payload: InvitePayload) => {
57
+ await onInvite?.(payload);
58
+ const now = new Date().toISOString();
59
+ setSent((list) => [
60
+ ...payload.emails.map((email, i) => ({
61
+ id: `new-${list.length + i}-${email}`,
62
+ email,
63
+ role: roleLabel(payload.role),
64
+ invitedBy: 'You',
65
+ invitedAt: now,
66
+ })),
67
+ ...list,
68
+ ]);
69
+ };
70
+
71
+ return (
72
+ <SettingsFrame
73
+ page="members"
74
+ width="wide"
75
+ description="Invite people to the workspace and manage what they can do."
76
+ actions={[{ label: 'Invite members', icon: UserPlus, variant: 'primary', onClick: () => setInviteOpen(true) }]}
77
+ {...frame}
78
+ >
79
+ <Section title="Members" description={`${members.length} people have access to this workspace.`}>
80
+ <MembersTable
81
+ members={members}
82
+ roles={roles}
83
+ currentUserId={currentUserId}
84
+ onRoleChange={onRoleChange}
85
+ onRemove={onRemove}
86
+ />
87
+ </Section>
88
+ <PendingInvitations
89
+ invitations={[...sent, ...invitations]}
90
+ onResend={onResendInvitation}
91
+ onRevoke={onRevokeInvitation}
92
+ />
93
+ <Section title="Roles" description="What each role can do. Owners can change any member's role.">
94
+ <Table className="min-w-[560px]">
95
+ <TableCaption srOnly>Permissions by role</TableCaption>
96
+ <TableHeader>
97
+ <TableRow>
98
+ <TableHead>Permission</TableHead>
99
+ {roles.map((r) => (
100
+ <TableHead key={r.value} align="center">
101
+ {r.label}
102
+ </TableHead>
103
+ ))}
104
+ </TableRow>
105
+ </TableHeader>
106
+ <TableBody>
107
+ {permissions.map((p) => (
108
+ <TableRow key={p.id}>
109
+ <TableHead scope="row" className="font-normal text-foreground">
110
+ {p.label}
111
+ </TableHead>
112
+ {roles.map((r) => {
113
+ const allowed = p.roles.includes(r.value);
114
+ const Glyph = allowed ? Check : Minus;
115
+ return (
116
+ <TableCell key={r.value} align="center">
117
+ <Glyph
118
+ size={15}
119
+ aria-hidden
120
+ className={allowed ? 'mx-auto text-primary-text' : 'mx-auto text-muted-foreground'}
121
+ />
122
+ <span className="sr-only">{allowed ? 'Allowed' : 'Not allowed'}</span>
123
+ </TableCell>
124
+ );
125
+ })}
126
+ </TableRow>
127
+ ))}
128
+ </TableBody>
129
+ </Table>
130
+ </Section>
131
+ <InviteMembersDialog
132
+ trigger={null}
133
+ open={inviteOpen}
134
+ onOpenChange={setInviteOpen}
135
+ roles={roles}
136
+ onInvite={invite}
137
+ />
138
+ </SettingsFrame>
139
+ );
140
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ memberRoles,
3
+ sampleInvitations,
4
+ sampleMembers,
5
+ type Invitation,
6
+ type Member,
7
+ type RoleOption,
8
+ } from '@gntik-ai/blocks';
9
+
10
+ export const members: Member[] = sampleMembers;
11
+ export const invitations: Invitation[] = sampleInvitations;
12
+ export const roles: RoleOption[] = memberRoles;
13
+
14
+ export interface RolePermission {
15
+ id: string;
16
+ label: string;
17
+ /** Role values that have this permission. */
18
+ roles: string[];
19
+ }
20
+
21
+ export const rolePermissions: RolePermission[] = [
22
+ { id: 'view', label: 'View projects and deployments', roles: ['owner', 'admin', 'member', 'viewer'] },
23
+ { id: 'deploy', label: 'Create and deploy projects', roles: ['owner', 'admin', 'member'] },
24
+ { id: 'secrets', label: 'Manage environment secrets', roles: ['owner', 'admin'] },
25
+ { id: 'members', label: 'Invite and remove members', roles: ['owner', 'admin'] },
26
+ { id: 'billing', label: 'Manage billing and plans', roles: ['owner'] },
27
+ { id: 'delete', label: 'Delete the workspace', roles: ['owner'] },
28
+ ];
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Members settings',
5
+ family: 'Settings',
6
+ priority: 'P1',
7
+ status: 'beta',
8
+ description:
9
+ 'Workspace members with inline roles, pending invitations, an invite dialog and a role permission matrix.',
10
+ layout: 'SettingsLayout',
11
+ blocks: ['app-sidebar', 'app-topbar', 'page-header', 'members-table', 'pending-invitations', 'invite-members-dialog'],
12
+ };
@@ -0,0 +1,31 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { expectNoAxeViolations } from '../test/a11y';
5
+ import SettingsNotificationsPage from './Page';
6
+
7
+ describe('SettingsNotificationsPage', () => {
8
+ it('renders the matrix', () => {
9
+ render(<SettingsNotificationsPage />);
10
+ expect(screen.getByRole('heading', { level: 1, name: 'Notifications' })).toBeInTheDocument();
11
+ expect(screen.getByRole('table', { name: 'Notification preferences' })).toBeInTheDocument();
12
+ }, 15000);
13
+
14
+ it('saves a changed preference and can discard', async () => {
15
+ const onSave = vi.fn();
16
+ render(<SettingsNotificationsPage onSave={onSave} />);
17
+ const cell = screen.getByRole('checkbox', { name: 'Member joined by Push' });
18
+ await userEvent.click(cell);
19
+ expect(screen.getByRole('button', { name: 'Save changes' })).toBeEnabled();
20
+ await userEvent.click(screen.getByRole('button', { name: 'Discard' }));
21
+ expect(screen.getByRole('button', { name: 'Save changes' })).toBeDisabled();
22
+ await userEvent.click(cell);
23
+ await userEvent.click(screen.getByRole('button', { name: 'Save changes' }));
24
+ expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ 'member-joined': ['push'] }));
25
+ }, 15000);
26
+
27
+ it('has no axe violations', async () => {
28
+ const { container } = render(<SettingsNotificationsPage />);
29
+ await expectNoAxeViolations(container);
30
+ }, 15000);
31
+ });
@@ -0,0 +1,71 @@
1
+ import {
2
+ NotificationMatrix,
3
+ StickyActionBar,
4
+ type NotificationChannel,
5
+ type NotificationEvent,
6
+ type NotificationMatrixValue,
7
+ } from '@gntik-ai/blocks';
8
+ import { Section } from '@gntik-ai/ui';
9
+ import { useState } from 'react';
10
+ import { SettingsFrame, type SettingsFrameOptions } from '../settings-profile/SettingsFrame';
11
+ import { channels as defaultChannels, events as defaultEvents, preferences as defaultPreferences } from './data';
12
+
13
+ export interface SettingsNotificationsProps extends SettingsFrameOptions {
14
+ channels: NotificationChannel[];
15
+ events: NotificationEvent[];
16
+ /** Saved preferences: event id → enabled channel ids. */
17
+ value: NotificationMatrixValue;
18
+ /** Persists the matrix. A returned promise shows the saving state. */
19
+ onSave: (value: NotificationMatrixValue) => void | Promise<void>;
20
+ }
21
+
22
+ const key = (v: NotificationMatrixValue) =>
23
+ JSON.stringify(
24
+ Object.keys(v)
25
+ .sort()
26
+ .map((k) => [k, [...(v[k] ?? [])].sort()]),
27
+ );
28
+
29
+ /** Notification settings: NotificationMatrix with a StickyActionBar. */
30
+ export default function SettingsNotificationsPage({
31
+ channels = defaultChannels,
32
+ events = defaultEvents,
33
+ value: saved = defaultPreferences,
34
+ onSave,
35
+ ...frame
36
+ }: Partial<SettingsNotificationsProps>) {
37
+ const [base, setBase] = useState(saved);
38
+ const [draft, setDraft] = useState(saved);
39
+ const [saving, setSaving] = useState(false);
40
+ const dirty = key(draft) !== key(base);
41
+
42
+ const save = async () => {
43
+ setSaving(true);
44
+ try {
45
+ await onSave?.(draft);
46
+ setBase(draft);
47
+ } finally {
48
+ setSaving(false);
49
+ }
50
+ };
51
+
52
+ return (
53
+ <SettingsFrame
54
+ page="notifications"
55
+ width="wide"
56
+ description="Choose how you hear about what happens in your workspace."
57
+ {...frame}
58
+ >
59
+ <Section title="Notification preferences" description="Security alerts always reach your email.">
60
+ <NotificationMatrix channels={channels} events={events} value={draft} onValueChange={setDraft} />
61
+ </Section>
62
+ <StickyActionBar
63
+ dirty={dirty}
64
+ saving={saving}
65
+ onSave={() => void save()}
66
+ onCancel={() => setDraft(base)}
67
+ aria-label="Notification changes"
68
+ />
69
+ </SettingsFrame>
70
+ );
71
+ }
@@ -0,0 +1,12 @@
1
+ import {
2
+ notificationChannels,
3
+ notificationDefaults,
4
+ notificationEvents,
5
+ type NotificationChannel,
6
+ type NotificationEvent,
7
+ type NotificationMatrixValue,
8
+ } from '@gntik-ai/blocks';
9
+
10
+ export const channels: NotificationChannel[] = notificationChannels;
11
+ export const events: NotificationEvent[] = notificationEvents;
12
+ export const preferences: NotificationMatrixValue = notificationDefaults;
@@ -0,0 +1,11 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Notification settings',
5
+ family: 'Settings',
6
+ priority: 'P2',
7
+ status: 'beta',
8
+ description: 'Event × channel notification matrix with bulk row and column toggles and a sticky save bar.',
9
+ layout: 'SettingsLayout',
10
+ blocks: ['app-sidebar', 'app-topbar', 'page-header', 'notification-matrix', 'sticky-action-bar'],
11
+ };
@@ -0,0 +1,9 @@
1
+ import { render } from '@testing-library/react';
2
+ import { expect, it } from 'vitest';
3
+ import SettingsProfilePage from './Page';
4
+ import { profileDOM } from './__tests__/dom';
5
+
6
+ it('preserves fixture (a): the default editable profile DOM', async () => {
7
+ const { container } = render(<SettingsProfilePage />);
8
+ await expect(profileDOM(container)).toMatchFileSnapshot('./fixtures/editable.snap');
9
+ });
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import { expect, it } from 'vitest';
4
+ import SettingsProfilePage from './Page';
5
+ import { profileDOM } from './__tests__/dom';
6
+
7
+ it('fixture (b): explicit editable mode equals the unchanged fixture (a)', () => {
8
+ const { container } = render(<SettingsProfilePage mode="editable" />);
9
+ expect(profileDOM(container)).toEqual(readFileSync('src/settings-profile/fixtures/editable.snap', 'utf8'));
10
+ expect(screen.getByRole('button', { name: 'Discard' })).toBeInTheDocument();
11
+ expect(screen.getByRole('button', { name: 'Save changes' })).toBeInTheDocument();
12
+ });
13
+
14
+ it('switches modes without breaking hook order', () => {
15
+ const { container, rerender } = render(<SettingsProfilePage mode="editable" />);
16
+ expect(screen.getByRole('textbox', { name: 'Full name' })).toBeInTheDocument();
17
+ rerender(<SettingsProfilePage mode="read-only" />);
18
+ expect(within(container.querySelector<HTMLElement>('#settings-content')!).queryAllByRole('textbox')).toHaveLength(0);
19
+ rerender(<SettingsProfilePage mode="editable" />);
20
+ expect(screen.getByRole('textbox', { name: 'Full name' })).toBeInTheDocument();
21
+ });
@@ -0,0 +1,41 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { expectNoAxeViolations } from '../test/a11y';
5
+ import SettingsProfilePage from './Page';
6
+
7
+ describe('SettingsProfilePage', () => {
8
+ it('renders the settings frame with the profile page selected', () => {
9
+ render(<SettingsProfilePage />);
10
+ expect(screen.getByRole('heading', { level: 1, name: 'Profile' })).toBeInTheDocument();
11
+ expect(screen.getByRole('navigation', { name: 'Settings' })).toBeInTheDocument();
12
+ expect(screen.getByRole('button', { name: /^Profile/ })).toHaveAttribute('aria-current', 'page');
13
+ expect(screen.getByRole('heading', { name: 'Personal details' })).toBeInTheDocument();
14
+ }, 15000);
15
+
16
+ it('enables Save after an edit and saves the draft', async () => {
17
+ const onSave = vi.fn();
18
+ render(<SettingsProfilePage onSave={onSave} />);
19
+ const save = screen.getByRole('button', { name: 'Save changes' });
20
+ expect(save).toBeDisabled();
21
+ const name = screen.getByLabelText('Full name');
22
+ await userEvent.clear(name);
23
+ await userEvent.type(name, 'Avery Q. Collins');
24
+ expect(save).toBeEnabled();
25
+ await userEvent.click(save);
26
+ expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ name: 'Avery Q. Collins' }));
27
+ expect(await screen.findByText('All changes saved')).toBeInTheDocument();
28
+ }, 15000);
29
+
30
+ it('reports section navigation', async () => {
31
+ const onNavigate = vi.fn();
32
+ render(<SettingsProfilePage onNavigate={onNavigate} />);
33
+ await userEvent.click(screen.getByRole('button', { name: /^Billing/ }));
34
+ expect(onNavigate).toHaveBeenCalledWith('billing');
35
+ }, 15000);
36
+
37
+ it('has no axe violations', async () => {
38
+ const { container } = render(<SettingsProfilePage />);
39
+ await expectNoAxeViolations(container);
40
+ }, 15000);
41
+ });
@@ -0,0 +1,133 @@
1
+ import { FormSection, ProfileHeader, StickyActionBar, type Profile } from '@gntik-ai/blocks';
2
+ import { Field, FieldDescription, FieldLabel, Input, SimpleSelect, Stack } from '@gntik-ai/ui';
3
+ import { useState } from 'react';
4
+ import {
5
+ locales as defaultLocales,
6
+ profile as defaultProfile,
7
+ profileValues,
8
+ timeZones as defaultTimeZones,
9
+ type ProfileFormValues,
10
+ type SelectOption,
11
+ } from './data';
12
+ import { SettingsFrame, type SettingsFrameOptions } from './SettingsFrame';
13
+ import { ReadOnlyProfile, type ReadOnlyProfileOptions } from './ReadOnlyProfile';
14
+
15
+ export interface SettingsProfileProps extends SettingsFrameOptions, ReadOnlyProfileOptions {
16
+ /** Defaults to 'editable'; 'read-only' shows details without form controls or save actions. */
17
+ mode?: 'editable' | 'read-only';
18
+ profile: Profile;
19
+ /** Saved form values. */
20
+ values: ProfileFormValues;
21
+ locales: SelectOption[];
22
+ timeZones: SelectOption[];
23
+ /** Persists the form. A returned promise shows the saving state. */
24
+ onSave: (values: ProfileFormValues) => void | Promise<void>;
25
+ onAvatarChange: (file: File) => void;
26
+ }
27
+
28
+ /** Profile settings: ProfileHeader, personal and regional FormSections, StickyActionBar. */
29
+ export default function SettingsProfilePage({
30
+ mode = 'editable',
31
+ identity,
32
+ context,
33
+ contextFallback,
34
+ roles,
35
+ emptyRolesMessage,
36
+ ...props
37
+ }: Partial<SettingsProfileProps>) {
38
+ if (mode === 'read-only') {
39
+ return (
40
+ <ReadOnlyProfile
41
+ {...props}
42
+ identity={identity}
43
+ context={context}
44
+ contextFallback={contextFallback}
45
+ roles={roles}
46
+ emptyRolesMessage={emptyRolesMessage}
47
+ />
48
+ );
49
+ }
50
+ return <EditableProfile {...props} />;
51
+ }
52
+
53
+ function EditableProfile({
54
+ profile = defaultProfile,
55
+ values: saved = profileValues,
56
+ locales = defaultLocales,
57
+ timeZones = defaultTimeZones,
58
+ onSave,
59
+ onAvatarChange,
60
+ ...frame
61
+ }: Partial<SettingsProfileProps>) {
62
+ const [base, setBase] = useState(saved);
63
+ const [draft, setDraft] = useState(saved);
64
+ const [saving, setSaving] = useState(false);
65
+ const dirty = (Object.keys(draft) as Array<keyof ProfileFormValues>).some((k) => draft[k] !== base[k]);
66
+ const set = (key: keyof ProfileFormValues) => (value: string | null) =>
67
+ setDraft((d) => ({ ...d, [key]: value ?? '' }));
68
+
69
+ const save = async () => {
70
+ setSaving(true);
71
+ try {
72
+ await onSave?.(draft);
73
+ setBase(draft);
74
+ } finally {
75
+ setSaving(false);
76
+ }
77
+ };
78
+
79
+ return (
80
+ <SettingsFrame page="profile" description="How other members see you across the workspace." {...frame}>
81
+ <ProfileHeader
82
+ profile={{ ...profile, name: base.name }}
83
+ headingLevel="h2"
84
+ onEdit={null}
85
+ onAvatarChange={onAvatarChange}
86
+ />
87
+ <Stack gap={8}>
88
+ <FormSection
89
+ title="Personal details"
90
+ description="Your name and email are visible to everyone in the workspace."
91
+ divided
92
+ >
93
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
94
+ <Field>
95
+ <FieldLabel>Full name</FieldLabel>
96
+ <Input value={draft.name} onValueChange={set('name')} autoComplete="name" />
97
+ </Field>
98
+ <Field>
99
+ <FieldLabel>Email</FieldLabel>
100
+ <Input type="email" value={draft.email} onValueChange={set('email')} autoComplete="email" />
101
+ <FieldDescription>We send a confirmation link when it changes.</FieldDescription>
102
+ </Field>
103
+ </div>
104
+ </FormSection>
105
+ <FormSection title="Language and region" description="Used for dates, numbers and email digests." divided>
106
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
107
+ <SimpleSelect
108
+ label="Locale"
109
+ items={locales}
110
+ value={draft.locale}
111
+ onValueChange={set('locale')}
112
+ className="w-full"
113
+ />
114
+ <SimpleSelect
115
+ label="Time zone"
116
+ items={timeZones}
117
+ value={draft.timeZone}
118
+ onValueChange={set('timeZone')}
119
+ className="w-full"
120
+ />
121
+ </div>
122
+ </FormSection>
123
+ </Stack>
124
+ <StickyActionBar
125
+ dirty={dirty}
126
+ saving={saving}
127
+ onSave={() => void save()}
128
+ onCancel={() => setDraft(base)}
129
+ aria-label="Profile changes"
130
+ />
131
+ </SettingsFrame>
132
+ );
133
+ }