@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,243 @@
1
+ import { NoResultsEmpty, PageHeader, SectionHeader } from '@gntik-ai/blocks';
2
+ import { Search } from '@gntik-ai/icons';
3
+ import {
4
+ Checkbox,
5
+ CheckboxGroup,
6
+ Grid,
7
+ GridItem,
8
+ Input,
9
+ Kbd,
10
+ Link,
11
+ Page,
12
+ Stack,
13
+ useI18n,
14
+ type BreadcrumbItem,
15
+ } from '@gntik-ai/ui';
16
+ import { useId, useRef, useState, type KeyboardEvent } from 'react';
17
+ import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
18
+ import {
19
+ searchBreadcrumbs,
20
+ searchDefaultQuery,
21
+ searchFacets,
22
+ searchHits,
23
+ searchTypeLabels,
24
+ searchTypeOrder,
25
+ type SearchFacet,
26
+ type SearchHit,
27
+ } from './data';
28
+
29
+ export type SearchSelection = Record<SearchFacet['id'], string[]>;
30
+
31
+ export interface SearchResultsProps {
32
+ /** The index to search (a product passes its server results and keeps `query` in sync). */
33
+ hits: SearchHit[];
34
+ facets: SearchFacet[];
35
+ defaultQuery: string;
36
+ onQueryChange: (query: string) => void;
37
+ onSelectionChange: (selection: SearchSelection) => void;
38
+ breadcrumbs: BreadcrumbItem[];
39
+ currentHref: string;
40
+ /** ConsoleShell props (app nav, user, workspaces, sidebar footer, topbar…). */
41
+ shell: Omit<ConsoleShellProps, 'children'>;
42
+ }
43
+
44
+ const EMPTY_SELECTION: SearchSelection = { type: [], owner: [] };
45
+
46
+ const facetValue = (hit: SearchHit, facet: SearchFacet['id']) => (facet === 'type' ? hit.type : hit.owner);
47
+
48
+ /** Hits whose title or snippet contain the query (case-insensitive). */
49
+ export function matchQuery(hits: readonly SearchHit[], query: string) {
50
+ const q = query.trim().toLowerCase();
51
+ return q ? hits.filter((h) => `${h.title} ${h.snippet}`.toLowerCase().includes(q)) : [...hits];
52
+ }
53
+
54
+ /** Search results: ConsoleShell + Page with Checkbox-group facets and results grouped by type. */
55
+ export default function SearchResultsPage(props: Partial<SearchResultsProps>) {
56
+ const { t } = useI18n();
57
+ const {
58
+ hits = searchHits,
59
+ facets = searchFacets,
60
+ defaultQuery = searchDefaultQuery,
61
+ onQueryChange,
62
+ onSelectionChange,
63
+ breadcrumbs = searchBreadcrumbs,
64
+ currentHref = '/search',
65
+ shell,
66
+ } = props;
67
+ const id = useId();
68
+ const resultsRef = useRef<HTMLDivElement>(null);
69
+ const [query, setQuery] = useState(defaultQuery);
70
+ const [selection, setSelection] = useState<SearchSelection>(EMPTY_SELECTION);
71
+
72
+ const matched = matchQuery(hits, query);
73
+ const results = matched.filter((h) =>
74
+ (Object.keys(selection) as Array<SearchFacet['id']>).every(
75
+ (f) => selection[f].length === 0 || selection[f].includes(facetValue(h, f)),
76
+ ),
77
+ );
78
+ const groups = searchTypeOrder
79
+ .map((type) => ({ type, hits: results.filter((h) => h.type === type) }))
80
+ .filter((g) => g.hits.length > 0);
81
+ const filterCount = selection.type.length + selection.owner.length;
82
+
83
+ const changeQuery = (next: string) => {
84
+ setQuery(next);
85
+ onQueryChange?.(next);
86
+ };
87
+ const changeSelection = (next: SearchSelection) => {
88
+ setSelection(next);
89
+ onSelectionChange?.(next);
90
+ };
91
+
92
+ /** Arrow keys move through every result link; Home / End jump to the ends. */
93
+ const focusResult = (move: 'first' | 'last' | 1 | -1) => {
94
+ const links = Array.from(resultsRef.current?.querySelectorAll<HTMLAnchorElement>('a[data-search-result]') ?? []);
95
+ if (links.length === 0) return;
96
+ const at = links.indexOf(document.activeElement as HTMLAnchorElement);
97
+ const next =
98
+ move === 'first' ? 0 : move === 'last' ? links.length - 1 : Math.min(links.length - 1, Math.max(0, at + move));
99
+ links[next]?.focus();
100
+ };
101
+ const onResultsKey = (e: KeyboardEvent<HTMLDivElement>) => {
102
+ const keys: Record<string, 'first' | 'last' | 1 | -1> = { ArrowDown: 1, ArrowUp: -1, Home: 'first', End: 'last' };
103
+ const move = keys[e.key];
104
+ if (move === undefined) return;
105
+ e.preventDefault();
106
+ focusResult(move);
107
+ };
108
+
109
+ return (
110
+ <ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
111
+ <Page
112
+ width="wide"
113
+ header={
114
+ <PageHeader
115
+ breadcrumbs={null}
116
+ title="Search"
117
+ description="Projects, deployments, members and documents across the workspace."
118
+ status=""
119
+ meta={[]}
120
+ tabs={null}
121
+ actions={[]}
122
+ />
123
+ }
124
+ >
125
+ <div role="search" aria-label="Workspace search">
126
+ <Input
127
+ type="search"
128
+ aria-label="Search the workspace"
129
+ aria-describedby={`${id}-hint`}
130
+ leadingIcon={Search}
131
+ value={query}
132
+ onValueChange={(v) => changeQuery(String(v))}
133
+ onKeyDown={(e) => {
134
+ if (e.key === 'ArrowDown') {
135
+ e.preventDefault();
136
+ focusResult('first');
137
+ }
138
+ }}
139
+ placeholder={t('common.searchPlaceholder')}
140
+ className="max-w-xl"
141
+ />
142
+ <p id={`${id}-hint`} className="mt-2 text-[12px] text-muted-foreground">
143
+ <Kbd>↓</Kbd> moves into the results · <Kbd>↑</Kbd> <Kbd>↓</Kbd> step through them
144
+ </p>
145
+ </div>
146
+ <Grid cols={{ base: 1, lg: 4 }} gap={6}>
147
+ <GridItem as="aside" aria-label={t('filters.label')} span={{ base: 1, lg: 1 }}>
148
+ <Stack gap={6}>
149
+ {facets.map((facet) => (
150
+ <div key={facet.id}>
151
+ <p
152
+ id={`${id}-${facet.id}`}
153
+ className="mb-2.5 font-mono text-[10.5px] font-medium tracking-[0.12em] text-muted-foreground uppercase"
154
+ >
155
+ {facet.label}
156
+ </p>
157
+ <CheckboxGroup
158
+ aria-labelledby={`${id}-${facet.id}`}
159
+ value={selection[facet.id]}
160
+ onValueChange={(value) => changeSelection({ ...selection, [facet.id]: value })}
161
+ >
162
+ {facet.options.map((o) => {
163
+ const count = matched.filter((h) => facetValue(h, facet.id) === o.value).length;
164
+ return (
165
+ <Checkbox
166
+ key={o.value}
167
+ name={facet.id}
168
+ value={o.value}
169
+ label={
170
+ <>
171
+ {o.label} <span className="font-mono text-[11px] text-muted-foreground">{count}</span>
172
+ </>
173
+ }
174
+ />
175
+ );
176
+ })}
177
+ </CheckboxGroup>
178
+ </div>
179
+ ))}
180
+ </Stack>
181
+ </GridItem>
182
+ <GridItem span={{ base: 1, lg: 3 }}>
183
+ <p aria-live="polite" className="mb-4 text-[12.5px] text-muted-foreground">
184
+ <span className="font-mono text-foreground tabular-nums">{results.length}</span>{' '}
185
+ {results.length === 1 ? 'result' : 'results'}
186
+ {query.trim() && (
187
+ <>
188
+ {' '}
189
+ for “<span className="font-mono">{query.trim()}</span>”
190
+ </>
191
+ )}
192
+ </p>
193
+ {groups.length === 0 ? (
194
+ <NoResultsEmpty
195
+ titleAs="h2"
196
+ entity="results"
197
+ query={query.trim()}
198
+ filterCount={filterCount}
199
+ onClearFilters={() => changeSelection(EMPTY_SELECTION)}
200
+ onClearSearch={() => changeQuery('')}
201
+ />
202
+ ) : (
203
+ <div ref={resultsRef} onKeyDown={onResultsKey} className="flex flex-col gap-8">
204
+ {groups.map((group) => {
205
+ const headingId = `${id}-group-${group.type}`;
206
+ return (
207
+ <section key={group.type} aria-labelledby={headingId}>
208
+ <SectionHeader
209
+ headingId={headingId}
210
+ title={searchTypeLabels[group.type]}
211
+ count={group.hits.length}
212
+ description={null}
213
+ actions={[]}
214
+ />
215
+ <ul aria-labelledby={headingId} className="mt-2 flex flex-col divide-y divide-border">
216
+ {group.hits.map((hit) => (
217
+ <li key={hit.id} className="py-3">
218
+ <Link
219
+ underline="hover"
220
+ data-search-result
221
+ href={hit.href}
222
+ className="text-[14px] font-semibold"
223
+ >
224
+ {hit.title}
225
+ </Link>
226
+ <p className="mt-0.5 text-[13px] text-muted-foreground">{hit.snippet}</p>
227
+ <p className="mt-1 font-mono text-[11px] text-muted-foreground">
228
+ {hit.meta} · {hit.owner}
229
+ </p>
230
+ </li>
231
+ ))}
232
+ </ul>
233
+ </section>
234
+ );
235
+ })}
236
+ </div>
237
+ )}
238
+ </GridItem>
239
+ </Grid>
240
+ </Page>
241
+ </ConsoleShell>
242
+ );
243
+ }
@@ -0,0 +1,130 @@
1
+ import type { BreadcrumbItem } from '@gntik-ai/ui';
2
+
3
+ export type SearchResultType = 'project' | 'deployment' | 'member' | 'document';
4
+
5
+ export interface SearchHit {
6
+ id: string;
7
+ type: SearchResultType;
8
+ title: string;
9
+ snippet: string;
10
+ href: string;
11
+ owner: string;
12
+ /** Mono context line (region, id, email…). */
13
+ meta: string;
14
+ }
15
+
16
+ export interface SearchFacet {
17
+ id: 'type' | 'owner';
18
+ label: string;
19
+ options: Array<{ value: string; label: string }>;
20
+ }
21
+
22
+ export const searchTypeLabels: Record<SearchResultType, string> = {
23
+ project: 'Projects',
24
+ deployment: 'Deployments',
25
+ member: 'Members',
26
+ document: 'Documents',
27
+ };
28
+
29
+ export const searchTypeOrder: SearchResultType[] = ['project', 'deployment', 'member', 'document'];
30
+
31
+ /** Neutral search index for the sample. */
32
+ export const searchHits: SearchHit[] = [
33
+ {
34
+ id: 'p1',
35
+ type: 'project',
36
+ title: 'billing-api',
37
+ snippet: 'Invoices, payment methods and usage metering for the workspace.',
38
+ href: '/projects/billing-api',
39
+ owner: 'Dana Whitfield',
40
+ meta: 'eu-west-1 · Node 24',
41
+ },
42
+ {
43
+ id: 'p2',
44
+ type: 'project',
45
+ title: 'billing-web',
46
+ snippet: 'Customer-facing billing portal and plan management.',
47
+ href: '/projects/billing-web',
48
+ owner: 'Sam Patel',
49
+ meta: 'us-east-1 · Vite',
50
+ },
51
+ {
52
+ id: 'p3',
53
+ type: 'project',
54
+ title: 'orders-api',
55
+ snippet: 'Order intake and fulfilment; emits billing events.',
56
+ href: '/projects/orders-api',
57
+ owner: 'Lee Chen',
58
+ meta: 'eu-central-1 · Go',
59
+ },
60
+ {
61
+ id: 'd1',
62
+ type: 'deployment',
63
+ title: 'billing-api build 412',
64
+ snippet: 'Promoted to production · health checks passed in 38 s.',
65
+ href: '/deployments/dep_412',
66
+ owner: 'Dana Whitfield',
67
+ meta: 'dep_412 · production',
68
+ },
69
+ {
70
+ id: 'd2',
71
+ type: 'deployment',
72
+ title: 'billing-web #87',
73
+ snippet: 'Preview deployment for “New invoice layout”.',
74
+ href: '/deployments/dep_087',
75
+ owner: 'Sam Patel',
76
+ meta: 'dep_087 · preview',
77
+ },
78
+ {
79
+ id: 'd3',
80
+ type: 'deployment',
81
+ title: 'billing-api build 411',
82
+ snippet: 'Rolled back: error rate above 2% in us-east-1.',
83
+ href: '/deployments/dep_411',
84
+ owner: 'Lee Chen',
85
+ meta: 'dep_411 · production',
86
+ },
87
+ {
88
+ id: 'm1',
89
+ type: 'member',
90
+ title: 'Riley Morgan',
91
+ snippet: 'Billing admin · manages invoices and payment methods.',
92
+ href: '/members/riley',
93
+ owner: 'Dana Whitfield',
94
+ meta: 'riley@example.com',
95
+ },
96
+ {
97
+ id: 'doc1',
98
+ type: 'document',
99
+ title: 'Billing runbook',
100
+ snippet: 'What to do when a billing sync fails or invoices are delayed.',
101
+ href: '/docs/billing-runbook',
102
+ owner: 'Lee Chen',
103
+ meta: 'Updated 3 days ago',
104
+ },
105
+ {
106
+ id: 'doc2',
107
+ type: 'document',
108
+ title: 'Usage-based billing guide',
109
+ snippet: 'How metered usage turns into invoice line items.',
110
+ href: '/docs/usage-billing',
111
+ owner: 'Sam Patel',
112
+ meta: 'Updated last week',
113
+ },
114
+ ];
115
+
116
+ export const searchFacets: SearchFacet[] = [
117
+ { id: 'type', label: 'Type', options: searchTypeOrder.map((t) => ({ value: t, label: searchTypeLabels[t] })) },
118
+ {
119
+ id: 'owner',
120
+ label: 'Owner',
121
+ options: ['Dana Whitfield', 'Sam Patel', 'Lee Chen'].map((name) => ({ value: name, label: name })),
122
+ },
123
+ ];
124
+
125
+ export const searchDefaultQuery = 'billing';
126
+
127
+ export const searchBreadcrumbs: BreadcrumbItem[] = [
128
+ { label: 'Acme Industries', href: '/overview' },
129
+ { label: 'Search' },
130
+ ];
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Search results',
5
+ family: 'Work',
6
+ priority: 'P2',
7
+ status: 'beta',
8
+ description:
9
+ 'Workspace search: query field, facets as Checkbox groups with counts, results grouped by type under SectionHeaders, arrow-key navigation through the results, and NoResultsEmpty.',
10
+ layout: 'SidebarLayout · Page',
11
+ blocks: ['app-sidebar', 'app-topbar', 'page-header', 'section-header', 'empty-states'],
12
+ };
@@ -0,0 +1,39 @@
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 SettingsApiKeysPage from './Page';
6
+
7
+ describe('SettingsApiKeysPage', () => {
8
+ it('renders the key list', () => {
9
+ render(<SettingsApiKeysPage />);
10
+ expect(screen.getByRole('heading', { level: 1, name: 'API keys' })).toBeInTheDocument();
11
+ expect(screen.getByRole('table', { name: 'API keys' })).toBeInTheDocument();
12
+ expect(screen.getByText('ci-deploy')).toBeInTheDocument();
13
+ }, 15000);
14
+
15
+ it('revokes a key after typing its name', async () => {
16
+ const onRevoke = vi.fn();
17
+ render(<SettingsApiKeysPage onRevoke={onRevoke} />);
18
+ await userEvent.click(screen.getByRole('button', { name: 'Actions for ci-deploy' }));
19
+ await userEvent.click(await screen.findByRole('menuitem', { name: 'Revoke key' }));
20
+ const dialog = await screen.findByRole('alertdialog', { name: 'Revoke API key?' });
21
+ const confirm = within(dialog).getByRole('button', { name: 'Revoke key' });
22
+ expect(confirm).toBeDisabled();
23
+ await userEvent.type(within(dialog).getByRole('textbox'), 'ci-deploy');
24
+ await userEvent.click(confirm);
25
+ expect(onRevoke).toHaveBeenCalledWith(expect.objectContaining({ id: 'k1' }));
26
+ expect(within(screen.getByRole('table', { name: 'API keys' })).queryByText('ci-deploy')).not.toBeInTheDocument();
27
+ }, 15000);
28
+
29
+ it('opens the create dialog from the header', async () => {
30
+ render(<SettingsApiKeysPage />);
31
+ await userEvent.click(screen.getAllByRole('button', { name: 'Create API key' })[0]!);
32
+ expect(await screen.findByRole('dialog', { name: 'Create API key' })).toBeInTheDocument();
33
+ }, 15000);
34
+
35
+ it('has no axe violations', async () => {
36
+ const { container } = render(<SettingsApiKeysPage />);
37
+ await expectNoAxeViolations(container);
38
+ }, 15000);
39
+ });
@@ -0,0 +1,171 @@
1
+ import {
2
+ ConfirmDestructive,
3
+ CreateApiKeyDialog,
4
+ DataTable,
5
+ generateDemoSecret,
6
+ type ApiKeyExpiry,
7
+ type ApiKeyScope,
8
+ type CreateApiKeyInput,
9
+ type DataTableColumn,
10
+ } from '@gntik-ai/blocks';
11
+ import { KeyRound, Plus, Trash2 } from '@gntik-ai/icons';
12
+ import { Badge, EmptyState, Timestamp } from '@gntik-ai/ui';
13
+ import { useState } from 'react';
14
+ import { SettingsFrame, type SettingsFrameOptions } from '../settings-profile/SettingsFrame';
15
+ import { apiKeys as defaultKeys, expiries as defaultExpiries, scopes as defaultScopes, type ApiKey } from './data';
16
+
17
+ export interface SettingsApiKeysProps extends SettingsFrameOptions {
18
+ keys: ApiKey[];
19
+ scopes: ApiKeyScope[];
20
+ expiries: ApiKeyExpiry[];
21
+ /** Creates a key and returns its secret (shown once). Defaults to a demo generator. */
22
+ onCreate: (input: CreateApiKeyInput) => string | Promise<string>;
23
+ /** Revokes a key after the typed confirmation. A rejection shows its message. */
24
+ onRevoke: (key: ApiKey) => void | Promise<void>;
25
+ }
26
+
27
+ const COLUMNS: DataTableColumn<ApiKey>[] = [
28
+ {
29
+ id: 'name',
30
+ header: 'Name',
31
+ accessor: (k) => k.name,
32
+ sortable: true,
33
+ cell: (k) => (
34
+ <div className="min-w-0">
35
+ <div className="truncate text-[13px] font-medium text-foreground">{k.name}</div>
36
+ <div className="font-mono text-[11.5px] text-muted-foreground">{k.prefix}…</div>
37
+ </div>
38
+ ),
39
+ },
40
+ {
41
+ id: 'scopes',
42
+ header: 'Scopes',
43
+ hideable: true,
44
+ cell: (k) => (
45
+ <div className="flex flex-wrap gap-1">
46
+ {k.scopes.map((s) => (
47
+ <Badge key={s} size="sm" className="font-mono">
48
+ {s}
49
+ </Badge>
50
+ ))}
51
+ </div>
52
+ ),
53
+ },
54
+ {
55
+ id: 'created',
56
+ header: 'Created',
57
+ accessor: (k) => k.createdAt,
58
+ sortable: true,
59
+ cell: (k) => <Timestamp value={k.createdAt} tooltip={false} />,
60
+ },
61
+ {
62
+ id: 'lastUsed',
63
+ header: 'Last used',
64
+ accessor: (k) => k.lastUsedAt ?? '',
65
+ sortable: true,
66
+ cell: (k) => (k.lastUsedAt ? <Timestamp value={k.lastUsedAt} tooltip={false} /> : 'Never'),
67
+ },
68
+ { id: 'createdBy', header: 'Created by', accessor: (k) => k.createdBy, defaultHidden: true },
69
+ ];
70
+
71
+ /** API keys settings: DataTable of keys, CreateApiKeyDialog (header action), revoke via ConfirmDestructive. */
72
+ export default function SettingsApiKeysPage({
73
+ keys = defaultKeys,
74
+ scopes = defaultScopes,
75
+ expiries = defaultExpiries,
76
+ onCreate = () => generateDemoSecret(),
77
+ onRevoke,
78
+ ...frame
79
+ }: Partial<SettingsApiKeysProps>) {
80
+ const [created, setCreated] = useState<ApiKey[]>([]);
81
+ const [revokedIds, setRevokedIds] = useState<string[]>([]);
82
+ const [createOpen, setCreateOpen] = useState(false);
83
+ const [revoking, setRevoking] = useState<ApiKey | null>(null);
84
+ const [confirmOpen, setConfirmOpen] = useState(false);
85
+ const rows = [...created, ...keys].filter((k) => !revokedIds.includes(k.id));
86
+
87
+ const create = async (input: CreateApiKeyInput) => {
88
+ const secret = await onCreate(input);
89
+ setCreated((list) => [
90
+ {
91
+ id: `new-${list.length}-${input.name}`,
92
+ name: input.name,
93
+ prefix: secret.slice(0, 12),
94
+ scopes: input.scopes,
95
+ createdAt: new Date().toISOString(),
96
+ lastUsedAt: null,
97
+ createdBy: 'You',
98
+ },
99
+ ...list,
100
+ ]);
101
+ return secret;
102
+ };
103
+
104
+ const revoke = async () => {
105
+ if (!revoking) return;
106
+ await onRevoke?.(revoking);
107
+ setRevokedIds((ids) => [...ids, revoking.id]);
108
+ };
109
+
110
+ return (
111
+ <SettingsFrame
112
+ page="api-keys"
113
+ width="wide"
114
+ description="Keys authenticate scripts and services against the API. Treat them like passwords."
115
+ actions={[{ label: 'Create API key', icon: Plus, variant: 'primary', onClick: () => setCreateOpen(true) }]}
116
+ {...frame}
117
+ >
118
+ <DataTable
119
+ rows={rows}
120
+ columns={COLUMNS}
121
+ caption="API keys"
122
+ showCaption
123
+ paginated={false}
124
+ getRowLabel={(k) => k.name}
125
+ rowActions={(k) => [
126
+ {
127
+ label: 'Revoke key',
128
+ icon: Trash2,
129
+ destructive: true,
130
+ onSelect: () => {
131
+ setRevoking(k);
132
+ setConfirmOpen(true);
133
+ },
134
+ },
135
+ ]}
136
+ emptyState={
137
+ <EmptyState
138
+ icon={KeyRound}
139
+ title="No API keys"
140
+ description="Create a key to call the API from scripts and services."
141
+ />
142
+ }
143
+ />
144
+ <CreateApiKeyDialog
145
+ trigger={null}
146
+ open={createOpen}
147
+ onOpenChange={setCreateOpen}
148
+ scopes={scopes}
149
+ expiries={expiries}
150
+ onCreate={create}
151
+ />
152
+ {revoking && (
153
+ <ConfirmDestructive
154
+ key={revoking.id}
155
+ triggerLabel={null}
156
+ open={confirmOpen}
157
+ onOpenChange={setConfirmOpen}
158
+ resourceName={revoking.name}
159
+ resourceType="API key"
160
+ title="Revoke API key?"
161
+ confirmLabel="Revoke key"
162
+ consequences={[
163
+ 'Requests signed with this key start failing immediately',
164
+ 'Scripts and services using it must get a new key',
165
+ ]}
166
+ onConfirm={revoke}
167
+ />
168
+ )}
169
+ </SettingsFrame>
170
+ );
171
+ }
@@ -0,0 +1,58 @@
1
+ import { apiKeyExpiries, apiKeyScopes, type ApiKeyExpiry, type ApiKeyScope } from '@gntik-ai/blocks';
2
+
3
+ const daysAgo = (d: number) => new Date(Date.now() - d * 86_400_000).toISOString();
4
+
5
+ export interface ApiKey {
6
+ id: string;
7
+ name: string;
8
+ /** Visible start of the secret, e.g. "sk_live_3f9a". */
9
+ prefix: string;
10
+ scopes: string[];
11
+ /** ISO date. */
12
+ createdAt: string;
13
+ /** ISO date; null when never used. */
14
+ lastUsedAt: string | null;
15
+ createdBy: string;
16
+ }
17
+
18
+ export const scopes: ApiKeyScope[] = apiKeyScopes;
19
+ export const expiries: ApiKeyExpiry[] = apiKeyExpiries;
20
+
21
+ export const apiKeys: ApiKey[] = [
22
+ {
23
+ id: 'k1',
24
+ name: 'ci-deploy',
25
+ prefix: 'sk_live_3f9a',
26
+ scopes: ['projects:read', 'projects:write'],
27
+ createdAt: daysAgo(120),
28
+ lastUsedAt: daysAgo(0.02),
29
+ createdBy: 'Avery Collins',
30
+ },
31
+ {
32
+ id: 'k2',
33
+ name: 'metrics-exporter',
34
+ prefix: 'sk_live_81c0',
35
+ scopes: ['projects:read', 'billing:read'],
36
+ createdAt: daysAgo(64),
37
+ lastUsedAt: daysAgo(1),
38
+ createdBy: 'Sam Rivera',
39
+ },
40
+ {
41
+ id: 'k3',
42
+ name: 'directory-sync',
43
+ prefix: 'sk_live_c27e',
44
+ scopes: ['members:read'],
45
+ createdAt: daysAgo(30),
46
+ lastUsedAt: daysAgo(9),
47
+ createdBy: 'Avery Collins',
48
+ },
49
+ {
50
+ id: 'k4',
51
+ name: 'local-testing',
52
+ prefix: 'sk_live_09bd',
53
+ scopes: ['projects:read'],
54
+ createdAt: daysAgo(4),
55
+ lastUsedAt: null,
56
+ createdBy: 'Jordan Lee',
57
+ },
58
+ ];
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'API keys settings',
5
+ family: 'Settings',
6
+ priority: 'P2',
7
+ status: 'beta',
8
+ description:
9
+ 'API key list with scopes and last use, a create-key dialog that shows the secret once, and typed revoke confirmation.',
10
+ layout: 'SettingsLayout',
11
+ blocks: ['app-sidebar', 'app-topbar', 'page-header', 'data-table', 'create-api-key-dialog', 'confirm-destructive'],
12
+ };