# gntik-ui
> The product-agnostic design system of gntik-ai: React 19 + Tailwind CSS 4 components on Base UI, layouts, page blocks and page templates over one frozen token layer (green hue 145, Geist + Geist Mono, dark default + light + high-contrast themes). Products differ only by a brand preset (name + logo). 118 components, 12 layouts, 77 blocks, 61 templates.
Packages are published as `@gntik-ai/*` on npmjs.org: `@gntik-ai/tokens` (the brand), `@gntik-ai/ui` (components, layouts, ThemeProvider, presets), `@gntik-ai/blocks`, `@gntik-ai/templates` (pages, `ConsoleShell`), `@gntik-ai/chat`, `@gntik-ai/icons`, `@gntik-ai/charts`, `@gntik-ai/flow`, `@gntik-ai/editor`, and the `gntik-ui` CLI. Build pages from a template first, then layouts + blocks, then components. Styling uses token classes only: no hex or Tailwind palette colours, no gradients or glow, no `dark:` variants.
## Docs
- [AGENTS.md](https://github.com/gntik-ai/gntik-ui/blob/main/AGENTS.md): how to find, compose and add kit items, the brand rules and the gate commands
- [kit-registry.json](https://github.com/gntik-ai/gntik-ui/blob/main/kit-registry.json): machine-readable list of every installable item with files and dependencies
- [Release manifest](https://github.com/gntik-ai/gntik-ui/blob/main/packages/mcp/registry-manifest.json): shared package/registry item inventory, pinned versions and release baseline; the live registry follows main
- [INVENTORY.md](https://github.com/gntik-ai/gntik-ui/blob/main/INVENTORY.md): inventory of the catalog and the packages
- [Component contract](https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/CONTRIBUTING.md): folder layout, docs, tests and style rules for a component
- [Brand tokens](https://github.com/gntik-ai/gntik-ui/blob/main/packages/tokens/src/brand.css): colour, typography, radii and shadows for the three themes
- [MCP server](https://github.com/gntik-ai/gntik-ui/blob/main/packages/mcp/README.md): list_kit, get_component, get_block, get_template, scaffold_page, scaffold_preset and validate_page
## Components
### Button (`button`)
Triggers an action. Five variants (primary, secondary, soft, ghost, destructive), three sizes, icons and a loading state. IconButton is the square, icon-only form.
- Package: `@gntik-ai/ui` · export `Button` · group Actions · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Button/Button.doc.ts
- Primitive: `@base-ui/react/button`
- ARIA pattern: button
- Registry dependencies: spinner, ui-utils
- Install: `gntik-ui add button`; own the source with `gntik-ui eject button`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter | Activates the button |
| Space | Activates the button |
| Tab | Moves focus; a loading button stays focusable |
Tokens: primary, primary-foreground, card, border, secondary, destructive, primary-text, focus-ring
Usage:
```tsx
import { MoreHorizontal } from '@gntik-ai/icons';
import { Button, IconButton } from '@gntik-ai/ui';
export default function ButtonStates() {
return (
Small
Large
Saving
Disabled
);
}
```
### CopyButton (`copy-button`)
Copies a string, or the result of a (possibly async) getter, to the clipboard. Icon form (with tooltip) or labelled form, a "Copied" success state that resets after 2 s, a polite announcement (through LiveAnnouncer when present) and a fallback to execCommand when the Clipboard API is missing. Optional failureLabel and failureAnnouncement override failure feedback; failurePoliteness changes the LiveAnnouncer failure priority (assertive by default). `copyToClipboard` is exported too.
- Package: `@gntik-ai/ui` · export `CopyButton` · group Actions · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/CopyButton/CopyButton.doc.ts
- Primitive: `@base-ui/react/button`
- ARIA pattern: button + status
- Registry dependencies: button, live-announcer, tooltip, ui-utils
- Install: `gntik-ui add copy-button`; own the source with `gntik-ui eject copy-button`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter | Copies the value and announces "Copied to clipboard" |
| Space | Copies the value and announces "Copied to clipboard" |
| Tab | Moves focus to and from the button |
Tokens: muted-foreground, secondary, primary-text, destructive-text, focus-ring
Usage:
```tsx
import { CopyButton } from '@gntik-ai/ui';
export default function CopyButtonBasic() {
const command = 'pnpm add @gntik-ai/ui';
return (
);
}
```
### DownloadButton (`download-button`)
Saves required data (a string, Blob, or sync/async getter) as fileName. mimeType defaults to the Blob type, otherwise text/plain;charset=utf-8; extensions are not inferred. Inherits Button props, with a Download icon and translated default label. Async getters use Button loading, stay focusable and ignore repeat activation. onDownloaded fires after saving; onDownloadError receives failures, announced politely through LiveAnnouncer or a local status region. The static CSV, async success/failure and one-time JSON examples demonstrate these states and callbacks. downloadFile(data, fileName, mimeType?) is also exported; it removes the temporary anchor, revokes its URL on the next task, and does nothing without object URL support. Browsers differ: iOS Safari may open a tab. Exports are held in memory; use a server export for very large row sets. toCsv(rows, columns, options?) accepts columns with header and value (a row key or getter). Defaults: comma delimiter, header true, BOM false, CRLF, escapeFormulas true. Options also accept semicolon or tab delimiters, header false, BOM true, LF and escapeFormulas false. Null/undefined become empty cells, Dates use ISO, numbers/booleans retain their values and objects use JSON. Text starting with =, +, -, @, TAB or CR gets an apostrophe before CSV quoting; legitimate + or - text also changes. Disabling escapeFormulas is unsafe for spreadsheets. escapeCsvCell(text, delimiter?) always protects formulas and quotes delimiters, double quotes, CR and LF. It shares a name with the templates package helper: import explicitly from @gntik-ai/ui. Its CR-led cells are deliberately quoted, unlike the legacy audit-log helper.
- Package: `@gntik-ai/ui` · export `DownloadButton` · group Actions · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/DownloadButton/DownloadButton.doc.ts
- Primitive: `@base-ui/react/button`
- ARIA pattern: button + status
- Registry dependencies: button, live-announcer, ui-utils
- Install: `gntik-ui add download-button`; own the source with `gntik-ui eject download-button`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter | Downloads the file |
| Space | Downloads the file |
| Tab | Moves focus to and from the button, including while pending |
Tokens: primary, primary-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { DownloadButton, toCsv } from '@gntik-ai/ui';
/** The secondary button fails once; activating it again retries successfully. */
export default function DownloadButtonAsync() {
const [failed, setFailed] = useState(false);
const getCsv = () =>
new Promise((resolve) => {
setTimeout(() => resolve(toCsv([{ count: 42 }], [{ header: 'Count', value: 'count' }])), 500);
});
return (
{
if (!failed) throw new Error('Example export unavailable');
return getCsv();
}}
fileName="retry.csv"
mimeType="text/csv"
onDownloadError={() => setFailed(true)}
/>
);
}
```
### MessageFeedback (`message-feedback`)
Actions for an assistant reply: copy, regenerate and thumbs up / down toggles (aria-pressed). Thumbs down opens a popover with a reason picker and an optional comment. Controlled with `value` + `onFeedback`; pressing the active thumb clears it. A polite status confirms the feedback. Copy actions show common.copyFailed when clipboard access is denied or unavailable, with no success icon; successful copies keep their labels and reset after 1600ms. See the bilingual Result feedback live preview.
- Package: `@gntik-ai/chat` · export `MessageFeedback` · group Actions · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/MessageFeedback/MessageFeedback.doc.ts
- Primitive: `Popover`
- ARIA pattern: toggle button + dialog
- Install: `gntik-ui add message-feedback`; own the source with `gntik-ui eject message-feedback`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves through copy, regenerate, good and bad |
| Enter / Space | Toggles the focused thumb (aria-pressed); a second press clears it |
| Enter / Space (on bad) | Also opens the details popover; focus moves into it |
| Arrow keys | Inside the popover: move between reasons |
| Escape | Closes the popover (the rating stays) and returns focus to the thumb |
Tokens: muted-foreground, primary-text, destructive-text, popover, border, focus-ring
Usage:
```tsx
import { TooltipProvider } from '@gntik-ai/ui';
import { useState } from 'react';
import { MessageFeedback, type FeedbackValue } from '@gntik-ai/chat';
export default function MessageFeedbackReply() {
const [feedback, setFeedback] = useState({ rating: null });
const [regenerated, setRegenerated] = useState(0);
return (
Eleven of the twelve deployments succeeded this week; one was rolled back after a failed health check.
setRegenerated((n) => n + 1)}
/>
rating: {feedback.rating ?? 'none'}
{feedback.reason ? ` · reason: ${feedback.reason}` : ''}
{feedback.comment ? ` · comment: ${feedback.comment}` : ''}
{regenerated ? ` · regenerated ${regenerated}×` : ''}
);
}
```
### SplitButton (`split-button`)
A primary action joined to a menu of secondary actions. Two real buttons in a role="group": the action, and a chevron menu button (aria-haspopup="menu", aria-expanded) that opens the kit Menu. Takes Button's variants and sizes (`auto` follows the density), `actions` for a simple list or `menu` for custom MenuItems.
- Package: `@gntik-ai/ui` · export `SplitButton` · group Actions · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/SplitButton/SplitButton.doc.ts
- Primitive: `@base-ui/react/menu`
- ARIA pattern: button + menu button
- Registry dependencies: button, menu, ui-utils
- Install: `gntik-ui add split-button`; own the source with `gntik-ui eject split-button`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves from the primary action to the menu button |
| Enter / Space | On the action: runs it. On the menu button: opens the menu with the first item highlighted |
| ArrowDown | On the menu button: opens the menu |
| ArrowDown / ArrowUp | Moves between menu items (wraps) |
| Enter | In the menu: runs the highlighted action and closes the menu |
| Escape | Closes the menu and returns focus to the menu button |
Tokens: primary, primary-foreground, card, border, secondary, destructive, destructive-foreground, popover, focus-ring
Usage:
```tsx
import { CalendarClock, FileDown, Rocket, Trash2 } from '@gntik-ai/icons';
import { useState } from 'react';
import { SplitButton } from '@gntik-ai/ui';
export default function SplitButtonBasic() {
const [last, setLast] = useState('Nothing yet');
return (
setLast('Deployed now')}
menuLabel="More deploy options"
actions={[
{ label: 'Schedule deploy', icon: CalendarClock, onSelect: () => setLast('Deploy scheduled') },
{ label: 'Export manifest', icon: FileDown, onSelect: () => setLast('Manifest exported') },
{
label: 'Discard draft',
icon: Trash2,
destructive: true,
separator: true,
onSelect: () => setLast('Draft discarded'),
},
]}
>
Deploy
Last action: {last}
);
}
```
### SuggestionChips (`suggestion-chips`)
Prompt suggestions: a two-column grid of cards with descriptions for an empty conversation, or a row of pills for follow-ups after a reply. Choosing one calls onSelect with its prompt.
- Package: `@gntik-ai/chat` · export `SuggestionChips` · group Actions · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/SuggestionChips/SuggestionChips.doc.ts
- ARIA pattern: button group
- Install: `gntik-ui add suggestion-chips`; own the source with `gntik-ui eject suggestion-chips`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves between suggestions |
| Enter | Sends the focused suggestion |
| Space | Sends the focused suggestion |
Tokens: card, border, primary, primary-text, secondary, muted-foreground, focus-ring
Usage:
```tsx
import { CreditCard, Rocket, Users, FileSearch } from '@gntik-ai/icons';
import { useState } from 'react';
import { SuggestionChips } from '@gntik-ai/chat';
export default function SuggestionChipsZeroState() {
const [picked, setPicked] = useState(null);
return (
{picked ? `Selected: ${picked}` : 'Pick a suggestion.'}
);
}
```
### ToggleGroup (`toggle-group`)
Toggle is a two-state (pressed) button; ToggleGroup sets Toggles side by side with shared state: a segmented control (single) or a toolbar of independent options (multiple). Segmented and joined looks, two sizes, icon-only items.
- Package: `@gntik-ai/ui` · export `ToggleGroup` · group Actions · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/ToggleGroup/ToggleGroup.doc.ts
- Primitive: `@base-ui/react/toggle-group`
- ARIA pattern: toolbar-style group of toggle buttons (aria-pressed)
- Registry dependencies: ui-utils
- Install: `gntik-ui add toggle-group`; own the source with `gntik-ui eject toggle-group`
Keyboard:
| Key | Behaviour |
| --- | --- |
| ArrowRight / ArrowLeft | Moves focus to the next / previous item (wraps) |
| Home / End | Moves focus to the first / last item |
| Space / Enter | Toggles the focused item |
| Tab | Moves focus into and out of the group (one tab stop) |
Tokens: card, border, secondary, foreground, muted-foreground, primary, focus-ring
Usage:
```tsx
import { LayoutGrid, List, SquareKanban } from '@gntik-ai/icons';
import { useState } from 'react';
import { Toggle, ToggleGroup } from '@gntik-ai/ui';
export default function ToggleGroupJoined() {
const [view, setView] = useState(['week']);
return (
v.length && setView(v)}
>
Day
Week
Month
);
}
```
### Toolbar (`toolbar`)
Action bar above tables and lists: search, filters and actions with a single Tab stop and arrow-key navigation. Start / center / end lanes, groups, separators, links and inputs; buttons use the kit Button styles. Variants: bar, floating, plain.
- Package: `@gntik-ai/ui` · export `Toolbar` · group Actions · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Toolbar/Toolbar.doc.ts
- Primitive: `@base-ui/react/toolbar`
- ARIA pattern: toolbar
- Registry dependencies: button, ui-utils
- Install: `gntik-ui add toolbar`; own the source with `gntik-ui eject toolbar`
Keyboard:
| Key | Behaviour |
| --- | --- |
| ArrowRight / ArrowLeft | Moves focus to the next / previous item (roving tabindex, wraps around) |
| Tab / Shift+Tab | Moves focus out of the toolbar (one Tab stop) |
| Enter / Space | Activates the focused button |
Tokens: border, secondary, card, background, primary, primary-text, muted-foreground, foreground, focus-ring
Usage:
```tsx
import { Pause, RefreshCw, Trash2 } from '@gntik-ai/icons';
import {
Toolbar,
ToolbarButton,
ToolbarCenter,
ToolbarEnd,
ToolbarGroup,
ToolbarLink,
ToolbarSeparator,
ToolbarStart,
} from '@gntik-ai/ui';
export default function ToolbarBulkActions() {
return (
3 selected
Pause
Reassign
Delete
View members
);
}
```
### Accordion (`accordion`)
A stack of collapsible sections separated by dividers, flush or inside a card. Single (default) or multiple open items; the chevron rotates and the height animates. Compound parts: Accordion, AccordionItem, AccordionTrigger, AccordionPanel.
- Package: `@gntik-ai/ui` · export `Accordion` · group Display · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Accordion/Accordion.doc.ts
- Primitive: `@base-ui/react/accordion`
- ARIA pattern: accordion
- Registry dependencies: ui-utils
- Install: `gntik-ui add accordion`; own the source with `gntik-ui eject accordion`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space | Expands or collapses the focused section |
| ArrowDown / ArrowUp | Moves focus to the next / previous trigger (wraps around) |
| Home / End | Moves focus to the first / last trigger |
| Tab | Moves focus through the triggers and any focusable content in open panels |
Tokens: border, card, secondary, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from '@gntik-ai/ui';
export default function AccordionMultiple() {
const [open, setOpen] = useState(['general']);
return (
General
Project name, description and default region.
Notifications
Choose who is notified when a deployment fails.
Danger zone (owners only)
Transfer or delete the project.
);
}
```
### AgentSurface (`agent-surface`)
Opt-in, placement-neutral renderer for allow-listed agent content. Accepts only A2UI v0.9 command envelopes (A2UI_VERSION). Pass an immutable messages log and a trusted catalog; append createSurface, updateComponents, updateDataModel and deleteSurface messages. Component records have id, component, optional children (child ids), and flat catalog properties; the root id is "root". Text/Heading/Badge use text; Text optionally accepts href; Heading accepts level 1–6. Card contains children; Stack accepts direction (row/column) and gap (0–12). Button uses label, optional disabled and action { name, context }; Input uses label, optional text value/disabled and only type text. Select uses label, options [{ value, label }], optional value/disabled; Checkbox uses label, optional checked/disabled. Alert uses title and optional text. { path: "/user/name" } reads a JSON Pointer; Input/Select value and Checkbox checked write back to that path. updateDataModel { surfaceId, path?, value } replaces the value at path (default: model root). Every default Button is a submit control: Enter in an Input can activate the first Button, so keep a safe primary action first and avoid destructive actions in that position. Buttons, keyboard activation and implicit form submission emit onAction { name, surfaceId, context } with the current bound values. Replacing the log replays it; keep existing message objects stable when appending. Runtime validation rejects unsupported versions, malformed JSON, prototype keys, cycles, shared children and excessive trees (256 nodes, depth 32, 32 surfaces). Missing child definitions may arrive later. Unknown components and invalid properties show a localized placeholder and report a deduplicated onError diagnostic. The visible, accessible agent-content frame cannot be relabelled by the agent. Deletion or version suppression of rendered content restores focus to the next surviving sibling control, the first focusable textarea in the document, or the labelled container. Place the chat composer as that first textarea or provide a focusable sibling after the renderer; another textarea can otherwise receive focus. defineAgentCatalog accepts trusted host wrappers { component, validate }; validate must return a fresh allow-list of known properties or null. Wrappers receive { props, children, onValueChange, onAction } and must assign explicit kit props. Never spread agent data into elements. Custom URL properties must have names ending in href, src or url (case-insensitive) to receive the Markdown safeUrl policy; text renders as React text nodes. Custom catalogs can reintroduce phishing and executable content: audit their wrappers and validators, never allow passwords, payments, file uploads, arbitrary markup or host-chrome styling. Transport, side-panel placement, v0.8 adapters and Markdown integration belong to consumers or future changes. The live example demonstrates every default entry, model patches, actions, rejection, deletion and reset.
- Package: `@gntik-ai/chat` · export `AgentSurface` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/AgentSurface/AgentSurface.doc.ts
- Primitive: `Kit components (Base UI)`
- ARIA pattern: labelled group containing a form
- Registry dependencies: markdown
- Install: `gntik-ui add agent-surface`; own the source with `gntik-ui eject agent-surface`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Moves between enabled form controls and links |
| Enter / Space | Activates a Button exactly once; Enter in an Input submits with the first Button |
| Space | Toggles a Checkbox and updates its bound model path |
| Enter / ArrowDown | Opens a Select; ArrowUp/ArrowDown navigates options |
| Enter / Escape | Selects an option / closes a Select and returns focus |
| Surface deletion | Moves focus to a surviving sibling control, the composer, or the labelled container |
Tokens: background, card, border, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { Button, Stack, Text } from '@gntik-ai/ui';
import { useState } from 'react';
import { useChatI18n } from '@gntik-ai/chat';
import { AgentSurface } from '@gntik-ai/chat';
import { defaultAgentCatalog } from '@gntik-ai/chat';
import { A2UI_VERSION, type A2uiMessage } from '@gntik-ai/chat';
/** Transport belongs to the consumer. This example appends commands locally. */
export default function AgentSurfaceForm() {
const { tc } = useChatI18n();
function initial(): A2uiMessage[] {
return [
{ version: A2UI_VERSION, createSurface: { surfaceId: 's1' } },
{
version: A2UI_VERSION,
updateDataModel: {
surfaceId: 's1',
value: { user: { name: 'Ana' }, language: 'en', notify: false, submitted: false },
},
},
{
version: A2UI_VERSION,
updateComponents: {
surfaceId: 's1',
components: [
{ id: 'root', component: 'Card', children: ['stack'] },
{
id: 'stack',
component: 'Stack',
direction: 'column',
gap: 3,
children: ['heading', 'badge', 'notice', 'name', 'echo', 'language', 'notify', 'link', 'submit'],
},
{ id: 'heading', component: 'Heading', text: tc('chat.surfaceExampleTitle'), level: 2 },
{ id: 'badge', component: 'Badge', text: tc('chat.surfaceExampleBadge') },
{
id: 'notice',
component: 'Alert',
title: tc('chat.surfaceExampleTitle'),
text: tc('chat.surfaceExampleNotice'),
},
{
id: 'name',
component: 'Input',
disabled: { path: '/submitted' },
label: tc('chat.surfaceExampleName'),
value: { path: '/user/name' },
},
{ id: 'echo', component: 'Text', text: { path: '/user/name' } },
{
id: 'language',
component: 'Select',
disabled: { path: '/submitted' },
label: tc('chat.surfaceExampleChoice'),
value: { path: '/language' },
options: [
{ value: 'en', label: tc('chat.surfaceExampleEnglish') },
{ value: 'es', label: tc('chat.surfaceExampleSpanish') },
],
},
{
id: 'notify',
component: 'Checkbox',
disabled: { path: '/submitted' },
label: tc('chat.surfaceExampleNotify'),
checked: { path: '/notify' },
},
{ id: 'link', component: 'Text', text: tc('chat.surfaceExampleLink'), href: 'https://ui.gntik.ai' },
{
id: 'submit',
component: 'Button',
disabled: { path: '/submitted' },
label: tc('chat.surfaceExampleSubmit'),
action: {
name: 'submit',
context: {
name: { path: '/user/name' },
language: { path: '/language' },
notify: { path: '/notify' },
},
},
},
],
},
},
];
}
const [messages, setMessages] = useState(initial);
const [removed, setRemoved] = useState(false);
const [status, setStatus] = useState('');
const append = (message: A2uiMessage) => setMessages((current) => [...current, message]);
return (
{
setStatus(tc('chat.surfaceExampleStatus', { name: String(context.name ?? '') }));
append({ version: A2UI_VERSION, updateDataModel: { surfaceId: 's1', path: '/submitted', value: true } });
}}
onError={() => setStatus(tc('chat.surfaceExampleRejected'))}
/>
append({ version: A2UI_VERSION, updateDataModel: { surfaceId: 's1', path: '/user/name', value: 'Bea' } })
}
>
{tc('chat.surfaceExamplePatch')}
append({
version: A2UI_VERSION,
updateComponents: { surfaceId: 's1', components: [{ id: 'root', component: 'Iframe' }] },
})
}
>
{tc('chat.surfaceExampleUnsupported')}
{
append({ version: A2UI_VERSION, deleteSurface: { surfaceId: 's1' } });
setRemoved(true);
}}
>
{tc('chat.surfaceExampleRemove')}
{
setMessages(initial());
setRemoved(false);
setStatus('');
}}
>
{tc('chat.surfaceExampleReset')}
{status}
);
}
```
### AttachmentList (`attachment`)
Files attached to a message or waiting in the composer. Each AttachmentChip shows an image thumbnail or a file-type icon, the name, the size, upload progress (a labelled progressbar; indeterminate holds still under reduced motion), an error with Retry, and a Remove button. `variant="tile"` gives larger image previews inside a message. `validateFiles` / `matchesAccept` back the composer’s accept and max-size checks.
- Package: `@gntik-ai/chat` · export `AttachmentList` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/Attachment/Attachment.doc.ts
- ARIA pattern: list
- Install: `gntik-ui add attachment`; own the source with `gntik-ui eject attachment`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves through each chip’s Retry (on error) and Remove buttons |
| Enter / Space | Activates the focused Retry or Remove button |
Tokens: secondary, card, border, muted-foreground, primary, destructive, destructive-text, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { AttachmentList } from '@gntik-ai/chat';
import type { ChatAttachment } from '@gntik-ai/chat';
import { PREVIEW_IMAGE as PREVIEW } from '@gntik-ai/chat';
const INITIAL: ChatAttachment[] = [
{ id: 'a', name: 'architecture-diagram.png', size: 284_311, type: 'image/png', previewUrl: PREVIEW },
{ id: 'b', name: 'invoices-september.csv', size: 48_213, type: 'text/csv', progress: 62 },
{ id: 'c', name: 'contract-draft.pdf', size: 1_904_118, type: 'application/pdf', progress: null },
{ id: 'd', name: 'release-notes.md', size: 9_042, error: 'Network error' },
];
export default function AttachmentStates() {
const [files, setFiles] = useState(INITIAL);
return (
setFiles((f) => f.filter((x) => x.id !== id))}
onRetry={(id) => setFiles((f) => f.map((x) => (x.id === id ? { ...x, error: undefined, progress: 100 } : x)))}
/>
);
}
```
### Avatar (`avatar`)
Person or entity avatar: image with an initials (or icon) fallback, five sizes (xs–xl), round or rounded, tinted tones and an optional presence dot. AvatarGroup stacks avatars with a "+N more" overflow.
- Package: `@gntik-ai/ui` · export `Avatar` · group Display · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Avatar/Avatar.doc.ts
- Primitive: `@base-ui/react/avatar`
- ARIA pattern: img
- Registry dependencies: ui-utils
- Install: `gntik-ui add avatar`; own the source with `gntik-ui eject avatar`
Tokens: primary, primary-foreground, accent, secondary, category-violet, category-cyan, category-amber, category-rose, success, warning, destructive, card
Usage:
```tsx
import { Avatar, AvatarGroup } from '@gntik-ai/ui';
const MEMBERS = [
{ name: 'Maria Ruiz', tone: 'primary' },
{ name: 'Jordan Lee', tone: 'violet' },
{ name: 'Sam King', tone: 'cyan' },
{ name: 'Ana Torres', tone: 'rose' },
{ name: 'Val Gomez', tone: 'amber' },
{ name: 'Dev Nair', tone: 'accent' },
{ name: 'Priya Shah', tone: 'secondary' },
] as const;
export default function AvatarGroupMembers() {
return (
{MEMBERS.map((m) => (
))}
{MEMBERS.map((m) => (
))}
);
}
```
### Badge (`badge`)
Compact label for status, counts and categories. Semantic tones (neutral, primary, success, warning, destructive, info) plus four category tones, in soft, outline or solid; optional dot and remove button for tags.
- Package: `@gntik-ai/ui` · export `Badge` · group Display · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Badge/Badge.doc.ts
- ARIA pattern: button (remove)
- Registry dependencies: ui-utils
- Install: `gntik-ui add badge`; own the source with `gntik-ui eject badge`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves focus to the remove button, when present |
| Enter | Activates the remove button |
| Space | Activates the remove button |
Tokens: primary, primary-text, success, success-text, warning, warning-text, destructive, destructive-text, info, category-violet, category-cyan, category-amber, category-rose, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Badge } from '@gntik-ai/ui';
import type { BadgeTone } from '@gntik-ai/ui';
const INITIAL: Array<{ label: string; tone: BadgeTone }> = [
{ label: 'production', tone: 'rose' },
{ label: 'eu-west-1', tone: 'cyan' },
{ label: 'billing', tone: 'violet' },
{ label: 'beta', tone: 'amber' },
];
export default function BadgeRemovable() {
const [tags, setTags] = useState(INITIAL);
return (
{tags.map((t) => (
setTags((all) => all.filter((x) => x.label !== t.label))}
>
{t.label}
))}
{tags.length === 0 && (
setTags(INITIAL)}
className="font-mono text-[11px] text-primary-text hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring"
>
Restore tags
)}
);
}
```
### Blockquote (`blockquote`)
Quoted content with its citation: a figure with the blockquote and a figcaption whose cite names the source (linked when `cite` is a URL). Variants: an inline-start rule for prose, a card for testimonials, a centred pull quote.
- Package: `@gntik-ai/ui` · export `Blockquote` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Blockquote/Blockquote.doc.ts
- ARIA pattern: figure + blockquote + figcaption/cite
- Registry dependencies: ui-utils
- Install: `gntik-ui add blockquote`; own the source with `gntik-ui eject blockquote`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves to the source link when `cite` is set |
Tokens: primary, primary-text, card, border, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { Blockquote } from '@gntik-ai/ui';
export default function BlockquoteProse() {
return (
The postmortem closed with one recommendation for every team that runs scheduled deployments:
Treat the rollback path as a feature. If it has never run in production, it does not exist.
);
}
```
### Carousel (`carousel`)
Scroll-snap media strip: one, two, three or auto-width slides per view, previous/next buttons, optional picker dots, arrow keys on the focused strip. No autoplay by default; `autoplay` adds a pause control, pauses on hover and focus, and starts paused under reduced motion. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `Carousel` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Carousel/Carousel.doc.ts
- ARIA pattern: APG carousel (region with aria-roledescription="carousel", slides as labelled groups)
- Registry dependencies: ui-utils
- Install: `gntik-ui add carousel`; own the source with `gntik-ui eject carousel`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves through the controls, then the strip, then the slide content |
| ArrowRight / ArrowLeft | On the focused strip: next / previous slide (mirrored in RTL) |
| Home / End | On the focused strip: first / last slide |
| Enter / Space | Activates previous, next, a picker dot or the pause control |
Tokens: card, border, foreground, muted-foreground, primary, secondary, focus-ring
Usage:
```tsx
import { Carousel } from '@gntik-ai/ui';
const tips = [
'Pin the dashboards you open every morning to the sidebar.',
'Press ⌘K anywhere to jump to a project, member or invoice.',
'Deployments can be rolled back from their detail page in one click.',
];
export default function CarouselAutoplay() {
return (
{tips.map((tip) => (
{tip}
))}
);
}
```
### ChatMessage (`chat-message`)
One conversation turn for the roles user (end-aligned bubble), participant (another person, e.g. a teammate: start-aligned outlined bubble with their name and avatar via `author` / `avatarSrc`), assistant, system and tool: avatar, author, timestamp, body and hover/focus actions (copy, retry, thumbs feedback). While streaming it is aria-busy, hides its actions and shows a TypingIndicator ("Assistant is typing") until text arrives and a caret after streamed plain text; ChatAnnouncer announces start and end once instead of every token. Copy actions show common.copyFailed when clipboard access is denied or unavailable, with no success icon; successful copies keep their labels and reset after 1600ms. See the bilingual Result feedback live preview.
- Package: `@gntik-ai/chat` · export `ChatMessage` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/ChatMessage/ChatMessage.doc.ts
- ARIA pattern: article
- Registry dependencies: typing-indicator
- Install: `gntik-ui add chat-message`; own the source with `gntik-ui eject chat-message`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Reaches each action in order (copy, retry, good, bad); focusing one reveals the action group |
| Enter | Activates the focused action |
| Space | Activates the focused action; thumbs toggle (aria-pressed) |
Tokens: foreground, muted-foreground, secondary, card, border, primary, primary-text, destructive-text, focus-ring
Usage:
```tsx
import { TooltipProvider } from '@gntik-ai/ui';
import { useState } from 'react';
import { Markdown } from '@gntik-ai/chat';
import { ChatMessage, type ChatFeedback } from '@gntik-ai/chat';
const REPLY = 'The **web-app** project has 3 deployments today. The latest one, `v2.14.0`, finished in 2 m 10 s.';
export default function ChatMessageRoles() {
const [feedback, setFeedback] = useState(null);
return (
Conversation started · project web-app
How many deployments did web-app have today?
{}}
feedback={feedback}
onFeedback={setFeedback}
>
{REPLY}
);
}
```
### Citation (`citation`)
Numbered source references for grounded answers: an inline [n] marker that previews the source (title, host, snippet, link) on hover, focus + Enter or click, and CitationList, the numbered list of sources under a reply. Only http(s) URLs become links.
- Package: `@gntik-ai/chat` · export `Citation` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/Citation/Citation.doc.ts
- Primitive: `@base-ui/react/popover`
- ARIA pattern: dialog (non-modal)
- Install: `gntik-ui add citation`; own the source with `gntik-ui eject citation`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter | On a marker: opens the source preview |
| Space | On a marker: opens the source preview |
| Escape | Closes the preview and returns focus to the marker |
| Tab | Moves to the "Open source" link inside an open preview |
Tokens: primary, primary-text, popover, secondary, card, border, muted-foreground, focus-ring
Usage:
```tsx
import { Citation, CitationList, type CitationSource } from '@gntik-ai/chat';
const SOURCES: CitationSource[] = [
{
title: 'Deployment limits and quotas',
url: 'https://docs.example.com/deployments/limits',
snippet: 'Each project can run up to 20 deployments a day. Limits reset at midnight UTC.',
},
{
title: 'Billing FAQ: seats and invoices',
url: 'https://help.example.com/billing/faq',
snippet: 'Invoices are issued on the first business day of the month and list every seat that was active.',
},
];
export default function CitationAnswer() {
return (
A project can run up to 20 deployments a day
, and invoices list every seat that was active during the month
.
);
}
```
### CodeBlock (`code-block`)
Read-only code panel with a filename / language header, a copy button ("Copied" feedback plus a polite live announcement), optional line numbers, highlighted lines, a soft-wrap toggle and a max height. Default colouring is a minimal tokenizer for ts/tsx/js/json/bash/css that only uses contrast-safe text tokens; pass `highlight` to plug in another highlighter. Code is the inline variant for running text.
- Package: `@gntik-ai/ui` · export `CodeBlock` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/CodeBlock/CodeBlock.doc.ts
- ARIA pattern: button · region
- Registry dependencies: ui-utils
- Install: `gntik-ui add code-block`; own the source with `gntik-ui eject code-block`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves through the wrap toggle, the copy button and the scrollable code region |
| Enter | Copies the code (on the copy button) or toggles wrapping (on the wrap toggle) |
| Space | Same as Enter |
| Arrow keys | Scroll the focused code region |
Tokens: card, background, border, secondary, foreground, muted-foreground, primary, primary-text, warning-text, destructive-text, focus-ring
Usage:
```tsx
import { CodeBlock } from '@gntik-ai/ui';
const SOURCE = `import { Button } from '@gntik-ai/ui';
// Ships the current project to production.
export function DeployButton({ project }: { project: string }) {
const label = \`Deploy \${project}\`;
return (
deploy(project, { retries: 3 })}>
{label}
);
}
`;
export default function CodeBlockFile() {
return (
);
}
```
### Collapsible (`collapsible`)
A single show/hide section: an inline toggle ("Advanced settings") or a full-width row in the Accordion family. The chevron rotates and the height animates. Compound parts: Collapsible, CollapsibleTrigger, CollapsiblePanel.
- Package: `@gntik-ai/ui` · export `Collapsible` · group Display · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Collapsible/Collapsible.doc.ts
- Primitive: `@base-ui/react/collapsible`
- ARIA pattern: disclosure
- Registry dependencies: ui-utils
- Install: `gntik-ui add collapsible`; own the source with `gntik-ui eject collapsible`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space | Expands or collapses the panel |
| Tab | Moves focus from the trigger into the open panel content |
Tokens: border, secondary, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@gntik-ai/ui';
export default function CollapsibleAdvanced() {
return (
Advanced settings
Keep build cache between deployments
);
}
```
### DiffViewer (`diff-viewer`)
Compares two texts, or two JSON values pretty-printed with sorted keys, line by line (Myers diff, no dependencies). Split or unified layout with a switch, line numbers, added/removed tints plus +/− markers and screen-reader text (never colour alone), word-level highlights inside paired lines, long unchanged runs collapsed behind "Expand N lines", +/− stats and optional file or version labels. The pure `diffLines`, `diffWords`, `diffJson` and `stableStringify` are exported.
- Package: `@gntik-ai/ui` · export `DiffViewer` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/DiffViewer/DiffViewer.doc.ts
- Primitive: `@base-ui/react/toggle-group`
- ARIA pattern: region + toggle group
- Registry dependencies: button, toggle-group, ui-utils
- Install: `gntik-ui add diff-viewer`; own the source with `gntik-ui eject diff-viewer`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves to the view switch, the scrollable diff region and each "Expand N lines" button |
| ArrowLeft / ArrowRight | In the view switch: moves between Unified and Split |
| Enter / Space | In the view switch: selects the layout |
| Enter / Space | On "Expand N lines": reveals the hidden lines and focuses the diff region |
| ArrowUp / ArrowDown | In the focused region: scrolls the diff |
Tokens: card, border, foreground, muted-foreground, secondary, success, success-text, destructive, destructive-text, focus-ring
Usage:
```tsx
import { DiffViewer } from '@gntik-ai/ui';
const previous = { name: 'ingest-worker', replicas: 2, env: { LOG_LEVEL: 'info', REGION: 'eu-west' }, ports: [8080] };
// Same keys in a different order: only real changes show up.
const next = {
ports: [8080, 9090],
env: { REGION: 'eu-west', LOG_LEVEL: 'debug' },
replicas: 3,
name: 'ingest-worker',
};
export default function DiffViewerJson() {
return (
);
}
```
### JsonViewer (`json-viewer`)
Collapsible JSON inspector built on TreeList: objects and arrays expand, values use contrast-safe type tokens, `search` highlights matches and expands their ancestors, `maxDepth` caps nesting, and a toolbar copies the selected node path or value. Optional `maxDisplayChars` (finite integer >= 1) measures UTF-16 units: strings as-is, other values as two-space JSON. Over-cap values become read-only text with an ellipsis (dropping a trailing high surrogate) and a warning Badge labelled by `labels.truncated`. Search has no effect in this fallback; Copy value copies the full measured text, as explained by `labels.copyFullHint`, and Copy path is disabled. `maxHeight` bounds the scrollable body. Only in capped mode, null, undefined and empty strings show `emptyContent` (default a muted dash). Leaving the cap unset preserves existing behavior. The cap limits rendering, not serialization cost. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `JsonViewer` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/JsonViewer/JsonViewer.doc.ts
- ARIA pattern: tree view
- Registry dependencies: badge, tree-list, ui-utils
- Install: `gntik-ui add json-viewer`; own the source with `gntik-ui eject json-viewer`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves between toolbar buttons and the tree or named capped-text scroll region |
| ArrowDown / ArrowUp | Next / previous visible key |
| ArrowRight / ArrowLeft | Expands / collapses an object or array (or moves to child / parent) |
| Home / End | First / last visible key |
| Enter / Space | Selects the key (its path shows in the toolbar) |
| Type a character | Moves to the next key starting with it |
| Arrow keys / PageDown / PageUp | Scrolls the focused capped-text region using native browser scrolling |
Tokens: card, border, secondary, foreground, muted-foreground, primary-text, warning-text, destructive-text, warning, warning-chip-text, focus-ring
Usage:
```tsx
import { JsonViewer } from '@gntik-ai/ui';
const PAYLOAD = { records: Array.from({ length: 80 }, (_, id) => ({ id, message: 'Completed processing' })) };
export default function JsonViewerCapped() {
return (
);
}
```
### KanbanBoard (`kanban-board`)
Columns of cards you reorder within a column and move across columns by pointer drag or keyboard. Controlled (`columns` + `onMove`, with `moveKanbanCard` to apply a move); every pick-up, move, drop and cancel is announced through a LiveAnnouncer. Built on the dependency-free `useSortable` hook (pointer + keyboard, press-and-hold on touch). Cards lift with a border and shadow only; nothing animates under reduced motion. Density-aware card padding (`size: "auto"`).
- Package: `@gntik-ai/ui` · export `KanbanBoard` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/KanbanBoard/KanbanBoard.doc.ts
- ARIA pattern: Sortable lists (cards as buttons with aria-roledescription, aria-pressed while moving, aria-describedby instructions) + aria-live announcements
- Registry dependencies: live-announcer, ui-utils
- Install: `gntik-ui add kanban-board`; own the source with `gntik-ui eject kanban-board`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves between cards (column by column) |
| Space | Picks up the focused card; on a picked-up card, drops it |
| ArrowUp / ArrowDown | While moving: one position up / down in the column |
| ArrowLeft / ArrowRight | While moving: to the previous / next column (mirrored in RTL) |
| Home / End | While moving: to the top / bottom of the column |
| Enter | While moving: drops the card; otherwise opens it (`onCardOpen`) |
| Escape | While moving: cancels and returns the card to where it started |
Tokens: secondary, card, border, primary, accent, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Badge } from '@gntik-ai/ui';
import { KanbanBoard, moveKanbanCard, type KanbanCard, type KanbanColumn } from '@gntik-ai/ui';
interface Ticket extends KanbanCard {
priority: 'High' | 'Normal';
owner: string;
}
const initial: KanbanColumn[] = [
{
id: 'triage',
title: 'Triage',
cards: [
{ id: 't1', title: 'Webhook retries stall', priority: 'High', owner: 'Ana' },
{ id: 't2', title: 'Typo on the billing page', priority: 'Normal', owner: 'Luis' },
],
},
{
id: 'fixing',
title: 'Fixing',
cards: [{ id: 't3', title: 'Slow search on large teams', priority: 'High', owner: 'Mei' }],
},
{ id: 'shipped', title: 'Shipped', cards: [] },
];
export default function KanbanBoardCustomCard() {
const [columns, setColumns] = useState(initial);
return (
label="Support tickets"
size="sm"
headingLevel={4}
emptyText="Drop a fixed ticket here"
columns={columns}
onMove={(move) => setColumns((cols) => moveKanbanCard(cols, move))}
renderCard={(ticket) => (
{ticket.title}
{ticket.priority}
{ticket.owner}
)}
/>
);
}
```
### Kbd (`kbd`)
Keyboard key chip () for shortcuts and hints, in three sizes. Use mod in Kbd or KbdCombo for ⌘ on macOS/iOS and Ctrl elsewhere; explicit keys stay unchanged. Symbol keys and Ctrl are announced by name, unless label overrides the spoken name. useOs() reports the detected OS (undetermined during SSR); getOs() detects it outside React. Both and the Os type are exported from @gntik-ai/ui.
- Package: `@gntik-ai/ui` · export `Kbd` · group Display · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Kbd/Kbd.doc.ts
- Registry dependencies: ui-utils
- Install: `gntik-ui add kbd`; own the source with `gntik-ui eject kbd`
Tokens: border, card, muted-foreground
Usage:
```tsx
import { Kbd, KbdCombo } from '@gntik-ai/ui';
import { useOs } from '@gntik-ai/ui';
export default function KbdShortcuts() {
const os = useOs();
return (
Search
New project
Close esc
useOs(): {os}
↑↓ navigate
↵ select
Tab next field
);
}
```
### Markdown (`markdown`)
Safe, token-styled Markdown with GFM, code copying and streaming fence repair. Raw HTML stays skipped by default and URL attributes always pass safeUrl, including plugin-created URLs and component overrides. components merges over kit renderers; codeRenderers matches languages case-insensitively and receives code, language, streaming and complete (false only for the repaired fence). customTags allowlists standalone block tags: children use the same Markdown pipeline, attributes exclude on* and style, and unclosed tags render only while streaming. Unknown or malformed tags stay in the normal skipped-HTML pipeline without consuming later prose. useMarkdownContext exposes streaming to every renderer. Keep component maps and plugin arrays stable to preserve memoisation. User remarkPlugins follow GFM; user rehypePlugins follow kit HTML processing. Overrides and plugins are trusted executable code receiving untrusted content: validate data, render React text/children, and never use dangerouslySetInnerHTML. allowHtml lazily imports rehype-raw and rehype-sanitize; until loaded, HTML stays skipped. Its fixed subset is b, strong, i, em, u, s, del, sub, sup, kbd, mark, abbr[title], br, details[open] and summary. Other raw elements unwrap to text; scripts, styles, frames, embeds, forms, media, SVG, MathML, template and textarea drop their contents. Event handlers and style attributes are removed. MarkdownCustom demonstrates overrides, chart fences, think tags, both plugin types, opt-in HTML and partial streaming. Copy actions show common.copyFailed when clipboard access fails; successful copies reset after 1600ms.
- Package: `@gntik-ai/chat` · export `Markdown` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/Markdown/Markdown.doc.ts
- Install: `gntik-ui add markdown`; own the source with `gntik-ui eject markdown`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves through links and code-block copy buttons in reading order |
| Enter | Follows a link or copies the focused code block |
Tokens: foreground, muted-foreground, primary-text, secondary, border, focus-ring
Usage:
```tsx
import { Alert, Badge, Card, Progress, useI18n } from '@gntik-ai/ui';
import type { ReactNode } from 'react';
import {
Markdown,
useMarkdownContext,
type MarkdownCodeRendererProps,
type MarkdownCustomTagProps,
type MarkdownProps,
} from '@gntik-ai/chat';
function Callout({ children }: { children?: ReactNode }) {
return {children} ;
}
function ChartFence({ code, complete }: MarkdownCodeRendererProps) {
const { t } = useI18n();
const { streaming } = useMarkdownContext();
const value = Number(code);
return (
{t(streaming && !complete ? 'chat.markdownPartial' : 'chat.markdownComplete')}
);
}
function Reasoning({ children, complete }: MarkdownCustomTagProps) {
const { t } = useI18n();
return (
{t(complete ? 'chat.markdownReasoning' : 'chat.markdownPartial')}
{children}
);
}
interface ExampleNode {
type: string;
depth?: number;
tagName?: string;
properties?: Record;
children?: ExampleNode[];
}
function walk(node: ExampleNode, change: (node: ExampleNode) => void) {
change(node);
node.children?.forEach((child) => walk(child, change));
}
// Stable maps and plugin arrays preserve Markdown's memoisation.
const components = { blockquote: Callout };
const codeRenderers = { chart: ChartFence };
const customTags = { think: Reasoning };
const remarkPlugins: MarkdownProps['remarkPlugins'] = [
() => (tree: ExampleNode) => {
walk(tree, (node) => {
if (node.type === 'heading') node.depth = 3;
});
},
];
const rehypePlugins: MarkdownProps['rehypePlugins'] = [
() => (tree: ExampleNode) => {
walk(tree, (node) => {
if (node.tagName === 'details') node.properties = { ...node.properties, open: true };
});
},
];
const extensions = { components, codeRenderers, customTags, remarkPlugins, rehypePlugins };
/** Overrides receive untrusted text: validate data and render React children, never raw HTML. */
export default function MarkdownCustom() {
const { t } = useI18n();
return (
{t('chat.markdownCustomReply')}
{t('chat.markdownCustomPartial')}
);
}
```
### OverflowList (`overflow-list`)
Row of chips or avatars that renders items up to `max` (or to the available width with `responsive`, measured by ResizeObserver) and a "+N more" button that opens a Popover listing the rest. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `OverflowList` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/OverflowList/OverflowList.doc.ts
- Primitive: `@base-ui/react/popover`
- ARIA pattern: list + disclosure popover
- Registry dependencies: popover, ui-utils
- Install: `gntik-ui add overflow-list`; own the source with `gntik-ui eject overflow-list`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves to the "+N more" button (and any interactive items) |
| Enter / Space | On the "+N more" button: opens the popover with the hidden items |
| Escape | Closes the popover and returns focus to the button |
Tokens: card, border, secondary, muted-foreground, foreground, popover, focus-ring
Usage:
```tsx
import { Badge } from '@gntik-ai/ui';
import { OverflowList } from '@gntik-ai/ui';
const LABELS = ['backend', 'billing', 'eu-west-1', 'production', 'pci', 'on-call', 'v2-migration'];
export default function OverflowListLabels() {
return (
{l} }
renderOverflowItem={(l) => l}
/>
{l} }
renderOverflowItem={(l) => l}
/>
);
}
```
### Sparkline (`sparkline`)
Inline trend in pure SVG (no chart library): line or bars, optional area fill and min / max / last markers, fixed width and height. Coloured with token classes (stroke-primary, fill-primary/14…) so it follows the theme, and exposed as role="img" with a generated summary ("Requests: trending up 12% from 140 to 157…").
- Package: `@gntik-ai/ui` · export `Sparkline` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Sparkline/Sparkline.doc.ts
- ARIA pattern: img
- Registry dependencies: ui-utils
- Install: `gntik-ui add sparkline`; own the source with `gntik-ui eject sparkline`
Tokens: primary, success, warning, destructive, info, muted-foreground, card
Usage:
```tsx
import { Sparkline } from '@gntik-ai/ui';
const STATS = [
{
label: 'Requests',
value: '48.2k',
delta: '+12%',
good: true,
data: [31, 34, 33, 38, 36, 41, 40, 44, 43, 46, 45, 48],
area: true,
tone: 'primary',
},
{
label: 'p95 latency',
value: '182 ms',
delta: '−8%',
good: true,
data: [214, 220, 205, 210, 198, 201, 195, 190, 192, 186, 184, 182],
area: false,
tone: 'primary',
},
{
label: 'Error rate',
value: '1.4%',
delta: '+0.6 pt',
good: false,
data: [0.6, 0.7, 0.6, 0.8, 0.9, 0.8, 1.1, 1.0, 1.2, 1.3, 1.2, 1.4],
area: false,
tone: 'destructive',
},
] as const;
export default function SparklineStats() {
return (
{STATS.map((stat) => (
{stat.label}
{stat.value}
{stat.delta}
))}
);
}
```
### StatusDot (`status-dot`)
State dot with an optional label and a soft pulse for live states (disabled under reduced motion). Tones: success, warning, destructive, info, neutral, primary — generic, with no product-specific states. Without a label it needs an aria-label (exposed as an image) so colour is never the only signal.
- Package: `@gntik-ai/ui` · export `StatusDot` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/StatusDot/StatusDot.doc.ts
- ARIA pattern: img (when unlabelled)
- Registry dependencies: ui-utils
- Install: `gntik-ui add status-dot`; own the source with `gntik-ui eject status-dot`
Tokens: success, warning, destructive, info, muted-foreground, primary, foreground
Usage:
```tsx
import { StatusDot } from '@gntik-ai/ui';
const SERVICES = [
{ name: 'api-gateway', tone: 'success', state: 'Healthy' },
{ name: 'billing-worker', tone: 'warning', state: 'Slow responses' },
{ name: 'search-index', tone: 'destructive', state: 'Down' },
] as const;
export default function StatusDotInline() {
return (
{SERVICES.map((svc) => (
{svc.name}
{svc.state}
))}
);
}
```
### StatusTag (`status-tag`)
State pill with a dot (running, queued, paused, failed, draft…). The state → tone mapping is a prop-driven dictionary merged over generic defaults, so products add their own states without touching the component.
- Package: `@gntik-ai/ui` · export `StatusTag` · group Display · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/StatusTag/StatusTag.doc.ts
- Registry dependencies: badge, ui-utils
- Install: `gntik-ui add status-tag`; own the source with `gntik-ui eject status-tag`
Tokens: success, success-text, info, warning, warning-text, destructive, destructive-text, muted-foreground
Usage:
```tsx
import { StatusTag } from '@gntik-ai/ui';
import type { StatusDefinition } from '@gntik-ai/ui';
/** Product-specific states are passed in, never hardcoded in the component. */
const DEPLOYMENT_STATES: Record = {
building: { label: 'Building', tone: 'info' },
live: { label: 'Live', tone: 'primary' },
rolled_back: { label: 'Rolled back', tone: 'warning' },
};
export default function StatusTagStates() {
return (
{['running', 'queued', 'paused', 'draft', 'degraded', 'failed'].map((s) => (
))}
{['building', 'live', 'rolled_back'].map((s) => (
))}
);
}
```
### Table (`table`)
Styled semantic table parts: Table, TableHeader, TableBody, TableFooter, TableRow (selected state), TableHead (optional sort button with aria-sort), TableCell and TableCaption. Compact or comfortable density and an optional sticky header.
- Package: `@gntik-ai/ui` · export `Table` · group Display · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Table/Table.doc.ts
- ARIA pattern: table (sortable columns)
- Registry dependencies: ui-utils
- Install: `gntik-ui add table`; own the source with `gntik-ui eject table`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves focus between sortable column headers and interactive cells |
| Enter | On a sortable header: sorts ascending, then toggles ascending ⇄ descending (aria-sort) |
| Space | On a sortable header: same as Enter |
Tokens: border, card, secondary, accent, primary, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@gntik-ai/ui';
import { DEPLOYMENTS, count } from '@gntik-ai/ui';
const checkboxClass =
'size-4 cursor-pointer accent-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring';
export default function TableSelectable() {
const [selected, setSelected] = useState>(new Set([2]));
const toggle = (id: number) =>
setSelected((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
return (
);
}
```
### Terminal (`terminal`)
Streamed command output (build, deploy, job logs). ANSI SGR colours map to contrast-safe tokens (`parseAnsi` is exported), `\r` progress lines rewrite in place, and a line cap drops the oldest output. It follows the tail until the user scrolls up, then offers “Jump to latest” with the count of new lines. Optional line numbers and timestamps, a wrap toggle, copy all and search with highlighted matches. The output is a `role="log"` with aria-live off unless `live` opts in. Stream through the `ref` handle (`write`, `writeln`, `clear`) or pass `lines`.
- Package: `@gntik-ai/ui` · export `Terminal` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Terminal/Terminal.doc.ts
- ARIA pattern: log · button · searchbox
- Registry dependencies: ui-utils
- Install: `gntik-ui add terminal`; own the source with `gntik-ui eject terminal`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves through the search box, match arrows, wrap toggle, copy button and the output log |
| End | In the log: jumps to the newest line and resumes following the tail |
| Home | In the log: jumps to the first line and stops following |
| Arrow keys / Page Up / Page Down | Scroll the focused log |
| Ctrl+F / ⌘F | Inside the terminal: focuses the search box |
| Enter / Shift+Enter | In the search box: next / previous match (scrolled into view) |
| Escape | In the search box: clears the search |
Tokens: card, background, border, popover, secondary, foreground, muted-foreground, destructive-text, success-text, warning-text, info, category-violet, warning, primary, primary-text, focus-ring
Usage:
```tsx
import { Play, RotateCcw } from '@gntik-ai/icons';
import { useEffect, useRef, useState } from 'react';
import { Button } from '@gntik-ai/ui';
import { Terminal, type TerminalHandle } from '@gntik-ai/ui';
const E = '\u001b[';
/** A fake build: ANSI-coloured lines, one progress line rewritten with `\r`, then a summary. */
const SCRIPT: string[] = [
`${E}2m$ ${E}0mpnpm build\n`,
`${E}36minfo${E}0m resolving 412 packages\n`,
`${E}36minfo${E}0m compiling ${E}1m@acme/web${E}0m\n`,
'progress 25%',
'\rprogress 50%',
'\rprogress 75%',
'\rprogress 100%\n',
`${E}33mwarn${E}0m bundle "vendor" is 612 kB (limit 500 kB)\n`,
`${E}32m✓${E}0m built in 4.2s · ${E}1m3 entries${E}0m\n`,
`${E}31merror${E}0m test "checkout › applies coupon" failed\n`,
`${E}90m at checkout.test.ts:42:7${E}0m\n`,
`${E}1;31m✗ 1 failed${E}0m, ${E}32m128 passed${E}0m\n`,
];
export default function TerminalBuildLog() {
const term = useRef(null);
const [running, setRunning] = useState(false);
const [step, setStep] = useState(0);
useEffect(() => {
if (!running) return;
const id = window.setInterval(() => {
setStep((n) => {
const chunk = SCRIPT[n];
if (chunk === undefined) {
setRunning(false);
return n;
}
term.current?.write(chunk);
return n + 1;
});
}, 350);
return () => window.clearInterval(id);
}, [running]);
return (
= SCRIPT.length} onClick={() => setRunning(true)}>
Run build
{
setRunning(false);
setStep(0);
term.current?.clear();
}}
>
Reset
);
}
```
### Text (`text`)
Type roles on the brand scale. Text: body, label, supporting, caption, code and display, with tones (default, muted, primary, success, warning, destructive — contrast-safe *-text aliases). Heading: semantic level 1–6 with an independent visual size. Both truncate or clamp to N lines and render any element via `as` / `render`.
- Package: `@gntik-ai/ui` · export `Text` · group Display · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Text/Text.doc.ts
- Primitive: `@base-ui/react/use-render`
- Registry dependencies: ui-utils
- Install: `gntik-ui add text`; own the source with `gntik-ui eject text`
Tokens: foreground, muted-foreground, primary-text, success-text, warning-text, destructive-text, secondary
Usage:
```tsx
import { Heading, Text } from '@gntik-ai/ui';
export default function TextRoles() {
return (
2.4k
Project overview
Deployments roll out region by region. Run deploy --canary to ship to a single
region first.
Billing email
Invoices and receipts are sent to this address.
Updated 3 minutes ago
Active
Paid
Due soon
Overdue
);
}
```
### Thumbnail (`thumbnail`)
File or image preview tile: image or file-type icon with extension tag, name and size caption, upload progress (progressbar + shimmer that holds still under reduced motion), error state with retry, and a remove button. ThumbnailList lays tiles out as a wrapping list. Sizes use the shared formatBytes export (decimal SI by default); ThumbnailSizes demonstrates localized labels, IEC base, bits, display widths and precision. Built-in labels and sizes follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `Thumbnail` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Thumbnail/Thumbnail.doc.ts
- ARIA pattern: figure
- Registry dependencies: ui-utils
- Install: `gntik-ui add thumbnail`; own the source with `gntik-ui eject thumbnail`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves to the remove button (and retry, in the error state) of each tile |
| Enter / Space | Activates remove or retry |
Tokens: card, border, secondary, muted-foreground, foreground, primary, destructive, destructive-text, focus-ring
Usage:
```tsx
import { I18nProvider } from '@gntik-ai/ui';
import { formatBytes, type FormatBytesOptions } from '@gntik-ai/ui';
import { Thumbnail, ThumbnailList } from '@gntik-ai/ui';
const FORMATS: FormatBytesOptions[] = [
{ locale: 'en' },
{ locale: 'es' },
{ unitDisplay: 'long' },
{ unitDisplay: 'narrow' },
{ base: 1024 },
{ base: 1024, locale: 'es', unitDisplay: 'long' },
{ unit: 'bit' },
{ unit: 'bit', unitDisplay: 'long' },
{ maximumFractionDigits: 2 },
];
/** Provider-localized sizes, a per-thumbnail override, and the shared formatter options. */
export default function ThumbnailSizes() {
return (
{FORMATS.map((options, index) => (
{formatBytes(25_000_000, options)}
))}
);
}
```
### Timer (`timer`)
Elapsed time since a start, live while running ("1:02:05" or compact "2m 14s"), with paused and stopped states. Renders role="timer" around a duration; silent by default, `announce` speaks it politely once a minute. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `Timer` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Timer/Timer.doc.ts
- ARIA pattern: timer
- Registry dependencies: ui-utils
- Install: `gntik-ui add timer`; own the source with `gntik-ui eject timer`
Keyboard:
| Key | Behaviour |
| --- | --- |
| — | Not interactive; never takes focus |
Tokens: foreground, muted-foreground, success, warning
Usage:
```tsx
import { Timer } from '@gntik-ai/ui';
const T0 = Date.now();
const JOBS = [
{ name: 'Build 482', status: 'running', start: T0 - 134_000 },
{ name: 'Data import', status: 'paused', start: T0 - 3_725_000, end: T0 - 600_000 },
{ name: 'Nightly backup', status: 'stopped', start: T0 - 7_200_000, end: T0 - 5_400_000 },
] as const;
export default function TimerStates() {
return (
{JOBS.map((job) => (
{job.name}
))}
);
}
```
### Timestamp (`timestamp`)
A date in a element: relative ("3 minutes ago", Intl.RelativeTimeFormat) or absolute (Intl.DateTimeFormat), localised with `locale`. Accepts a Date, ISO string or epoch ms. Relative labels refresh on an adaptive interval that is cleared on unmount. The full date shows in a tooltip on hover or focus and is also part of the accessible text. Display timeZone defaults to I18nProvider (system when unset); use the same zone on server and client. The TimeZone example shows the provider default and UTC overrides
- Package: `@gntik-ai/ui` · export `Timestamp` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Timestamp/Timestamp.doc.ts
- Primitive: `@base-ui/react/tooltip`
- ARIA pattern: tooltip
- Registry dependencies: tooltip, ui-utils
- Install: `gntik-ui add timestamp`; own the source with `gntik-ui eject timestamp`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Focuses the timestamp and shows the full date in a tooltip |
| Escape | Hides the tooltip |
Tokens: foreground, muted-foreground, border, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { TooltipProvider } from '@gntik-ai/ui';
import { Timestamp } from '@gntik-ai/ui';
const MINUTE = 60_000;
export default function TimestampActivity() {
const [now] = useState(() => Date.now());
const events = [
{ id: 1, text: 'Maria Ruiz deployed api-gateway to production', at: now - 3 * MINUTE },
{ id: 2, text: 'Invoice INV-2041 was paid', at: now - 2 * 60 * MINUTE },
{
id: 3,
text: 'Daniel Okafor joined the Platform project',
at: new Date(now - 3 * 24 * 60 * MINUTE).toISOString(),
},
];
return (
{events.map((e) => (
{e.text}
))}
);
}
```
### Token (`token`)
Removable chip for active filters and recipient inputs: label with an optional muted prefix ("Region:"), a leading icon or avatar slot, tones (neutral, primary, success, warning, destructive), sizes sm and md, and an accessible "Remove {label}" button. Disabled tokens keep their label but cannot be removed.
- Package: `@gntik-ai/ui` · export `Token` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Token/Token.doc.ts
- ARIA pattern: button (remove)
- Registry dependencies: ui-utils
- Install: `gntik-ui add token`; own the source with `gntik-ui eject token`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves focus to the remove button |
| Enter | Removes the token |
| Space | Removes the token |
| Backspace | Removes the token while its remove button is focused (also Delete) |
Tokens: border, background, foreground, muted-foreground, secondary, primary, primary-text, success-text, warning-text, destructive-text, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Token } from '@gntik-ai/ui';
const INITIAL = [
{ key: 'Status', value: 'Failed' },
{ key: 'Region', value: 'eu-west-1' },
{ key: 'Region', value: 'us-east-1' },
{ key: 'Owner', value: 'Platform team' },
];
export default function TokenFilters() {
const [filters, setFilters] = useState(INITIAL);
const remove = (index: number) => setFilters((list) => list.filter((_, i) => i !== index));
return (
Filters
{filters.map((f, i) => (
remove(i)} />
))}
{filters.length > 0 ? (
setFilters([])}
className="inline-flex h-7 items-center rounded-md px-2 text-[12px] font-medium text-muted-foreground transition-colors hover:text-destructive-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring"
>
Clear all
) : (
No filters applied
)}
);
}
```
### ToolCallCard (`tool-call-card`)
A tool invocation in an assistant turn: name, status (running, succeeded, failed), duration and a collapsible body with the arguments and the result or error as small collapsible JSON views. Failed calls open by default.
- Package: `@gntik-ai/chat` · export `ToolCallCard` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/ToolCallCard/ToolCallCard.doc.ts
- Primitive: `@base-ui/react/collapsible`
- ARIA pattern: disclosure
- Install: `gntik-ui add tool-call-card`; own the source with `gntik-ui eject tool-call-card`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter | Expands or collapses the card (or a JSON section) |
| Space | Expands or collapses the card (or a JSON section) |
| Tab | Moves to the JSON sections and their scrollable code |
Tokens: card, border, secondary, muted-foreground, success-text, destructive, destructive-text, focus-ring
Usage:
```tsx
import { ToolCallCard } from '@gntik-ai/chat';
export default function ToolCallCardStates() {
return (
);
}
```
### VirtualList (`virtual-list`)
Windowed list for thousands of rows: only the visible rows plus `overscan` are mounted, with a fixed `itemHeight` or measured rows from an estimate. One roving tab stop, aria-setsize/posinset on every row, `scrollToIndex` through a ref, and a single-select `listbox` mode. No dependencies. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `VirtualList` · group Display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/VirtualList/VirtualList.doc.ts
- ARIA pattern: list / listbox with roving tabindex
- Registry dependencies: ui-utils
- Install: `gntik-ui add virtual-list`; own the source with `gntik-ui eject virtual-list`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves focus to the active row (one tab stop) |
| ArrowDown / ArrowUp | Moves to the next / previous row, scrolling it into view |
| PageDown / PageUp | Moves one viewport down / up |
| Home / End | Moves to the first / last row |
| Enter | Activates the row (selects it in listbox mode) |
| Space | Listbox mode: selects the focused option |
Tokens: card, border, secondary, primary, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { useRef } from 'react';
import { Button } from '@gntik-ai/ui';
import { VirtualList, type VirtualListHandle } from '@gntik-ai/ui';
const levels = ['info', 'info', 'info', 'warn', 'info', 'error'] as const;
const logs = Array.from({ length: 5000 }, (_, i) => ({
id: `log-${i}`,
time: `12:${String(Math.floor(i / 60) % 60).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}`,
level: levels[i % levels.length] ?? 'info',
message: `deploy-gate: checked deployment #${4100 + i} for open incidents`,
}));
const levelClass = {
info: 'text-muted-foreground',
warn: 'text-warning-text',
error: 'text-destructive-text',
} as const;
export default function VirtualListLogs() {
const list = useRef(null);
return (
Deployment log{' '}
· {logs.length.toLocaleString('en-US')} lines
list.current?.scrollToIndex(logs.length - 1, { focus: true })}
>
Jump to latest
l.id}
itemHeight={30}
height={280}
rowClassName="gap-3 px-3 font-mono text-[12px]"
renderItem={(l) => (
<>
{l.time}
{l.level}
{l.message}
>
)}
/>
);
}
```
### Alert (`alert`)
Inline banner in four tones (info, success, warning, destructive) with title, description, an action row and an optional dismiss button. Destructive alerts use role="alert"; the others role="status".
- Package: `@gntik-ai/ui` · export `Alert` · group Feedback · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Alert/Alert.doc.ts
- ARIA pattern: alert / status
- Registry dependencies: ui-utils
- Install: `gntik-ui add alert`; own the source with `gntik-ui eject alert`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves focus to the actions and the dismiss button |
| Enter | Activates the dismiss button |
| Space | Activates the dismiss button |
Tokens: info, success, success-text, warning, warning-text, destructive, destructive-text, foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Button } from '@gntik-ai/ui';
import { Alert } from '@gntik-ai/ui';
export default function AlertDismissible() {
const [open, setOpen] = useState(true);
if (!open) {
return (
setOpen(true)}>
Show alert again
);
}
return (
setOpen(false)}
actions={
<>
Add payment method
View plans
>
}
className="max-w-2xl"
/>
);
}
```
### EmptyState (`empty-state`)
Placeholder for an empty list, table or page: a neutral icon halo, a title, a short explanation and primary / secondary actions. Three sizes and an optional dashed border.
- Package: `@gntik-ai/ui` · export `EmptyState` · group Feedback · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/EmptyState/EmptyState.doc.ts
- Registry dependencies: ui-utils
- Install: `gntik-ui add empty-state`; own the source with `gntik-ui eject empty-state`
Tokens: secondary, border, foreground, muted-foreground
Usage:
```tsx
import { FolderPlus, Plus, Upload } from '@gntik-ai/icons';
import { Button } from '@gntik-ai/ui';
import { EmptyState } from '@gntik-ai/ui';
export default function EmptyStateProjects() {
return (
New project}
secondaryAction={
Import
}
/>
);
}
```
### LiveAnnouncer (`live-announcer`)
Screen-reader announcements for changes with no visible text: a polite (role="status") and an assertive aria-live region, plus `useAnnounce()` for any component below it. Used by KanbanBoard for pick-up / move / drop / cancel. Outside a LiveAnnouncer, `useAnnounce()` is a no-op.
- Package: `@gntik-ai/ui` · export `LiveAnnouncer` · group Feedback · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/LiveAnnouncer/LiveAnnouncer.doc.ts
- ARIA pattern: aria-live region (status)
- Registry dependencies: ui-utils
- Install: `gntik-ui add live-announcer`; own the source with `gntik-ui eject live-announcer`
Usage:
```tsx
import { useState } from 'react';
import { Button } from '@gntik-ai/ui';
import { LiveAnnouncer, useAnnounce } from '@gntik-ai/ui';
function SaveDraft() {
const announce = useAnnounce();
const [saves, setSaves] = useState(0);
return (
{
setSaves((n) => n + 1);
announce('Draft saved.');
}}
>
Save draft
Saved {saves} times
);
}
export default function LiveAnnouncerSave() {
return (
);
}
```
### Meter (`meter`)
Usage meter for quotas and limits: label, value text, and a level set by thresholds (healthy → warning near the cap → destructive at or over it), with an optional note under the bar. `higherIsBetter` inverts the thresholds for scores and health (low = warning / destructive).
- Package: `@gntik-ai/ui` · export `Meter` · group Feedback · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Meter/Meter.doc.ts
- Primitive: `@base-ui/react/meter`
- ARIA pattern: meter
- Registry dependencies: ui-utils
- Install: `gntik-ui add meter`; own the source with `gntik-ui eject meter`
Tokens: primary, warning, warning-text, destructive, destructive-text, secondary, foreground, muted-foreground
Usage:
```tsx
import { Meter } from '@gntik-ai/ui';
/** Scores and budgets where low is bad: `higherIsBetter` inverts the thresholds. */
export default function MeterHealth() {
return (
);
}
```
### Progress (`progress`)
Progress bar for a running task: optional label and formatted value, four tones (primary, success, warning, destructive), three heights, and an indeterminate state (value null) that stops pulsing under reduced motion.
- Package: `@gntik-ai/ui` · export `Progress` · group Feedback · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Progress/Progress.doc.ts
- Primitive: `@base-ui/react/progress`
- ARIA pattern: progressbar
- Registry dependencies: ui-utils
- Install: `gntik-ui add progress`; own the source with `gntik-ui eject progress`
Tokens: primary, success, success-text, warning, warning-text, destructive, destructive-text, secondary, foreground
Usage:
```tsx
import { useState } from 'react';
import { Button } from '@gntik-ai/ui';
import { Progress } from '@gntik-ai/ui';
export default function ProgressBars() {
const [value, setValue] = useState(34);
return (
setValue((v) => Math.max(0, v - 10))}>
Back
setValue((v) => Math.min(100, v + 10))}>
Advance
);
}
```
### Skeleton (`skeleton`)
Loading placeholder in the shape of the content: block, text (one or more lines, the last shorter) or circle. Pulses gently and stops under prefers-reduced-motion. Decorative; announce loading on the container.
- Package: `@gntik-ai/ui` · export `Skeleton` · group Feedback · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Skeleton/Skeleton.doc.ts
- Registry dependencies: ui-utils
- Install: `gntik-ui add skeleton`; own the source with `gntik-ui eject skeleton`
Tokens: muted
Usage:
```tsx
import { Skeleton } from '@gntik-ai/ui';
export default function SkeletonList() {
return (
{[0, 1, 2].map((i) => (
))}
);
}
```
### Spinner (`spinner`)
Indeterminate loading indicator that inherits the current text colour.
- Package: `@gntik-ai/ui` · export `Spinner` · group Feedback · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Spinner/Spinner.doc.ts
- ARIA pattern: status
- Registry dependencies: ui-utils
- Install: `gntik-ui add spinner`; own the source with `gntik-ui eject spinner`
Tokens: currentColor
Usage:
```tsx
import { Spinner } from '@gntik-ai/ui';
```
### Toast (`toast`)
Short-lived notification stacked bottom-right. Five tones (neutral, success, info, warning, destructive) plus loading, title, description, a primary action and a dismiss action next to the close button; auto-dismiss pauses while the stack is hovered or focused. `toast.promise(task, { loading, success, error })` updates one toast in place (errors are announced assertively). Mount ToastProvider + Toaster once and queue with the useToast() hook.
- Package: `@gntik-ai/ui` · export `Toast` · group Feedback · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Toast/Toast.doc.ts
- Primitive: `@base-ui/react/toast`
- ARIA pattern: region + live announcements (status)
- Registry dependencies: spinner, ui-utils
- Install: `gntik-ui add toast`; own the source with `gntik-ui eject toast`
Keyboard:
| Key | Behaviour |
| --- | --- |
| F6 | Moves focus into the notifications region (pauses auto-dismiss) |
| Tab / Shift+Tab | Reaches each toast’s primary action, dismiss action and close button |
| Enter / Space | On the close button or dismiss action: dismisses the toast (an action also runs its handler) |
Tokens: popover, popover-foreground, border, foreground, muted-foreground, success-text, info, warning-text, destructive-text, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Button } from '@gntik-ai/ui';
import { ToastProvider, Toaster, useToast } from '@gntik-ai/ui';
function InviteButton() {
const toast = useToast();
const [status, setStatus] = useState('No reply yet');
return (
toast.add({
tone: 'info',
title: 'You were invited to “Platform”',
description: 'Join the workspace to see its projects.',
timeout: 10000,
action: { label: 'Accept', onClick: () => setStatus('Accepted') },
dismissAction: { label: 'Not now', onClick: () => setStatus('Postponed') },
})
}
>
Show invitation
{status}
);
}
export default function ToastActions() {
return (
);
}
```
### TypingIndicator (`typing-indicator`)
Three pulsing dots shown while a reply is being written, with the text "Assistant is typing" for screen readers (or visible with `showLabel`). The dots are decorative and hold still under reduced motion. ChatMessage renders it while a streaming turn is still empty.
- Package: `@gntik-ai/chat` · export `TypingIndicator` · group Feedback · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/TypingIndicator/TypingIndicator.doc.ts
- Install: `gntik-ui add typing-indicator`; own the source with `gntik-ui eject typing-indicator`
Tokens: muted-foreground
Usage:
```tsx
import { TypingIndicator } from '@gntik-ai/chat';
export default function TypingIndicatorStates() {
return (
);
}
```
### Calendar (`calendar`)
The public localeWeekStart(locale?) helper returns the locale first weekday (0–6, Monday fallback), matching scheduling blocks. Day or range selection with one or two months, inclusive min/max bounds and Intl names. The clickable month heading opens a month grid, then the year heading opens a shared 12-year page (2020–2031 for 2026). Set maxView="day" for the previous static header, or "month" to stop at months; the default is "year". Optional view/defaultView/onViewChange control or report the view (initially "day"). captionLayout="select" uses labelled kit month/year selects; yearRange={from,to} defaults to the current year ±10, clipped to min/max and including the visible year. Picking a month returns to days, preserving and clamping the focused day, and calls onMonthChange once with the first day. DatePicker and DateRangePicker forward maxView and captionLayout and reopen in the day view.
- Package: `@gntik-ai/ui` · export `Calendar` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Calendar/Calendar.doc.ts
- ARIA pattern: grid (date picker dialog calendar)
- Registry dependencies: select, ui-utils
- Install: `gntik-ui add calendar`; own the source with `gntik-ui eject calendar`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Day: ArrowLeft / ArrowRight | Moves focus to the previous / next day (reversed in RTL) |
| Day: ArrowUp / ArrowDown | Moves focus to the same day in the previous / next week |
| Day: PageUp / PageDown | Moves focus to the same day in the previous / next month (Shift: year) |
| Day: Home / End | Moves focus to the first / last day of the week |
| Day: Enter / Space | Selects the focused day (range: first press sets the start, second the end) |
| Month: ArrowLeft / ArrowRight | Moves one month (reversed in RTL) |
| Month: ArrowUp / ArrowDown | Moves three months |
| Month: Home / End | Moves to the first / last month of the row |
| Month: PageUp / PageDown | Moves one year |
| Month: Enter / Space | Opens the day view of the focused month; disabled months do nothing |
| Year: ArrowLeft / ArrowRight | Moves one year (reversed in RTL) |
| Year: ArrowUp / ArrowDown | Moves three years |
| Year: Home / End | Moves to the first / last year of the row |
| Year: PageUp / PageDown | Moves twelve years |
| Year: Enter / Space | Opens the month view of the focused year; disabled years do nothing |
| Heading: Enter / Space | Opens the month view, then year view, up to maxView |
| Popup: Escape | Closes the date picker and returns focus to its field from any view |
Tokens: foreground, muted-foreground, primary, primary-foreground, secondary, focus-ring
Usage:
```tsx
import { I18nProvider, localeWeekStart, useI18n } from '@gntik-ai/ui';
import { Calendar } from '@gntik-ai/ui';
function LocaleCalendar() {
const { locale, t } = useI18n();
return (
);
}
/** The exported helper matches the Calendar and scheduling defaults. */
export default function CalendarLocale() {
return (
{['en-US', 'es-ES'].map((locale) => (
))}
);
}
```
### ChatComposer (`chat-composer`)
Chat input: autosizing textarea, Enter sends and Shift+Enter adds a line, attachments (pick, paste or drag-and-drop files; `accept`, `maxSize` and `maxFiles` validation with messages in an alert; chips with image thumbnails, file-type icons, size, upload progress, error + Retry and Remove), a model-picker slot (e.g. a compact ModelPicker), a character counter near the limit, a disabled state, and a Stop button while a reply streams.
- Package: `@gntik-ai/chat` · export `ChatComposer` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/ChatComposer/ChatComposer.doc.ts
- Registry dependencies: attachment
- Install: `gntik-ui add chat-composer`; own the source with `gntik-ui eject chat-composer`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter | Sends the message (ignored while streaming or during IME composition) |
| Shift+Enter | Inserts a new line |
| Escape | While a reply streams: stops it |
| Tab | Moves through attachment Retry/Remove buttons, the text box, attach, tools, the model picker and Send/Stop |
| Ctrl/⌘+V | In the text box: pasted files are attached (validated) instead of inserted |
Tokens: card, destructive-chip-text, primary-text, border, primary, ring, secondary, muted-foreground, destructive-text, focus-ring
Usage:
```tsx
import { useEffect, useRef, useState } from 'react';
import { ModelPicker, DEMO_MODELS } from '@gntik-ai/chat';
import { ChatComposer, type ComposerAttachment } from '@gntik-ai/chat';
let seq = 0;
export default function ChatComposerFull() {
const [model, setModel] = useState('balanced');
const [files, setFiles] = useState([
{ id: 'f1', name: 'invoices-september.csv', size: 48_213, type: 'text/csv' },
{ id: 'f2', name: 'release-notes.md', size: 9_042, error: 'Network error' },
]);
const [sent, setSent] = useState([]);
const timers = useRef>>([]);
const urls = useRef([]);
useEffect(
() => () => {
timers.current.forEach(clearInterval);
urls.current.forEach((u) => URL.revokeObjectURL?.(u));
},
[],
);
// Mock upload: progress ticks to 100.
const upload = (id: string) => {
const timer = setInterval(() => {
setFiles((all) =>
all.map((f) => {
if (f.id !== id) return f;
const progress = Math.min(100, (f.progress ?? 0) + 20);
if (progress >= 100) clearInterval(timer);
return { ...f, progress };
}),
);
}, 300);
timers.current.push(timer);
};
const attach = (picked: File[]) => {
const added = picked.map((p) => {
const id = `file-${++seq}`;
const previewUrl =
p.type.startsWith('image/') && typeof URL.createObjectURL === 'function' ? URL.createObjectURL(p) : undefined;
if (previewUrl) urls.current.push(previewUrl);
return { id, name: p.name, size: p.size, type: p.type, previewUrl, progress: 0 };
});
setFiles((f) => [...f, ...added]);
added.forEach((a) => upload(a.id));
};
return (
setFiles((f) => f.filter((x) => x.id !== id))}
onRetryAttachment={(id) => {
setFiles((f) => f.map((x) => (x.id === id ? { ...x, error: undefined, progress: 0 } : x)));
upload(id);
}}
modelPicker={ }
onSubmit={(text) => {
setSent((s) => [text, ...s].slice(0, 3));
setFiles([]);
}}
/>
{sent.length > 0 && (
{sent.map((t, i) => (
Sent: {t}
))}
)}
);
}
```
### Checkbox (`checkbox`)
Checkbox with optional label and description, including the indeterminate state. CheckboxGroup shares state across several checkboxes (plain or bordered list layout) and supports a parent "select all" checkbox.
- Package: `@gntik-ai/ui` · export `Checkbox` · group Forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Checkbox/Checkbox.doc.ts
- Primitive: `@base-ui/react/checkbox, @base-ui/react/checkbox-group`
- ARIA pattern: checkbox (dual and mixed state)
- Registry dependencies: ui-utils
- Install: `gntik-ui add checkbox`; own the source with `gntik-ui eject checkbox`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Space | Toggles the focused checkbox; a parent checkbox ticks or clears the whole group |
| Tab | Moves focus to the next checkbox (each checkbox is a tab stop) |
Tokens: background, border, primary, primary-foreground, foreground, muted-foreground, destructive, focus-ring
Usage:
```tsx
import { useId, useState } from 'react';
import { Checkbox, CheckboxGroup } from '@gntik-ai/ui';
const OPTIONS = [
{ value: 'mentions', label: 'Mentions', description: 'When someone mentions you in a comment or review.' },
{ value: 'deployments', label: 'Deployments', description: 'When a deployment you own succeeds or fails.' },
{ value: 'billing', label: 'Billing', description: 'Invoices, failed payments and plan changes.' },
];
export default function CheckboxList() {
const [value, setValue] = useState(['mentions', 'deployments']);
const id = useId();
return (
Email notifications
{OPTIONS.map((o) => (
))}
);
}
```
### ChoiceCard (`choice-card`)
Cards as controls. ClickableCard: the whole card is one link or button (a stretched title, so there is a single focus target with a short name) with icon, description, meta and a chevron. SelectableCard inside SelectableCardGroup: option cards with radio (single) or checkbox (multiple) semantics on Base UI RadioGroup / CheckboxGroup, with a check indicator, title, description, meta and disabled state.
- Package: `@gntik-ai/ui` · export `ChoiceCard` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/ChoiceCard/ChoiceCard.doc.ts
- Primitive: `@base-ui/react/radio-group · @base-ui/react/checkbox-group`
- ARIA pattern: link / button · radiogroup · group of checkboxes
- Registry dependencies: ui-utils
- Install: `gntik-ui add choice-card`; own the source with `gntik-ui eject choice-card`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | ClickableCard: focuses its link or button. Group: enters the radio group once, on the selected card; visits each checkbox card |
| Enter | Activates a ClickableCard |
| Arrow keys | Radio group: move to the next / previous card and select it |
| Space | Checkbox group: toggles the focused card (radio: selects it) |
Tokens: background, border, secondary, foreground, muted-foreground, primary, primary-foreground, focus-ring
Usage:
```tsx
import { FolderKanban, Receipt, Users } from '@gntik-ai/icons';
import { ClickableCard } from '@gntik-ai/ui';
export default function ClickableCardList() {
return (
}
title="Atlas"
description="Customer-facing API and web app."
meta={8 members · updated 2 hours ago }
/>
}
title="Members"
description="Invite people and manage roles."
/>
{}}
icon={ }
title="Download invoices"
description="Export every invoice of this year as a ZIP."
chevron={false}
/>
}
title="Archived projects"
description="Available on the Business plan."
disabled
/>
);
}
```
### Combobox (`combobox`)
Searchable select: typing filters the list. Single select (ComboboxInput) or multi-select with removable chips (ComboboxChipsInput), an announced empty state, and rich rows via ComboboxItem children. Async options (`loadOptions(query, { signal })`: debounced, stale requests aborted, announced loading and error states with a Retry row) and creatable (`onCreate(input)` adds a “Create …” row). Opt-in `virtualized` windows flat items arrays (including async, Create and Retry rows); grouped items and static children retain full rendering. ComboboxContent accepts itemHeight (fixed pixels), estimatedItemHeight (default 32) and overscan (default 4). Keyboard highlights stay mounted; filtering resets scroll and reopening reveals the selection. Find-in-page and screen-reader browse mode reach mounted rows only; keyboard navigation reaches all options.
- Package: `@gntik-ai/ui` · export `Combobox` · group Forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Combobox/Combobox.doc.ts
- Primitive: `@base-ui/react/combobox`
- ARIA pattern: combobox (list autocomplete) + listbox
- Registry dependencies: select, spinner, ui-utils, virtual-list
- Install: `gntik-ui add combobox`; own the source with `gntik-ui eject combobox`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Home / End, PageUp / PageDown | Virtualized: jumps to the first / last option or moves by a viewport |
| Type a character | Opens the popup and filters the options |
| ArrowDown / ArrowUp | Opens the popup and moves the highlight between options |
| Enter | Selects the highlighted option (adds a chip in multi-select) |
| Escape | Closes the popup, keeping focus in the input |
| Backspace | Multi-select, empty input: removes the last chip |
| Enter on “Create …” / “Retry” | Creates an item from the typed text (onCreate) / loads the options again after an error |
Tokens: background, border, popover, popover-foreground, primary, primary-text, secondary, muted-foreground, focus-ring
Usage:
```tsx
import { useCallback } from 'react';
import { useI18n } from '@gntik-ai/ui';
import { matchText } from '@gntik-ai/ui';
import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem } from '@gntik-ai/ui';
interface Member {
value: string;
label: string;
email: string;
}
const DIRECTORY: Member[] = [
{ value: 'emma', label: 'Emma Crown', email: 'emma@example.com' },
{ value: 'leo', label: 'Leo Park', email: 'leo@example.com' },
{ value: 'mara', label: 'Mara Vidal', email: 'mara@example.com' },
{ value: 'noah', label: 'Noah Frey', email: 'noah@example.com' },
{ value: 'ines', label: 'Ines Roca', email: 'ines@example.com' },
{ value: 'omar', label: 'Omar Haddad', email: 'omar@example.com' },
{ value: 'yuki', label: 'Yuki Sato', email: 'yuki@example.com' },
];
/** Stands in for `fetch('/api/members?q=…', { signal })`: resolves after a short delay, honours abort. */
function searchMembers(query: string, { signal }: { signal: AbortSignal }, locale?: string): Promise {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
resolve(DIRECTORY.filter((m) => matchText(`${m.label} ${m.email}`, query, { locale })));
}, 300);
signal.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
export default function ComboboxAsync() {
const { locale } = useI18n();
const loadOptions = useCallback(
(query: string, context: { signal: AbortSignal }) => searchMembers(query, context, locale),
[locale],
);
return (
loadOptions={loadOptions} debounceMs={250}>
{(m: Member) => (
{m.label}
{m.email}
)}
Results load as you type; stale requests are cancelled.
);
}
```
### DateField (`date-field`)
Typed local date and date-time entry in Intl locale order. DateField and DateRangeField expose named spinbuttons, calendar/min/max/disabled-date validation, ISO or numeric locale paste, and hidden form values. Granularity adds hours, minutes or seconds; a 12-hour clock adds AM/PM. Two-digit years remain literal (26 means year 26); empty segments use the TimePicker placeholder. forceLeadingZeros pads supplied values; typed segments are padded. Works inside Field. DatePicker triggers remain read-only.
- Package: `@gntik-ai/ui` · export `DateField` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/DateField/DateField.doc.ts
- Primitive: `@base-ui/react/field (control) + spinbutton segments`
- ARIA pattern: group of spinbuttons (date field)
- Registry dependencies: calendar, time-picker, ui-utils
- Install: `gntik-ui add date-field`; own the source with `gntik-ui eject date-field`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Moves between segments, then out of the field; finishes a partially typed year |
| 0–9 | Types digits and advances when full; years accept up to four digits |
| ArrowUp / ArrowDown | Increments / decrements and wraps within the segment; day bounds follow month and leap year |
| Home / End | Sets the focused segment to its minimum / maximum |
| ArrowLeft / ArrowRight | Moves to the previous / next segment, including across a range |
| Backspace / Delete | Clears the segment and submitted value |
| A / P | On the AM/PM segment: selects AM / PM |
| Paste | Fills from ISO date/date-time or locale-ordered numeric date; ignores other text |
Tokens: background, border, foreground, muted-foreground, primary, primary-chip-text, secondary, destructive, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { useI18n } from '@gntik-ai/ui';
import { Field, FieldLabel } from '@gntik-ai/ui';
import { DateField } from '@gntik-ai/ui';
export default function DateFieldBasic() {
const { t } = useI18n();
const [date, setDate] = useState(new Date(2026, 9, 1));
return (
{t('dateField.label')}
);
}
```
### DatePicker (`date-picker`)
Date field with a focusable trigger ref: a read-only input with the Intl-formatted date that opens a Popover with a Calendar. DateRangePicker (alias DateRangeInput) adds presets — Today, Last 7 days, Last 30 days, This month, Last month, Custom — beside a two-month range calendar. Min/max, disabled dates, locale, sizes, invalid state; works inside Field. `withTime` adds a TimePicker under the calendar (date + time value, 12/24h, minute step), closed with Done. DatePicker and DateRangePicker accept ref on their input; calling focus leaves the popup closed. Both pickers support `clearable`, `onClear` and `clearLabel`: reset from the field button, Delete/Backspace, or the popup Clear action. Clearing closes the popup and restores field focus. `readOnly` prevents edits.
- Package: `@gntik-ai/ui` · export `DatePicker` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/DatePicker/DatePicker.doc.ts
- Primitive: `@base-ui/react/popover + Calendar (+ TimePicker with `withTime`)`
- ARIA pattern: combobox with a dialog popup (date picker dialog)
- Registry dependencies: button, calendar, popover, time-picker, ui-utils
- Install: `gntik-ui add date-picker`; own the source with `gntik-ui eject date-picker`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Delete / Backspace | With clearable on a non-empty editable trigger: clears and keeps focus |
| Enter / Space | On the field: opens the calendar with focus on the selected day (or today) |
| Arrow keys / PageUp / PageDown / Home / End | Move through the calendar (see Calendar) |
| Enter | In the calendar: picks the day (range: start, then end) and closes (`withTime`: stays open) |
| Escape | Closes the popup and returns focus to the field |
| Tab | `withTime`: from the calendar to the time segments and Done (see TimePicker for their keys) |
Tokens: background, border, foreground, muted-foreground, popover, primary, primary-text, secondary, destructive, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Field, FieldDescription, FieldError, FieldLabel } from '@gntik-ai/ui';
import { DatePicker } from '@gntik-ai/ui';
const today = new Date(2026, 5, 15);
const isWeekend = (d: Date) => d.getDay() === 0 || d.getDay() === 6;
export default function DatePickerBasic() {
const [release, setRelease] = useState(new Date(2026, 5, 18));
return (
Release date
Releases ship on weekdays only.
Invoice due
Choose a due date.
Contract start
);
}
```
### DateTimeRangePicker (`date-time-range-picker`)
Time range field for logs and metrics: quick presets (last 15 minutes, 1 hour, 24 hours, 7 days, 30 days, custom), a relative "Last N minutes/hours/days" mode and an absolute mode with a range Calendar plus start and end TimePickers, validated so the end is after the start. The value is serialisable: `{ mode: "relative", amount, unit }` or `{ mode: "absolute", start, end }`; `resolveTimeRange` turns it into instants.
- Package: `@gntik-ai/ui` · export `DateTimeRangePicker` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/DateTimeRangePicker/DateTimeRangePicker.doc.ts
- Primitive: `@base-ui/react/popover + Calendar, TimePicker, NumberInput, Select, ToggleGroup`
- ARIA pattern: combobox with a dialog popup
- Registry dependencies: button, calendar, date-picker, number-input, popover, select, time-picker, toggle-group, ui-utils
- Install: `gntik-ui add date-time-range-picker`; own the source with `gntik-ui eject date-time-range-picker`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space | On the field: opens the popup with focus on the active preset |
| Tab / Shift+Tab | Moves through presets, the mode switch, the editors and Apply |
| Enter / Space | On a preset: applies it and closes; on Custom range: switches to absolute mode and focuses the calendar |
| ArrowLeft / ArrowRight | In the mode switch: moves between Relative and Absolute |
| Arrow keys / PageUp / PageDown | In the calendar: move between days (see Calendar); in the time fields: spin (see TimePicker) |
| Escape | Closes the popup without applying and returns focus to the field |
Tokens: background, border, foreground, muted-foreground, popover, primary, primary-chip-text, secondary, destructive-text, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Field, FieldDescription, FieldLabel } from '@gntik-ai/ui';
import { DateTimeRangePicker } from '@gntik-ai/ui';
import type { TimeRangeValue } from '@gntik-ai/ui';
const now = new Date(2026, 5, 15, 12, 0);
export default function DateTimeRangePickerBasic() {
const [range, setRange] = useState({ mode: 'relative', amount: 24, unit: 'hour' });
return (
Log window
Relative ranges end now; custom ranges are absolute.
Incident period
);
}
```
### Field (`field`)
Accessible form field scaffolding: Field, FieldLabel, FieldDescription, FieldError and FieldItem wire ids and aria-describedby for any kit control; Fieldset and FieldsetLegend group related fields under one name.
- Package: `@gntik-ai/ui` · export `Field` · group Forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Field/Field.doc.ts
- Primitive: `@base-ui/react/field, @base-ui/react/fieldset`
- ARIA pattern: form field (label + description + error), fieldset/legend
- Registry dependencies: ui-utils
- Install: `gntik-ui add field`; own the source with `gntik-ui eject field`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Click on label | Moves focus to the field control |
| Tab | Moves focus to the control; its description and error are announced via aria-describedby |
Tokens: foreground, muted-foreground, destructive-text
Usage:
```tsx
import { useState } from 'react';
import { Button } from '@gntik-ai/ui';
import { Input } from '@gntik-ai/ui';
import { Field, FieldDescription, FieldError, FieldLabel } from '@gntik-ai/ui';
const isWorkEmail = (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
export default function FieldValidation() {
const [email, setEmail] = useState('member@example');
const [submitted, setSubmitted] = useState(true);
const emailInvalid = submitted && !isWorkEmail(email);
return (
);
}
```
### Input (`input`)
Single-line text input in two sizes (sm, md) with optional leading/trailing icons and addons (prefixes, units, keyboard hints, icon buttons). Invalid, disabled and read-only states; works inside Field. Opt-in `clearable` adds a labelled reset button; `clearOnEscape` consumes Escape only while the editable input has text. `onClear` runs after the value change and `clearLabel` overrides the localized name.
- Package: `@gntik-ai/ui` · export `Input` · group Forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Input/Input.doc.ts
- Primitive: `@base-ui/react/input`
- ARIA pattern: textbox
- Registry dependencies: ui-utils
- Install: `gntik-ui add input`; own the source with `gntik-ui eject input`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Escape | With clearOnEscape and non-empty editable text: clears and stops propagation; otherwise propagates |
| Tab | Moves focus into the input; trailing addon buttons follow in tab order |
| Click on label | Inside a Field, focuses the input |
Tokens: background, border, foreground, muted-foreground, primary, destructive, secondary, focus-ring
Usage:
```tsx
import { Eye, EyeOff, Lock, Search, Zap } from '@gntik-ai/icons';
import { useState } from 'react';
import { IconButton } from '@gntik-ai/ui';
import { Input, InputKbd } from '@gntik-ai/ui';
export default function InputAddons() {
const [show, setShow] = useState(false);
return (
⌘K} placeholder="Go to…" aria-label="Go to" />
setShow((v) => !v)}
/>
}
/>
);
}
```
### KeyValueEditor (`key-value-editor`)
Editable list of key/value rows (environment variables, headers, labels): add and remove rows (no reordering), per-row validation (required key, key pattern, duplicate keys, optional required value and a custom `validate`), secret rows whose values are masked with a reveal toggle, bulk paste of `.env` text (comments ignored, existing keys updated) and "Copy as .env". Controlled (`value` + `onChange`) or uncontrolled. `parseEnv`, `serializeEnv`, `validateKeyValueRows` and `createKeyValueRow` are exported.
- Package: `@gntik-ai/ui` · export `KeyValueEditor` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/KeyValueEditor/KeyValueEditor.doc.ts
- Primitive: `@base-ui/react/input`
- ARIA pattern: group of textboxes + toggle buttons
- Registry dependencies: button, copy-button, input, live-announcer, password-input, textarea, ui-utils
- Install: `gntik-ui add key-value-editor`; own the source with `gntik-ui eject key-value-editor`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves through each row: key, value, secret toggle, remove; then Add, Paste .env and Copy as .env |
| Enter / Space | On Add variable: appends a row and focuses its key |
| Enter / Space | On Remove: deletes the row, announces it and focuses the next row (or Add) |
| Enter / Space | On the secret toggle: masks or unmasks the value (aria-pressed) |
| Ctrl / ⌘ + V | In a key field, multi-line KEY=value text is imported as rows |
Tokens: background, border, card, foreground, muted-foreground, secondary, destructive, destructive-text, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { createKeyValueRow, type KeyValueRow } from '@gntik-ai/ui';
import { KeyValueEditor } from '@gntik-ai/ui';
const initial: KeyValueRow[] = [
createKeyValueRow('API_URL', 'https://api.example.com'),
createKeyValueRow('LOG_LEVEL', 'info'),
createKeyValueRow('DATABASE_PASSWORD', 'change-me-in-production', true),
];
/** Controlled: the parent keeps the rows and the validity (e.g. to disable Save). */
export default function KeyValueEditorEnv() {
const [rows, setRows] = useState(initial);
const [valid, setValid] = useState(true);
return (
setValid(ok)}
/>
{rows.filter((r) => r.key).length} variables · {valid ? 'ready to save' : 'fix the highlighted rows to save'}
);
}
```
### ListInput (`list-input`)
Repeatable key/value rows for environment variables, headers or labels: add, remove and reorder (buttons or Alt+Arrow), paste `KEY=value` lines to bulk-add, per-row validation with duplicate-key detection, optional masked values with a reveal toggle. Changes are announced. Use ListInput for standalone pairs without a form library; use FieldArray from @gntik-ai/ui/forms for arbitrary object fields managed by react-hook-form. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `ListInput` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/ListInput/ListInput.doc.ts
- ARIA pattern: list of labelled text field pairs
- Registry dependencies: button, input, ui-utils
- Install: `gntik-ui add list-input`; own the source with `gntik-ui eject list-input`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves through key, value, reveal, move and remove controls row by row |
| Enter (key field) | Moves to the row value |
| Enter (last value) | Adds a row and focuses its key |
| Alt+ArrowUp / Alt+ArrowDown | Moves the row up / down, keeping focus in the same field |
| Paste | Multi-line or KEY=value text is split into rows |
| Enter / Space on a remove button | Removes the row and focuses the next one |
Tokens: background, border, foreground, muted-foreground, destructive, destructive-text, secondary, focus-ring
Usage:
```tsx
import { ListInput } from '@gntik-ai/ui';
const SECRET = /(SECRET|TOKEN|PASSWORD|KEY)$/;
export default function ListInputEnvVars() {
return (
Environment variables
SECRET.test(row.key)}
validate={(row) => ({
key: /^[A-Z_][A-Z0-9_]*$/.test(row.key) ? null : 'Use UPPER_SNAKE_CASE.',
value: row.value ? null : 'A value is required.',
})}
/>
);
}
```
### ModelPicker (`model-picker`)
Choose the model (and provider) for a conversation: options grouped by provider with a description, capability badges (vision, tools, reasoning, files, web, audio) and the context size; unavailable models stay listed, disabled, with the reason. `compact` gives a small borderless trigger for the ChatComposer toolbar. Controlled (`value` + `onValueChange`).
- Package: `@gntik-ai/chat` · export `ModelPicker` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/ModelPicker/ModelPicker.doc.ts
- Primitive: `Select`
- ARIA pattern: select-only combobox
- Install: `gntik-ui add model-picker`; own the source with `gntik-ui eject model-picker`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space / ArrowDown | On the trigger: opens the list |
| ArrowDown / ArrowUp | Moves between models; a disabled one can be highlighted (its reason is read) but not selected |
| Type a character | Jumps to the model whose name starts with it |
| Enter | Selects the highlighted model and closes |
| Escape | Closes without changing; focus returns to the trigger |
Tokens: background, popover, border, secondary, primary, primary-text, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { ModelPicker } from '@gntik-ai/chat';
import { DEMO_MODELS } from '@gntik-ai/chat';
export default function ModelPickerProviders() {
const [model, setModel] = useState('balanced');
const [compactModel, setCompactModel] = useState('swift');
return (
);
}
```
### MultiSelect (`multi-select`)
Searchable multi-select: selected options as removable chips (extra ones collapse into "+N"), a "Select all" row, a clear button and rows with descriptions. Built on Base UI Combobox (multiple) with the Combobox house styles; works inside Field. Optional async options (`loadOptions`, debounced and abortable, with loading/error states) and creatable rows (`onCreate`). Opt-in virtualized renders only visible rows, with itemHeight, estimatedItemHeight (default 32) and overscan (default 4). Select all applies to the full enabled data set. Keyboard highlights stay mounted; find-in-page and browse mode reach rendered rows only. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `MultiSelect` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/MultiSelect/MultiSelect.doc.ts
- Primitive: `@base-ui/react/combobox`
- ARIA pattern: combobox (multi-select listbox)
- Registry dependencies: combobox, spinner, ui-utils, virtual-list
- Install: `gntik-ui add multi-select`; own the source with `gntik-ui eject multi-select`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Home / End, PageUp / PageDown | Virtualized: jumps to the first / last option or moves by a viewport |
| Type a character | Opens the popup and filters the options |
| ArrowDown / ArrowUp | Moves the highlight between options |
| Enter | Toggles the highlighted option (on "Select all": selects or clears every option) |
| Escape | Closes the popup, keeping focus in the input |
| Backspace | On an empty input, removes the last chip |
| Enter on “Create …” / “Retry” | Creates and selects an option from the typed text / loads the options again after an error |
Tokens: background, border, popover, popover-foreground, primary, primary-text, secondary, muted-foreground, destructive, focus-ring
Usage:
```tsx
import { useI18n } from '@gntik-ai/ui';
import { matchText } from '@gntik-ai/ui';
import { useCallback, useState } from 'react';
import { MultiSelect, type MultiSelectOption } from '@gntik-ai/ui';
const TOPICS: MultiSelectOption[] = [
{ value: 'billing', label: 'billing', description: '128 issues' },
{ value: 'onboarding', label: 'onboarding', description: '42 issues' },
{ value: 'performance', label: 'performance', description: '77 issues' },
{ value: 'search', label: 'search', description: '19 issues' },
{ value: 'security', label: 'security', description: '35 issues' },
];
/** Stands in for `fetch('/api/topics?q=…', { signal })`. */
function searchTopics(
query: string,
{ signal }: { signal: AbortSignal },
locale?: string,
): Promise {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve(TOPICS.filter((t) => matchText(t.label, query, { locale }))), 250);
signal.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
export default function MultiSelectAsync() {
const { locale } = useI18n();
const loadOptions = useCallback(
(query: string, context: { signal: AbortSignal }) => searchTopics(query, context, locale),
[locale],
);
const [value, setValue] = useState([]);
return (
{
const created = { value: input.toLowerCase(), label: input.toLowerCase(), description: 'New topic' };
TOPICS.push(created);
return created;
}}
/>
);
}
```
### NumberInput (`number-input`)
Numeric input with − / + steppers, a unit suffix, min/max/step clamping, locale formatting and an optional scrub label (drag to change). Sizes sm and md like Input; invalid, disabled and read-only states; works inside Field.
- Package: `@gntik-ai/ui` · export `NumberInput` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/NumberInput/NumberInput.doc.ts
- Primitive: `@base-ui/react/number-field`
- ARIA pattern: spinbutton-like textbox (number field)
- Registry dependencies: ui-utils
- Install: `gntik-ui add number-input`; own the source with `gntik-ui eject number-input`
Keyboard:
| Key | Behaviour |
| --- | --- |
| ArrowUp / ArrowDown | Increments / decrements by step (Shift: largeStep, Alt: smallStep) |
| PageUp / PageDown | Increments / decrements by largeStep |
| Home / End | Sets the value to min / max (when defined) |
| Click on label | Inside a Field, focuses the input |
Tokens: background, border, foreground, muted-foreground, primary, destructive, secondary, focus-ring
Usage:
```tsx
import { Field, FieldDescription, FieldLabel } from '@gntik-ai/ui';
import { NumberInput } from '@gntik-ai/ui';
export default function NumberInputBasic() {
return (
Replicas
Between 1 and 20 instances per region.
Storage (GB)
Steps of 10 GB; PageUp / PageDown jump by 100.
Monthly budget
);
}
```
### OtpInput (`otp-input`)
One-time / verification code input: 4–8 character boxes, numeric or alphanumeric, optional group separator. Typing auto-advances, paste fills every slot, onComplete fires when full; invalid and disabled states; works inside Field.
- Package: `@gntik-ai/ui` · export `OtpInput` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/OtpInput/OtpInput.doc.ts
- Primitive: `@base-ui/react/otp-field`
- ARIA pattern: group of single-character textboxes
- Registry dependencies: ui-utils
- Install: `gntik-ui add otp-input`; own the source with `gntik-ui eject otp-input`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Type a character | Fills the slot and moves to the next one |
| Backspace | Clears the slot, or the previous one when empty, and moves back |
| ArrowLeft / ArrowRight | Moves between slots |
| Paste (⌘V / Ctrl+V) | Fills the slots from the clipboard, dropping invalid characters |
Tokens: background, border, foreground, muted-foreground, primary, destructive, secondary, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Field, FieldDescription, FieldError, FieldLabel } from '@gntik-ai/ui';
import { OtpInput } from '@gntik-ai/ui';
/** Verification flow: "123456" is the valid demo code. */
export default function OtpInputBasic() {
const [status, setStatus] = useState<'idle' | 'ok' | 'error'>('idle');
return (
Verification code
setStatus('idle')}
onComplete={(code) => setStatus(code === '123456' ? 'ok' : 'error')}
/>
{status === 'ok' ? (
Code verified. Your device is trusted.
) : (
Enter the 6-digit code we sent to your authenticator app.
)}
That code is not valid. Try again.
Invite code
Letters and numbers, from the workspace invitation email.
);
}
```
### PasswordInput (`password-input`)
Password field on Input with a reveal toggle (aria-pressed) and an optional 4-segment strength meter. Strength comes from a small built-in heuristic or a `strength` prop (0–4); its text is linked to the input with aria-describedby. Works inside Field.
- Package: `@gntik-ai/ui` · export `PasswordInput` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/PasswordInput/PasswordInput.doc.ts
- Primitive: `@base-ui/react/input`
- ARIA pattern: textbox + toggle button
- Registry dependencies: button, input, ui-utils
- Install: `gntik-ui add password-input`; own the source with `gntik-ui eject password-input`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves from the input to the reveal toggle |
| Enter / Space | On the toggle: shows or hides the password (aria-pressed) |
Tokens: background, border, foreground, muted-foreground, secondary, destructive, destructive-text, warning, warning-text, success, success-text, focus-ring
Usage:
```tsx
import { Lock } from '@gntik-ai/icons';
import { Field, FieldLabel } from '@gntik-ai/ui';
import { PasswordInput } from '@gntik-ai/ui';
export default function PasswordInputBasic() {
return (
);
}
```
### PeriodPicker (`period-picker`)
Whole ISO weeks, months, calendar quarters or years, single or range. PeriodPicker is a field and Base UI popover; PeriodCalendar is inline. Single values are the first day of a whole period at local midnight, not a single-day selection. Ranges include the last day of the final period. Weeks always run Monday–Sunday and use the ISO week-year, regardless of locale. Month names use Intl; quarter/week labels and presets use the en/es i18n catalogs. Form values: YYYY-MM, YYYY-Qn, YYYY or YYYY-Www; ranges submit -start and -end. Defaults are this and last complete period; presets=[] hides them. Min/max compare period overlap; disabled periods remain focusable. Range fields apply on the second pick; Escape discards an unfinished range. Calendar and DatePicker remain day-only.
- Package: `@gntik-ai/ui` · export `PeriodPicker` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/PeriodPicker/PeriodPicker.doc.ts
- Primitive: `@base-ui/react/popover + internal Calendar period grids`
- ARIA pattern: combobox with a dialog popup; inline grid with roving tabindex
- Registry dependencies: calendar, date-picker, popover, ui-utils
- Install: `gntik-ui add period-picker`; own the source with `gntik-ui eject period-picker`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space (field) | Opens the popup and focuses the selected period or today |
| Tab / Shift+Tab | One tab stop inside each grid; other controls and presets remain in the tab order |
| ArrowLeft / ArrowRight | Moves one cell, crossing year/page boundaries; mirrored in RTL |
| ArrowUp / ArrowDown | Moves one row: three months/years, four quarters, or seven days |
| Home / End | Moves to the first/last cell of the row (ISO Monday/Sunday for weeks) |
| PageUp / PageDown | Months/quarters: same period in the previous/next year; years: 12-year page; weeks: month (Shift: year) |
| Enter / Space (grid) | Selects the period (range: start then end); aria-disabled cells cannot be selected |
| Enter / Space (title) | Drills up: weeks → months → years; picking a coarser cell returns with focus restored |
| Enter / Space (previous/next) | Moves one visible page: month for weeks, year for months/quarters, 12 years for years |
| Escape | Closes the popup and returns focus to the field without applying an unfinished range |
Tokens: background, foreground, muted-foreground, border, primary, primary-foreground, secondary, popover, focus-ring
Usage:
```tsx
import { useI18n } from '@gntik-ai/ui';
import { Field, FieldDescription, FieldLabel } from '@gntik-ai/ui';
import { PeriodPicker } from '@gntik-ai/ui';
export default function BillingMonth() {
const { t } = useI18n();
return (
{t('periodPicker.billingMonth')}
{t('periodPicker.wholePeriodDescription')}
);
}
```
### PermissionMatrix (`permission-matrix`)
Roles (columns) × permissions (rows grouped in sections) editor. Cells are tri-state — allowed, denied, inherited — shown with an icon and a word, never colour alone; inherited cells are dashed and can say what they resolve to. Read-only columns (e.g. owner) and per-cell disabled reasons in a tooltip; controlled `value` + `onChange`; one tab stop with grid arrow-key navigation.
- Package: `@gntik-ai/ui` · export `PermissionMatrix` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/PermissionMatrix/PermissionMatrix.doc.ts
- Primitive: `native table (role="grid") + @base-ui/react/tooltip`
- ARIA pattern: data grid (roving tabindex)
- Registry dependencies: tooltip, ui-utils
- Install: `gntik-ui add permission-matrix`; own the source with `gntik-ui eject permission-matrix`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Enters and leaves the grid (one tab stop, on the last focused cell) |
| ArrowLeft / ArrowRight / ArrowUp / ArrowDown | Moves between cells (left/right mirror in RTL); section headings are skipped |
| Home / End | First / last cell of the row |
| Ctrl+Home / Ctrl+End | First / last cell of the grid |
| PageUp / PageDown | First / last cell of the column |
| Enter / Space | Cycles the cell: allowed → denied → inherited |
| A / D / I | Sets the cell to allowed / denied / inherited |
Tokens: card, border, secondary, foreground, muted-foreground, success, success-chip-text, destructive, destructive-chip-text, focus-ring
Usage:
```tsx
import { useState } from 'react';
import {
PermissionMatrix,
type PermissionMatrixValue,
type PermissionRole,
type PermissionSection,
} from '@gntik-ai/ui';
const roles: PermissionRole[] = [
{ id: 'owner', label: 'Owner', readOnly: true, readOnlyReason: 'Owners always have every permission.' },
{ id: 'admin', label: 'Admin' },
{ id: 'member', label: 'Member' },
{ id: 'viewer', label: 'Viewer', description: 'Inherits from Member' },
];
const sections: PermissionSection[] = [
{
id: 'projects',
label: 'Projects',
permissions: [
{ id: 'projects.read', label: 'View projects' },
{ id: 'projects.write', label: 'Edit projects', description: 'Rename, archive and change settings' },
{ id: 'projects.delete', label: 'Delete projects' },
],
},
{
id: 'billing',
label: 'Billing',
permissions: [
{ id: 'billing.read', label: 'View invoices' },
{ id: 'billing.manage', label: 'Manage payment methods' },
],
},
];
const all = (state: 'allowed' | 'denied') =>
Object.fromEntries(sections.flatMap((s) => s.permissions.map((p) => [p.id, state])));
const initial: PermissionMatrixValue = {
owner: all('allowed'),
admin: {
'projects.read': 'allowed',
'projects.write': 'allowed',
'projects.delete': 'allowed',
'billing.read': 'allowed',
'billing.manage': 'denied',
},
member: {
'projects.read': 'allowed',
'projects.write': 'allowed',
'projects.delete': 'denied',
'billing.read': 'denied',
'billing.manage': 'denied',
},
viewer: { 'projects.write': 'denied' },
};
export default function PermissionMatrixBasic() {
const [value, setValue] = useState(initial);
return (
roleId === 'viewer' ? (value.member?.[permissionId] === 'denied' ? 'denied' : 'allowed') : undefined
}
getDisabledReason={(roleId, permissionId) =>
roleId === 'member' && permissionId === 'billing.manage' ? 'Only admins can manage payment methods.' : undefined
}
/>
);
}
```
### PowerSearch (`power-search`)
Structured filter input: pick a field, an operator and a value from suggestions (or type `status=failed`, `cost>100`), add free text, and edit committed terms from the keyboard. Emits a typed query AST (`{ type: "and", terms }`) with parse / format / match helpers; fields, operators and values are props.
- Package: `@gntik-ai/ui` · export `PowerSearch` · group Forms · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/PowerSearch/PowerSearch.doc.ts
- Primitive: `@base-ui/react/combobox`
- ARIA pattern: combobox + list of editable filter terms
- Registry dependencies: select, ui-utils
- Install: `gntik-ui add power-search`; own the source with `gntik-ui eject power-search`
Keyboard:
| Key | Behaviour |
| --- | --- |
| ArrowDown / ArrowUp | Opens the suggestions and moves the highlight |
| Enter | Picks the highlighted suggestion; otherwise commits the typed value or free text (empty input: submit) |
| field=value / field:value | Typed shortcut: jumps straight to the value step |
| Backspace (empty input) | Steps back: operator → field → edits the last term |
| ArrowLeft (input start) | Moves focus to the last term |
| ArrowLeft / ArrowRight / Home / End (on a term) | Moves between terms; past the last returns to the input |
| Enter / Space (on a term) | Pulls the term back into the input to edit it |
| Delete / Backspace (on a term) | Removes the term |
| Escape | Closes the suggestions; again clears the term being built |
Tokens: background, border, foreground, muted-foreground, primary, primary-text, secondary, popover, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { PowerSearch } from '@gntik-ai/ui';
import { formatPowerSearch, matchPowerSearch, parsePowerSearch, type PowerSearchQuery } from '@gntik-ai/ui';
import { deploymentFields, deployments, type DeploymentField } from '@gntik-ai/ui';
export default function PowerSearchDeployments() {
const [query, setQuery] = useState>(() =>
parsePowerSearch('status=failed', deploymentFields),
);
const rows = deployments.filter((d) => matchPowerSearch(query, d));
return (
{formatPowerSearch(query) || 'No filters'}
{rows.map((d) => (
{d.id}
{d.project}
{d.region}
{d.status}
))}
{rows.length === 0 && No deployments match. }
);
}
```
### QueryBuilder (`query-builder`)
Visual filter editor: nested AND/OR groups of field / operator / value conditions. Typed fields (text, number, enum, date, boolean) offer their operators and value editor (Input, NumberInput, Select / MultiSelect, DatePicker, ranges for "between"); add and remove conditions and groups up to `maxDepth`. The value is a serialisable tree; `queryToString(tree)` makes it human-readable and `evaluateQuery(tree, record)` tests a record. Inverted numeric or date "between" ranges show an inline error and are ignored by evaluation. Before sending a query to a backend, check each condition with `isConditionComplete` and `isConditionValid`; validation preserves the entered values.
- Package: `@gntik-ai/ui` · export `QueryBuilder` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/QueryBuilder/QueryBuilder.doc.ts
- Primitive: `Select, MultiSelect, Input, NumberInput, DatePicker, ToggleGroup, Button`
- ARIA pattern: nested groups of form controls
- Registry dependencies: button, calendar, date-picker, input, multi-select, number-input, select, toggle-group, ui-utils
- Install: `gntik-ui add query-builder`; own the source with `gntik-ui eject query-builder`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Moves through each condition's field, operator, value and remove button, then the group's buttons |
| Enter / Space | On Add condition / Add group: adds it and focuses its first field; on a remove button: removes it and focuses Add condition |
| ArrowLeft / ArrowRight | In a group's AND / OR switch: moves between the options (Enter / Space picks) |
| Enter / Space / ArrowDown | On a field, operator or enum value: opens its list (see Select) |
Tokens: card, secondary, border, foreground, muted-foreground, primary, focus-ring, destructive-text
Usage:
```tsx
import { useState } from 'react';
import { QueryBuilder } from '@gntik-ai/ui';
import { deploymentFields, deploymentQuery } from '@gntik-ai/ui';
export default function QueryBuilderBasic() {
const [query, setQuery] = useState(deploymentQuery);
return (
);
}
```
### RadioGroup (`radio-group`)
Single choice from a short list. RadioGroup + Radio with optional label, description and trailing content per option, in plain, list (bordered rows) and card variants.
- Package: `@gntik-ai/ui` · export `RadioGroup` · group Forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/RadioGroup/RadioGroup.doc.ts
- Primitive: `@base-ui/react/radio-group, @base-ui/react/radio`
- ARIA pattern: radio group
- Registry dependencies: ui-utils
- Install: `gntik-ui add radio-group`; own the source with `gntik-ui eject radio-group`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Enters the group once, on the selected radio (or the first one); Tab again leaves the group |
| Arrow Down / Arrow Right | Moves to the next radio and selects it (wraps) |
| Arrow Up / Arrow Left | Moves to the previous radio and selects it (wraps) |
| Space | Selects the focused radio if it is not already selected |
Tokens: background, border, primary, primary-foreground, foreground, muted-foreground, destructive, focus-ring
Usage:
```tsx
import { Fieldset, FieldsetLegend } from '@gntik-ai/ui';
import { Radio, RadioGroup } from '@gntik-ai/ui';
const PLANS = [
{ value: 'shared', label: 'Shared', description: 'Shared CPU · cold starts', price: 'Free' },
{ value: 'dedicated', label: 'Dedicated', description: '2 vCPU · always warm', price: '$80/mo' },
{
value: 'isolated',
label: 'Isolated',
description: 'Private network · no neighbours',
price: '$240/mo',
disabled: true,
},
];
export default function RadioGroupCards() {
return (
}>
Deployment plan
{PLANS.map((p) => (
))}
);
}
```
### ScheduleInput (`schedule-input`)
Cron schedule editor: presets (hourly at minute, daily at, weekly on days at, monthly on day at) that write a 5-field cron expression, plus a raw cron field (*, lists, ranges, steps, names, macros) with validation and an error message, a plain-language summary ("Every weekday at 09:00") and the next runs in a time zone. Pure helpers: parseCron, describeCron, nextRuns. Display timeZone defaults to I18nProvider (system when unset); use the same zone on server and client. The TimeZone example shows the provider default and UTC overrides
- Package: `@gntik-ai/ui` · export `ScheduleInput` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/ScheduleInput/ScheduleInput.doc.ts
- Primitive: `@base-ui/react/toggle-group + Field, Input, NumberInput, TimePicker`
- ARIA pattern: group of form controls (segmented control, textbox, spinbuttons)
- Registry dependencies: calendar, field, input, number-input, time-picker, toggle-group, ui-utils
- Install: `gntik-ui add schedule-input`; own the source with `gntik-ui eject schedule-input`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Moves between the frequency tabs, the preset fields and the cron field |
| ArrowLeft / ArrowRight | In the frequency or weekday group: moves between options |
| Enter / Space | Picks the focused frequency, or toggles the focused weekday |
| ArrowUp / ArrowDown | In the minute, day or time fields: steps the value |
| Type in the cron field | Re-validates, updates the summary and the next runs, and selects the preset that fits |
Tokens: background, border, card, foreground, muted-foreground, primary, secondary, destructive-text, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { ScheduleInput } from '@gntik-ai/ui';
const now = new Date(Date.UTC(2026, 5, 15, 7, 30));
export default function ScheduleInputBasic() {
const [cron, setCron] = useState('0 9 * * 1-5');
return (
);
}
```
### SecretField (`secret-field`)
A secret value (API key, token, connection string) on Input: masked by default (fixed-length mask, optional visible prefix/suffix), a reveal toggle (aria-pressed), a CopyButton that copies the real value and announces it, an optional Rotate action behind an AlertDialog confirmation, and optional created / last-used meta. Optional showLabel, hideLabel, copyLabel and copiedAnnouncement override i18n defaults. Read-only by default (`editable` turns it into a password field). While masked and read-only the secret is not in the DOM, and it is never put in a title or tooltip. `maskSecret` is exported too.
- Package: `@gntik-ai/ui` · export `SecretField` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/SecretField/SecretField.doc.ts
- Primitive: `@base-ui/react/input`
- ARIA pattern: read-only textbox + toggle button + button
- Registry dependencies: alert-dialog, button, copy-button, input, live-announcer, timestamp, ui-utils
- Install: `gntik-ui add secret-field`; own the source with `gntik-ui eject secret-field`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves through the field, the reveal toggle, Copy and Rotate |
| Enter / Space | On the reveal toggle: shows or hides the value (aria-pressed) |
| Enter / Space | On Copy: copies the secret and announces "Secret copied to clipboard" |
| Enter / Space | On Rotate: opens the confirmation (focus moves into it) |
| Escape | Closes the confirmation without rotating; focus returns to Rotate |
Tokens: background, border, secondary, foreground, muted-foreground, primary-text, warning, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Field, FieldDescription, FieldLabel } from '@gntik-ai/ui';
import { SecretField } from '@gntik-ai/ui';
const DAY = 24 * 3600 * 1000;
export default function SecretFieldBasic() {
const [now] = useState(() => Date.now());
return (
API key
Webhook signing secret
Verify the signature header with this value.
);
}
```
### Select (`select`)
Pick one value from a short list in a popup, with SimpleSelect ref and triggerRef aliases for its trigger button. Compound parts (Select, SelectLabel, SelectTrigger, SelectContent, SelectItem, SelectGroup, SelectGroupLabel, SelectSeparator) plus a one-line SimpleSelect. Three trigger sizes; rich rows (avatar, status dot) via item children. SimpleSelect can load its options on first open (`loadOptions(query, { signal })`) with announced loading, empty and error states. Creatable lives in Combobox / MultiSelect (a select has no text input). SimpleSelect `clearable` adds a sibling reset button; Delete or Backspace on its trigger clears to null and retains focus. `onClear` runs after the value change; `clearLabel` overrides the localized name. For long lists, use Combobox with virtualized and a flat items array (see ComboboxVirtualized); Select requires its options to stay mounted for typeahead.
- Package: `@gntik-ai/ui` · export `Select` · group Forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Select/Select.doc.ts
- Primitive: `@base-ui/react/select`
- ARIA pattern: select-only combobox + listbox
- Registry dependencies: combobox, spinner, ui-utils
- Install: `gntik-ui add select`; own the source with `gntik-ui eject select`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Delete / Backspace | With clearable on a non-empty editable trigger: clears and keeps focus |
| Enter / Space / ArrowDown | On the trigger: opens the popup |
| ArrowDown / ArrowUp | Moves the highlight between options |
| Enter | Selects the highlighted option and closes the popup |
| Escape | Closes the popup and returns focus to the trigger |
| Type a character | Typeahead: highlights the next option starting with it |
| Escape, then Enter / ArrowDown | After a failed async load: closing and reopening the popup retries |
Tokens: background, border, popover, popover-foreground, primary, primary-text, secondary, muted-foreground, ring, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { SimpleSelect, type SimpleSelectItem } from '@gntik-ai/ui';
/** Stands in for `fetch('/api/clusters', { signal })`: the list loads the first time the popup opens. */
function loadClusters(_query: string, { signal }: { signal: AbortSignal }): Promise {
return new Promise((resolve, reject) => {
const timer = setTimeout(
() =>
resolve([
{ value: 'prod-eu', label: 'prod-eu · 12 nodes' },
{ value: 'prod-us', label: 'prod-us · 9 nodes' },
{ value: 'staging', label: 'staging · 3 nodes' },
]),
400,
);
signal.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
export default function SelectAsync() {
const [cluster, setCluster] = useState('prod-eu');
return (
);
}
```
### Slider (`slider`)
Pick a number or a range on a track. Single or range thumbs (one per value), optional visible label and live value readout, two sizes, works inside Field and Form.
- Package: `@gntik-ai/ui` · export `Slider` · group Forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Slider/Slider.doc.ts
- Primitive: `@base-ui/react/slider`
- ARIA pattern: slider (multi-thumb for ranges)
- Registry dependencies: ui-utils
- Install: `gntik-ui add slider`; own the source with `gntik-ui eject slider`
Keyboard:
| Key | Behaviour |
| --- | --- |
| ArrowRight / ArrowUp | Increases the focused thumb by one step |
| ArrowLeft / ArrowDown | Decreases the focused thumb by one step |
| PageUp / PageDown | Increases / decreases by the large step |
| Home / End | Moves the focused thumb to the minimum / maximum |
| Tab | Moves focus between thumbs |
Tokens: primary, secondary, background, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { Slider } from '@gntik-ai/ui';
export default function SliderRange() {
return (
);
}
```
### Switch (`switch`)
On/off setting that applies immediately. Three sizes, optional inline label, works inside Field and Form.
- Package: `@gntik-ai/ui` · export `Switch` · group Forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Switch/Switch.doc.ts
- Primitive: `@base-ui/react/switch`
- ARIA pattern: switch
- Registry dependencies: ui-utils
- Install: `gntik-ui add switch`; own the source with `gntik-ui eject switch`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Space | Toggles the switch |
| Enter | Toggles the switch |
| Tab | Moves focus to and from the switch |
Tokens: primary, secondary, background, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Switch } from '@gntik-ai/ui';
export default function SwitchSettings() {
const [alerts, setAlerts] = useState(true);
return (
);
}
```
### Textarea (`textarea`)
Multi-line text input in two sizes, with optional CSS autosize (field-sizing: content) and vertical resize. Renders through Base UI Field.Control, so it works inside Field. Opt-in `clearable` reserves space for a reset button and returns focus to the textarea. `onClear` runs after the value change; `clearLabel` overrides the localized name.
- Package: `@gntik-ai/ui` · export `Textarea` · group Forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Textarea/Textarea.doc.ts
- Primitive: `@base-ui/react/field (Field.Control)`
- ARIA pattern: textbox (multi-line)
- Registry dependencies: ui-utils
- Install: `gntik-ui add textarea`; own the source with `gntik-ui eject textarea`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves focus into and out of the textarea |
| Enter | Inserts a new line (does not submit the form) |
Tokens: background, border, foreground, muted-foreground, primary, destructive, secondary, focus-ring
Usage:
```tsx
import { Info } from '@gntik-ai/icons';
import { Field, FieldDescription, FieldLabel } from '@gntik-ai/ui';
import { Textarea } from '@gntik-ai/ui';
export default function TextareaAutosize() {
return (
Project description
Grows with its content.
Versioned on every save; you can revert from the history.
);
}
```
### TimePicker (`time-picker`)
Accessible time field with a ref to its hours segment: hour and minute spinbutton segments, optional seconds, a 12-hour clock with AM/PM by locale or prop, minute step, min/max validation, controlled or uncontrolled (`"HH:mm"` strings). Digits type and auto-advance; arrows spin. Works inside Field; DatePicker `withTime` and DateTimeRangePicker reuse it. Opt-in `clearable` resets every segment to its placeholder and focuses hours. `onClear` runs after emitting null; `clearLabel` overrides the localized button name.
- Package: `@gntik-ai/ui` · export `TimePicker` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/TimePicker/TimePicker.doc.ts
- Primitive: `@base-ui/react/field (control) + spinbutton segments`
- ARIA pattern: group of spinbuttons (time field)
- Registry dependencies: ui-utils
- Install: `gntik-ui add time-picker`; own the source with `gntik-ui eject time-picker`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Moves between segments, then out of the field |
| ArrowUp / ArrowDown | Increments / decrements the focused segment (minutes by `step`), wrapping around |
| Home / End | Sets the segment to its minimum / maximum |
| 0–9 | Types into the segment; moves to the next one when it is full |
| A / P | On the AM/PM segment: picks AM or PM |
| ArrowLeft / ArrowRight | Moves to the previous / next segment |
| Backspace / Delete | Clears the segment (the value becomes null) |
Tokens: background, border, foreground, muted-foreground, primary, primary-chip-text, secondary, destructive, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Field, FieldDescription, FieldLabel } from '@gntik-ai/ui';
import { TimePicker } from '@gntik-ai/ui';
export default function TimePickerBasic() {
const [start, setStart] = useState('09:00');
return (
Maintenance window starts
Arrow keys move in 15-minute steps.
Reminder
Timeout at
Locked time
);
}
```
### TokenInput (`token-input`)
Free-entry chips for emails, labels or hosts: Enter, Tab or a separator commits, pasted lists are split, `validate` marks bad entries as invalid chips, `maxItems` caps the list. Additions and removals are announced; works inside Field.
- Package: `@gntik-ai/ui` · export `TokenInput` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/TokenInput/TokenInput.doc.ts
- Primitive: `@base-ui/react/field (Field.Control)`
- ARIA pattern: textbox + list of removable entries
- Registry dependencies: ui-utils
- Install: `gntik-ui add token-input`; own the source with `gntik-ui eject token-input`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Tab | Commits the typed text as a chip (Tab only when there is text) |
| , / ; | Commits the typed text (configurable separators) |
| Backspace | On an empty input, removes the last chip |
| Paste | Splits the clipboard on separators and new lines into chips |
| Tab to a chip button, Enter | Removes that chip |
Tokens: background, border, foreground, muted-foreground, primary, primary-text, destructive, destructive-text, secondary, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Field, FieldDescription, FieldLabel } from '@gntik-ai/ui';
import { TokenInput } from '@gntik-ai/ui';
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export default function TokenInputEmails() {
const [emails, setEmails] = useState(['ana@example.com', 'devops@example', 'li.wei@example.com']);
const invalid = emails.filter((e) => !EMAIL.test(e)).length;
return (
0}>
Invite members
raw.trim().toLowerCase()}
validate={(e) => (EMAIL.test(e) ? null : 'Not a valid email address')}
maxItems={10}
placeholder="name@company.com"
name="invites"
listLabel="Invitees"
/>
{invalid > 0
? `Fix ${invalid} invalid address${invalid > 1 ? 'es' : ''} before sending.`
: 'Press Enter or comma to add. Paste a list to add several.'}
);
}
```
### TransferList (`transfer-list`)
Two lists, available and selected, for picking and ordering a subset (columns, scopes, members): search each side, check several items, move them with the middle buttons, Enter or a double click, and reorder the selected side. Moves are announced. Opt-in virtualized windows both panes with shared row measurement, keeps the active option mounted and preserves full-data bulk actions and keyboard reordering. Find-in-page and screen-reader browse mode reach rendered rows only. Built-in defaults follow I18nProvider (en/es); labels overrides win per key. The catalog defaults example demonstrates the shared component labels, including overlays, lists, navigation and file controls.
- Package: `@gntik-ai/ui` · export `TransferList` · group Forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/TransferList/TransferList.doc.ts
- ARIA pattern: two multi-select listboxes (aria-activedescendant) + action buttons
- Registry dependencies: button, input, ui-utils, virtual-list
- Install: `gntik-ui add transfer-list`; own the source with `gntik-ui eject transfer-list`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves between search fields, the two lists and the action buttons |
| ArrowDown / ArrowUp, Home / End | Moves the active option in the focused list |
| Space | Checks or unchecks the active option |
| Shift+ArrowDown / Shift+ArrowUp | Moves and checks, extending the selection |
| Ctrl/Cmd+A | Checks (or unchecks) every visible option |
| Enter | Moves the checked options (or the active one) to the other list |
| Alt+ArrowUp / Alt+ArrowDown | Selected list: moves the checked options (or the active one) up / down |
Tokens: card, border, background, foreground, muted-foreground, primary, primary-foreground, secondary, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { I18nProvider } from '@gntik-ai/ui';
import { BottomSheet, BottomSheetContent, BottomSheetTrigger } from '@gntik-ai/ui';
import { Button } from '@gntik-ai/ui';
import { Carousel } from '@gntik-ai/ui';
import { JsonViewer } from '@gntik-ai/ui';
import { Lightbox } from '@gntik-ai/ui';
import { ListInput } from '@gntik-ai/ui';
import { MegaMenu } from '@gntik-ai/ui';
import { MultiSelect } from '@gntik-ai/ui';
import { OverflowList } from '@gntik-ai/ui';
import { Thumbnail, ThumbnailList } from '@gntik-ai/ui';
import { Timer } from '@gntik-ai/ui';
import { Tour } from '@gntik-ai/ui';
import { TreeList } from '@gntik-ai/ui';
import { VirtualList } from '@gntik-ai/ui';
import { TransferList } from '@gntik-ai/ui';
const items = [
{ value: 'report', label: 'Report' },
{ value: 'notes', label: 'Notes' },
];
const preview =
'data:image/svg+xml,' +
encodeURIComponent(
' ',
);
/** Caller content stays English; built-in labels follow the Spanish provider. */
export default function TransferListCatalogDefaults() {
const [tourOpen, setTourOpen] = useState(false);
const [mediaOpen, setMediaOpen] = useState(false);
const [files, setFiles] = useState(['report.pdf', 'upload.csv', 'retry.pdf']);
const [retrying, setRetrying] = useState(false);
return (
Spanish defaults, English caller content. Labels and function overrides still take precedence.
Promise.reject(new Error('Offline example'))}
/>
Report
Notes
null} height={80} />
item} />
{files.map((name) => (
setFiles((current) => current.filter((file) => file !== name))}
onRetry={name === 'retry.pdf' ? () => setRetrying(true) : undefined}
/>
))}
{
setFiles(['report.pdf', 'upload.csv', 'retry.pdf']);
setRetrying(false);
}}
>
Reset files
setTourOpen(true)}>Start tour
setMediaOpen(true)}>View media
}>Open sheet
Quarterly report
);
}
```
### AspectRatio (`aspect-ratio`)
A box locked to a width / height ratio (16:9 by default) for images, video and embeds; direct children fill it (object-cover). Radius none–xl and an optional bordered placeholder surface.
- Package: `@gntik-ai/ui` · export `AspectRatio` · group Layout · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/AspectRatio/AspectRatio.doc.ts
- Primitive: `@base-ui/react/use-render`
- Registry dependencies: ui-utils
- Install: `gntik-ui add aspect-ratio`; own the source with `gntik-ui eject aspect-ratio`
Tokens: border, secondary, muted-foreground, radius-lg
Usage:
```tsx
import { ImageIcon } from '@gntik-ai/icons';
import { AspectRatio } from '@gntik-ai/ui';
export default function AspectRatioMedia() {
return (
Release preview · 16:9
1:1
Project avatar
);
}
```
### Card (`card`)
The base content surface: border, token radius, generous padding and a flat shadow. Parts: Card, CardHeader (optionally divided), CardTitle, CardDescription, CardAction (header right slot), CardBody and CardFooter. Variants: default, well and outline.
- Package: `@gntik-ai/ui` · export `Card` · group Layout · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Card/Card.doc.ts
- Registry dependencies: ui-utils
- Install: `gntik-ui add card`; own the source with `gntik-ui eject card`
Tokens: card, card-foreground, border, secondary, muted-foreground, shadow-sm, radius-xl
Usage:
```tsx
import { Card, CardBody, CardDescription, CardHeader, CardTitle } from '@gntik-ai/ui';
const ROWS = [
['Plan', 'Team'],
['Seats', '12 of 20'],
['Renews', 'Nov 1, 2026'],
] as const;
export default function CardVariants() {
return (
Billing
Current plan and usage.
{ROWS.map(([k, v]) => (
{k}
{v}
))}
This region does not accept new deployments until the quota is reviewed.
);
}
```
### ChatDemo (`chat-demo`)
Reference composition of the chat pack: suggestions in the empty state, attachments (pick, paste or drop), a compact ModelPicker in the composer, a tool call, a streamed markdown reply with sources (TypingIndicator, caret), Stop, MessageFeedback (copy, regenerate, thumbs with reasons), and a resizable details panel. Streaming is mocked with timers that are cleaned up.
- Package: `@gntik-ai/chat` · export `ChatDemo` · group Layout · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/ChatDemo/ChatDemo.doc.ts
- Registry dependencies: attachment, chat-composer, chat-layout, chat-message, citation, markdown, message-feedback, model-picker, suggestion-chips, tool-call-card
- Install: `gntik-ui add chat-demo`; own the source with `gntik-ui eject chat-demo`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter | In the composer: sends; on a suggestion: sends it |
| Escape | In the composer while streaming: stops the reply |
Tokens: background, card, border, primary, focus-ring
Usage:
```tsx
import { ChatDemo } from '@gntik-ai/chat';
```
### ChatLayout (`chat-layout`)
Full-height chat frame: scrollable thread with auto-scroll that respects scrolling up, a jump-to-latest button, a composer pinned below and an optional resizable side panel (artifact or details).
- Package: `@gntik-ai/chat` · export `ChatLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/chat/src/ChatLayout/ChatLayout.doc.ts
- ARIA pattern: window splitter
- Install: `gntik-ui add chat-layout`; own the source with `gntik-ui eject chat-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves through the thread, the composer, the resize handle and the panel |
| ArrowLeft | On the resize handle: widens the side panel (Shift for bigger steps) |
| ArrowRight | On the resize handle: narrows the side panel |
| Home | On the resize handle: panel to its maximum width |
| End | On the resize handle: panel to its minimum width |
Tokens: background, foreground, card, border, primary, focus-ring
Usage:
```tsx
import { ChatComposer } from '@gntik-ai/chat';
import { ChatMessage } from '@gntik-ai/chat';
import { ChatLayout } from '@gntik-ai/chat';
export default function ChatLayoutWithPanel() {
return (
Quarterly usage review
}
panel={
Details
Project web-app · 3 members · 12 deployments this week.
}
composer={ {}} hint={null} />}
>
Which project used the most build minutes this quarter?
web-app used 1,240 build minutes, 46% of the total.
);
}
```
### Grid (`grid`)
Responsive CSS grid. `cols` takes 1–12 or a per-breakpoint object ({ base, sm, md, lg, xl }) mapped to static classes; `minChildWidth` auto-fills as many columns as fit. GridItem spans columns (also responsive, or `full`). Gap on the spacing scale.
- Package: `@gntik-ai/ui` · export `Grid` · group Layout · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Grid/Grid.doc.ts
- Primitive: `@base-ui/react/use-render`
- Registry dependencies: stack, ui-utils
- Install: `gntik-ui add grid`; own the source with `gntik-ui eject grid`
Usage:
```tsx
import { Grid } from '@gntik-ai/ui';
const PROJECTS = ['Billing API', 'Search', 'Mobile app', 'Data pipeline', 'Docs site', 'Status page'];
export default function GridAutoFill() {
return (
{PROJECTS.map((p) => (
{p}
))}
);
}
```
### Resizable (`resizable`)
Split layouts with draggable dividers: ResizablePanelGroup (horizontal or vertical), ResizablePanel (default / min / max size in %, collapsible) and ResizeHandle (optional grip). Pointer drag and full keyboard support; the layout can be remembered in localStorage with `autoSaveId`. No extra dependencies.
- Package: `@gntik-ai/ui` · export `Resizable` · group Layout · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Resizable/Resizable.doc.ts
- ARIA pattern: window splitter
- Registry dependencies: ui-utils
- Install: `gntik-ui add resizable`; own the source with `gntik-ui eject resizable`
Keyboard:
| Key | Behaviour |
| --- | --- |
| ArrowLeft / ArrowRight | Horizontal group: shrinks / grows the panel before the handle by `step` (default 5%) |
| ArrowUp / ArrowDown | Vertical group: shrinks / grows the panel above the handle by `step` |
| Home / End | Sets the panel before the handle to its minimum / maximum size |
| Enter | Collapses the adjacent collapsible panel, or restores it to its previous size |
Tokens: border, primary, card, muted-foreground, shadow-sm, focus-ring
Usage:
```tsx
import { ResizablePanelGroup, ResizablePanel, ResizeHandle } from '@gntik-ai/ui';
export default function ResizableSplit() {
return (
);
}
```
### ScrollArea (`scroll-area`)
Native scroll container with thin token-styled scrollbars that fade in on hover and while scrolling. Vertical, horizontal or both; optional bordered surface. With an aria-label the viewport is a named region, and it joins the Tab order whenever it overflows.
- Package: `@gntik-ai/ui` · export `ScrollArea` · group Layout · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/ScrollArea/ScrollArea.doc.ts
- Primitive: `@base-ui/react/scroll-area`
- ARIA pattern: region
- Registry dependencies: ui-utils
- Install: `gntik-ui add scroll-area`; own the source with `gntik-ui eject scroll-area`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Focuses the scrollable viewport (only when its content overflows) |
| ArrowUp / ArrowDown / PageUp / PageDown / Home / End | Scrolls the focused viewport natively |
Tokens: border, muted-foreground, card, focus-ring
Usage:
```tsx
import { ScrollArea } from '@gntik-ai/ui';
const EVENTS = Array.from({ length: 24 }, (_, i) => ({
id: i,
who: ['Ada Lovelace', 'Grace Hopper', 'Alan Turing', 'Katherine Johnson'][i % 4],
what: ['deployed Billing API', 'invited 2 members', 'paid invoice INV-0' + (40 + i), 'rolled back Search'][i % 4],
when: `${i + 2} min ago`,
}));
export default function ScrollAreaActivity() {
return (
{EVENTS.map((e) => (
{e.who} {e.what}
{e.when}
))}
);
}
```
### Section (`section`)
A painted content region (, named by its title). Variants plain / card / muted, padding none–lg, optional dividers between children, and a header slot with title, description and actions.
- Package: `@gntik-ai/ui` · export `Section` · group Layout · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Section/Section.doc.ts
- ARIA pattern: region
- Registry dependencies: ui-utils
- Install: `gntik-ui add section`; own the source with `gntik-ui eject section`
Tokens: card, card-foreground, secondary, border, foreground, muted-foreground, shadow-sm, radius-xl
Usage:
```tsx
import { Button } from '@gntik-ai/ui';
import { Switch } from '@gntik-ai/ui';
import { Section } from '@gntik-ai/ui';
const ROWS = [
['Email digests', 'A weekly summary of activity in your projects.'],
['Deployment alerts', 'Notify me when a deployment fails or rolls back.'],
['Invoice reminders', 'Send a reminder three days before an invoice is due.'],
] as const;
export default function SectionSettings() {
return (
Reset
}
className="max-w-[560px]"
>
{ROWS.map(([label, hint]) => (
{label}
{hint}
))}
);
}
```
### Separator (`separator`)
Thin rule between content. Horizontal or vertical, default / subtle / strong weight, and an optional label (plain text, mono kicker or pill) placed at the start, centre or end of a horizontal line.
- Package: `@gntik-ai/ui` · export `Separator` · group Layout · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Separator/Separator.doc.ts
- Primitive: `@base-ui/react/separator`
- ARIA pattern: separator
- Registry dependencies: ui-utils
- Install: `gntik-ui add separator`; own the source with `gntik-ui eject separator`
Tokens: border, secondary, muted-foreground
Usage:
```tsx
import { Separator } from '@gntik-ai/ui';
export default function SeparatorVariants() {
return (
);
}
```
### Stack (`stack`)
Flex layout primitives: Stack (any direction), HStack, VStack and Center. Gap on the spacing scale (0–12), align, justify, wrap and inline; render as any block tag with `as`, or any element with `render`. Paints nothing.
- Package: `@gntik-ai/ui` · export `Stack` · group Layout · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Stack/Stack.doc.ts
- Primitive: `@base-ui/react/use-render`
- Registry dependencies: ui-utils
- Install: `gntik-ui add stack`; own the source with `gntik-ui eject stack`
Usage:
```tsx
import { Inbox } from '@gntik-ai/icons';
import { Center, VStack } from '@gntik-ai/ui';
export default function StackCenter() {
return (
No invoices yet
);
}
```
### VisuallyHidden (`visually-hidden`)
Screen-reader-only content (sr-only), with a `focusable` variant that appears while focused. SkipLink is the canonical use: the first Tab stop on a page, revealed on focus, that moves focus to the main content.
- Package: `@gntik-ai/ui` · export `VisuallyHidden` · group Layout · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/VisuallyHidden/VisuallyHidden.doc.ts
- Primitive: `@base-ui/react/use-render`
- ARIA pattern: skip link
- Registry dependencies: ui-utils
- Install: `gntik-ui add visually-hidden`; own the source with `gntik-ui eject visually-hidden`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Focuses the SkipLink and reveals it (first Tab stop on the page) |
| Enter | Moves focus to the target (e.g. ), not just the scroll position |
Tokens: card, border, foreground, shadow-md, focus-ring
Usage:
```tsx
import { Button } from '@gntik-ai/ui';
import { SkipLink } from '@gntik-ai/ui';
export default function SkipLinkPage() {
return (
Projects
Members
Billing
Press Tab inside this preview: the skip link appears first and jumps straight here.
);
}
```
### Breadcrumbs (`breadcrumbs`)
Location trail in a nav landmark: chevron or slash separators, an optional icon-only root, and the current page marked with aria-current. With `maxItems` the middle collapses into an ellipsis button that expands the full trail in place.
- Package: `@gntik-ai/ui` · export `Breadcrumbs` · group Navigation · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Breadcrumbs/Breadcrumbs.doc.ts
- ARIA pattern: breadcrumb
- Registry dependencies: link, ui-utils
- Install: `gntik-ui add breadcrumbs`; own the source with `gntik-ui eject breadcrumbs`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves focus through the links and the ellipsis button; the current page is not a link |
| Enter | Follows the focused link |
| Enter / Space | On the ellipsis button: reveals the hidden levels and focuses the first one |
Tokens: foreground, muted-foreground, secondary, focus-ring
Usage:
```tsx
import { Home } from '@gntik-ai/icons';
import { Breadcrumbs } from '@gntik-ai/ui';
export default function BreadcrumbsBasic() {
return (
);
}
```
### CursorPagination (`cursor-pagination`)
See the cursor-pager-footer block example for data-table paginated={false} with a consumer-owned token stack. Stateless Previous/Next navigation for consumer-owned cursor stacks, with no page numbers, ranges or invented totals. Supply a plain-text summary (null keeps it empty) or shownCount with required labels.shownSummary and labels.totalSummary; only a positive exact total uses totalSummary. A pageSizeOptions select requires pageSize, onPageSizeChange, labels.rowsPerPage and labels.rowsPerPageLabel. Flat previousLabel, nextLabel and ariaLabel override labels.previous, labels.next and labels.navLabel, then provider catalog defaults. loading and busy are aliases: rendered buttons stay focusable and aria-disabled with aria-busy, while the select stays enabled. loadingLabel defaults to the catalog; null keeps the summary during busy. One persistent polite status writes once per changed text; mount, empty text and unchanged commits do not announce. Removal of the focused control moves focus to Next, Previous, the select or the nav root, in that order, even while busy; focus elsewhere never moves.
- Package: `@gntik-ai/ui` · export `CursorPagination` · group Navigation · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/CursorPagination/CursorPagination.doc.ts
- Primitive: `@base-ui/react/button + @base-ui/react/select (kit Button and SimpleSelect)`
- ARIA pattern: named navigation landmark with one persistent polite atomic status
- Registry dependencies: button, select, ui-utils
- Install: `gntik-ui add cursor-pagination`; own the source with `gntik-ui eject cursor-pagination`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Moves through the select (when present), Previous and Next, including aria-disabled buttons |
| Enter / Space | Activates an available button once; busy and unavailable buttons do nothing; opens the select |
| ArrowDown / ArrowUp | Opens the select and moves between page-size options |
| Enter / Escape | Selects the highlighted page size or closes the select; focus returns to its trigger |
Tokens: card, border, foreground, muted-foreground, secondary, focus-ring, background, primary, ring, popover, popover-foreground, primary-text
Usage:
```tsx
import { CursorPagination } from '@gntik-ai/ui';
export default function Busy() {
return (
{}} onNext={() => {}} />
);
}
```
### Link (`link`)
Styled anchor in brand green, underlined by default so inline links do not rely on colour (WCAG 1.4.1); standalone links (nav, cards, actions) pass underline="hover" or "none". `external` adds the external icon, a hidden "opens in a new tab" hint and safe rel/target. LinkProvider supplies the app router link so internal links navigate client-side; `render` swaps the element per link.
- Package: `@gntik-ai/ui` · export `Link` · group Navigation · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Link/Link.doc.ts
- Primitive: `@base-ui/react/use-render`
- ARIA pattern: link
- Registry dependencies: ui-utils
- Install: `gntik-ui add link`; own the source with `gntik-ui eject link`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter | Follows the link |
| Tab | Moves focus to and from the link |
Tokens: primary-text, muted-foreground, foreground, focus-ring
Usage:
```tsx
import type { ComponentPropsWithRef } from 'react';
import { Link, LinkProvider } from '@gntik-ai/ui';
/**
* Stand-in for a router's Link (Next.js, React Router, TanStack…). A real adapter maps `href`
* to the router's prop (e.g. `to`) and navigates on the client.
*/
function RouterLink(props: ComponentPropsWithRef<'a'>) {
return ;
}
export default function LinkRouter() {
return (
Projects
New project
Status page
);
}
```
### MegaMenu (`mega-menu`)
Top navigation with wide panels: items with sections open a panel of link columns with descriptions and optional featured content; plain items are links. Hover or click opens, the panel resizes between items, router links come from LinkProvider and the current page is marked. Use it as the StackedLayout sub-nav row; `megaMenuNavItems` feeds MobileNav. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `MegaMenu` · group Navigation · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/MegaMenu/MegaMenu.doc.ts
- Primitive: `@base-ui/react/navigation-menu`
- ARIA pattern: disclosure navigation (nav landmark, triggers with aria-expanded)
- Registry dependencies: link, nav-list, ui-utils
- Install: `gntik-ui add mega-menu`; own the source with `gntik-ui eject mega-menu`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves between top-level items, then into an open panel |
| Enter / Space / ArrowDown | Opens the panel of the focused trigger |
| ArrowRight / ArrowLeft | Moves between top-level items |
| Escape | Closes the panel and returns focus to its trigger |
Tokens: popover, popover-foreground, border, accent, accent-foreground, secondary, primary, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { MegaMenu } from '@gntik-ai/ui';
import { megaMenuItems } from '@gntik-ai/ui';
export default function MegaMenuBasic() {
const [href, setHref] = useState('#deployments');
return (
{
if (link.href) setHref(link.href);
}}
/>
Current page: {href}
);
}
```
### MobileNav (`mobile-nav`)
Small-screen navigation: a menu icon button opens a Drawer sheet (chrome surface) with the NavList groups and an optional footer slot. Activating an item closes the sheet; focus is trapped while open and returns to the button.
- Package: `@gntik-ai/ui` · export `MobileNav` · group Navigation · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/MobileNav/MobileNav.doc.ts
- Primitive: `@base-ui/react/drawer`
- ARIA pattern: modal dialog containing a navigation landmark
- Registry dependencies: button, drawer, nav-list, ui-utils
- Install: `gntik-ui add mobile-nav`; own the source with `gntik-ui eject mobile-nav`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space | On the menu button: opens the sheet and moves focus inside |
| Tab / Shift+Tab | Cycles through the sheet (focus is trapped) |
| Enter | Follows the focused link and closes the sheet |
| Escape | Closes the sheet and returns focus to the menu button |
Tokens: chrome, popover, border, accent, accent-foreground, primary, muted-foreground, focus-ring
Usage:
```tsx
import { BarChart3, CreditCard, FolderKanban, Home, Rocket, Settings, Users } from '@gntik-ai/icons';
import { useState } from 'react';
import type { NavGroup } from '@gntik-ai/ui';
import { MobileNav } from '@gntik-ai/ui';
const GROUPS: NavGroup[] = [
{
label: 'Workspace',
items: [
{ label: 'Overview', href: '#overview', icon: Home },
{ label: 'Projects', href: '#projects', icon: FolderKanban, badge: 12 },
{ label: 'Deployments', href: '#deployments', icon: Rocket },
{ label: 'Analytics', href: '#analytics', icon: BarChart3 },
],
},
{
label: 'Admin',
items: [
{ label: 'Members', href: '#members', icon: Users },
{ label: 'Billing', href: '#billing', icon: CreditCard },
{ label: 'Settings', href: '#settings', icon: Settings },
],
},
];
export default function MobileNavBasic() {
const [current, setCurrent] = useState('#projects');
return (
item.href && setCurrent(item.href)}
/>
Acme Cloud
{current.slice(1)}
);
}
```
### NavList (`nav-list`)
Vertical navigation for sidebars and settings pages: labelled (optionally foldable) groups of items with icon, label and badge, nested collapsible sub-items, and aria-current="page" on the active item with the brand accent bar. `collapsed` turns it into an icon rail with tooltips (nested sections open as flyout menus). Links render through LinkProvider, so client routers work.
- Package: `@gntik-ai/ui` · export `NavList` · group Navigation · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/NavList/NavList.doc.ts
- Primitive: `@base-ui/react/collapsible`
- ARIA pattern: navigation landmark with disclosure sections
- Registry dependencies: link, menu, tooltip, ui-utils
- Install: `gntik-ui add nav-list`; own the source with `gntik-ui eject nav-list`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Moves through items, group toggles and section toggles in order |
| Enter | Follows the focused link (or runs the item button) |
| Enter / Space | On a group heading or nested section: expands or collapses it |
| Enter / Space (rail) | Opens a nested section as a flyout menu (arrow keys move, Escape closes) |
Tokens: chrome, accent, accent-foreground, primary, primary-text, secondary, border, muted-foreground, foreground, focus-ring
Usage:
```tsx
import { Bell, Building2, CreditCard, Plug, Shield, User } from '@gntik-ai/icons';
import { useState } from 'react';
import { NavList, type NavGroup } from '@gntik-ai/ui';
export default function NavListSettings() {
const [active, setActive] = useState('members');
const item = (id: string, label: string, extra: Partial = {}) => ({
id,
label,
current: active === id,
onClick: () => setActive(id),
...extra,
});
const groups: NavGroup[] = [
{
items: [
item('profile', 'Profile', { icon: User }),
item('notifications', 'Notifications', { icon: Bell, badge: 4 }),
item('security', 'Security', { icon: Shield }),
],
},
{
label: 'Organization',
items: [
{
label: 'Workspace',
icon: Building2,
items: [item('general', 'General'), item('members', 'Members'), item('roles', 'Roles')],
},
{ label: 'Billing', icon: CreditCard, items: [item('plan', 'Plan'), item('invoices', 'Invoices')] },
item('integrations', 'Integrations', { icon: Plug, disabled: true }),
],
},
];
return ;
}
```
### Pagination (`pagination`)
Moves through pages of results. Pagination is numbered (first, last, siblings of the current page and ellipses, constant width) and page-size agnostic: page, pageCount, onPageChange. PaginationCompact is the table-footer form: "1–10 of 97" with previous/next.
- Package: `@gntik-ai/ui` · export `Pagination` · group Navigation · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Pagination/Pagination.doc.ts
- ARIA pattern: navigation landmark with aria-current
- Registry dependencies: ui-utils
- Install: `gntik-ui add pagination`; own the source with `gntik-ui eject pagination`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Moves through every enabled control (previous, each page, next) |
| Enter / Space | Activates the focused control and changes page |
Tokens: primary, primary-foreground, card, border, ring, secondary, muted-foreground, foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Pagination } from '@gntik-ai/ui';
export default function PaginationNumbered() {
const [page, setPage] = useState(6);
return ;
}
```
### Stepper (`stepper`)
Wizard progress as an ordered list of steps (complete · current · upcoming · error), horizontal or vertical, with optional descriptions. Completed steps can be made clickable to go back; `compact` collapses it to "Step 2 of 5" with a thin bar. The current step carries aria-current="step".
- Package: `@gntik-ai/ui` · export `Stepper` · group Navigation · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Stepper/Stepper.doc.ts
- ARIA pattern: ordered list in a labelled navigation landmark
- Registry dependencies: ui-utils
- Install: `gntik-ui add stepper`; own the source with `gntik-ui eject stepper`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves between clickable (completed) steps; other steps are not focusable |
| Enter / Space | Activates the focused completed step (onStepClick) |
Tokens: primary, primary-foreground, primary-text, secondary, border, destructive, destructive-foreground, destructive-text, muted-foreground, focus-ring
Usage:
```tsx
import { Stepper } from '@gntik-ai/ui';
const STEPS = [
{ label: 'Account' },
{ label: 'Workspace' },
{ label: 'Invite members' },
{ label: 'Billing' },
{ label: 'Done' },
];
export default function StepperCompact() {
return (
);
}
```
### Tabs (`tabs`)
Switches between views of the same block without leaving the page. Two variants (underline, pills), optional icons and count badges, a sliding indicator and a full-width layout. Compound parts: Tabs, TabsList, TabsTab, TabsIndicator, TabsPanel.
- Package: `@gntik-ai/ui` · export `Tabs` · group Navigation · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Tabs/Tabs.doc.ts
- Primitive: `@base-ui/react/tabs`
- ARIA pattern: tabs
- Registry dependencies: ui-utils
- Install: `gntik-ui add tabs`; own the source with `gntik-ui eject tabs`
Keyboard:
| Key | Behaviour |
| --- | --- |
| ArrowRight / ArrowLeft | Moves focus to the next / previous tab (wraps around) |
| Home / End | Moves focus to the first / last tab |
| Enter / Space | Activates the focused tab and shows its panel |
| Tab | Moves focus from the active tab into its panel |
Tokens: primary, primary-text, border, secondary, card, muted-foreground, foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Tabs, TabsList, TabsPanel, TabsTab } from '@gntik-ai/ui';
const RANGES = [
['day', 'Day'],
['week', 'Week'],
['month', 'Month'],
['quarter', 'Quarter'],
] as const;
export default function TabsPills() {
const [range, setRange] = useState('week');
return (
setRange(String(v))}>
{RANGES.map(([value, label]) => (
{label}
))}
{RANGES.map(([value, label]) => (
Billing usage for the selected {label.toLowerCase()}.
))}
Summary
Cost
Latency
Errors
Summary of the billing period.
Cost by project.
p50 and p95 latency.
Error rate by deployment.
);
}
```
### TreeList (`tree-list`)
Hierarchical list on the WAI-ARIA tree pattern (role tree / treeitem / group, aria-expanded, aria-level): roving focus, arrow keys, Home/End and typeahead, single or multiple selection, icons per node and children loaded on first expand with loading and error states. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `TreeList` · group Navigation · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/TreeList/TreeList.doc.ts
- ARIA pattern: tree view
- Registry dependencies: spinner, ui-utils
- Install: `gntik-ui add tree-list`; own the source with `gntik-ui eject tree-list`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves focus into the tree (selected or first node) and out of it |
| ArrowDown / ArrowUp | Next / previous visible node |
| ArrowRight | Closed node: expands it (loading children if needed). Open node: moves to its first child |
| ArrowLeft | Open node: collapses it. Otherwise: moves to the parent |
| Home / End | First / last visible node |
| Enter | Selects the node and runs onAction |
| Space | Selects the node (toggles in multiple mode) |
| Ctrl/⌘ + A | Multiple mode: selects every node |
| Type a character | Moves to the next node whose label starts with the typed text |
Tokens: foreground, muted-foreground, secondary, primary, primary-chip-text, primary-foreground, border, destructive-text, focus-ring
Usage:
```tsx
import { Cloud, FileCode, FileText, Folder, FolderOpen } from '@gntik-ai/icons';
import { useState } from 'react';
import { TreeList } from '@gntik-ai/ui';
import type { TreeNode } from '@gntik-ai/ui';
const folder = { icon: Folder, openIcon: FolderOpen };
const NODES: TreeNode[] = [
{
id: 'src',
label: 'src',
...folder,
children: [
{
id: 'components',
label: 'components',
...folder,
children: [
{ id: 'button', label: 'Button.tsx', icon: FileCode, meta: '2 KB' },
{ id: 'dialog', label: 'Dialog.tsx', icon: FileCode, meta: '5 KB' },
],
},
{ id: 'index', label: 'index.ts', icon: FileCode, meta: '1 KB' },
],
},
{ id: 'remote', label: 'remote-assets', icon: Cloud, hasChildren: true },
{ id: 'docs', label: 'docs', ...folder, children: [{ id: 'guide', label: 'getting-started.md', icon: FileText }] },
{ id: 'readme', label: 'README.md', icon: FileText, meta: '3 KB' },
];
/** Simulated fetch for a lazily loaded folder. */
function loadRemote(): Promise {
return new Promise((resolve) =>
setTimeout(
() =>
resolve([
{ id: 'logo', label: 'logo.svg', icon: FileText },
{ id: 'fonts', label: 'fonts.css', icon: FileCode },
]),
600,
),
);
}
export default function TreeListFiles() {
const [opened, setOpened] = useState('');
return (
setOpened(node.label)}
className="rounded-lg border border-border bg-card p-1.5"
/>
{opened && `Opened ${opened}`}
);
}
```
### UserMenu (`user-menu`)
Account menu for topbars and sidebar footers: avatar trigger (optionally with the name), or showEmail for a full-width name/email trigger in a sidebar footer. Disable showEmail in a collapsed rail. Opens a name/email header, data-driven items (buttons or router links), a Theme submenu bound to useTheme(), and a destructive Sign out.
- Package: `@gntik-ai/ui` · export `UserMenu` · group Navigation · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/UserMenu/UserMenu.doc.ts
- Primitive: `@base-ui/react/menu`
- ARIA pattern: menu button
- Registry dependencies: avatar, link, menu, theme, theme-switcher, ui-utils
- Install: `gntik-ui add user-menu`; own the source with `gntik-ui eject user-menu`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space / ArrowDown | On the avatar: opens the menu and focuses the first item |
| ArrowDown / ArrowUp | Moves between items (wraps) |
| ArrowRight / ArrowLeft | Opens / closes the Theme submenu |
| Enter | Runs the highlighted item (or selects a theme) and closes the menu |
| Escape | Closes the menu and returns focus to the avatar |
Tokens: popover, border, primary, primary-foreground, accent, accent-foreground, secondary, foreground, muted-foreground, destructive-text, focus-ring
Usage:
```tsx
import { CreditCard, Settings, User } from '@gntik-ai/icons';
import { useState } from 'react';
import { ThemeProvider } from '@gntik-ai/ui';
import { UserMenu } from '@gntik-ai/ui';
export default function UserMenuAccount() {
const [last, setLast] = useState('');
return (
// Apps mount ThemeProvider once at the root; it is local here so the example stands alone.
setLast('Profile') },
{ label: 'Settings', icon: Settings, shortcut: '⌘,', onSelect: () => setLast('Settings') },
{ label: 'Billing', icon: CreditCard, href: '#billing' },
]}
onSignOut={() => setLast('Signed out')}
/>
{last}
);
}
```
### WorkspaceSwitcher (`workspace-switcher`)
Two-line workspace picker (logo, name, plan/role meta) for the topbar or sidebar header. Short lists open a menu with the current workspace checked; long lists (or `searchable`) open a combobox with a search field. An optional "Create workspace" action closes the list.
- Package: `@gntik-ai/ui` · export `WorkspaceSwitcher` · group Navigation · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/WorkspaceSwitcher/WorkspaceSwitcher.doc.ts
- Primitive: `@base-ui/react/menu · @base-ui/react/combobox`
- ARIA pattern: menu button (short lists) / select-only combobox with search (long lists)
- Registry dependencies: avatar, menu, select, ui-utils
- Install: `gntik-ui add workspace-switcher`; own the source with `gntik-ui eject workspace-switcher`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space / ArrowDown | On the trigger: opens the list |
| ArrowDown / ArrowUp | Moves between workspaces and the create action |
| Typing (searchable) | Filters workspaces by name or meta |
| Enter | Switches to the highlighted workspace (or runs Create workspace) and closes |
| Escape | Closes the list and returns focus to the trigger |
Tokens: card, border, secondary, popover, primary, primary-foreground, primary-text, accent, accent-foreground, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { WorkspaceSwitcher, type Workspace } from '@gntik-ai/ui';
const WORKSPACES: Workspace[] = [
{ id: 'acme', name: 'Acme Industries', meta: 'Enterprise · Owner' },
{ id: 'design', name: 'Design Team', meta: 'Pro · Admin' },
{ id: 'platform', name: 'Platform', meta: 'Pro · Member' },
{ id: 'personal', name: 'Personal', meta: 'Free · Owner' },
];
export default function WorkspaceSwitcherMenu() {
const [current, setCurrent] = useState('acme');
const [log, setLog] = useState('');
return (
{
setCurrent(w.id);
setLog(`Switched to ${w.name}`);
}}
onCreate={() => setLog('Create workspace')}
/>
{log}
);
}
```
### AlertDialog (`alert-dialog`)
Confirmation that interrupts the user and demands a decision: tone icon (destructive, warning, info), title, consequence and Cancel + confirm actions. An outside press does not dismiss it. Parts: AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction.
- Package: `@gntik-ai/ui` · export `AlertDialog` · group Overlays · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/AlertDialog/AlertDialog.doc.ts
- Primitive: `@base-ui/react/alert-dialog`
- ARIA pattern: alertdialog
- Registry dependencies: button, dialog, ui-utils
- Install: `gntik-ui add alert-dialog`; own the source with `gntik-ui eject alert-dialog`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space | On the trigger: opens the dialog and moves focus inside |
| Tab / Shift+Tab | Cycles focus between the actions (focus is trapped) |
| Escape | Closes the dialog and returns focus to the trigger |
Tokens: popover, popover-foreground, border, background, muted-foreground, destructive, destructive-text, warning, warning-text, info, focus-ring
Usage:
```tsx
import { Trash2 } from '@gntik-ai/icons';
import { useState } from 'react';
import { Button } from '@gntik-ai/ui';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@gntik-ai/ui';
export default function AlertDialogDelete() {
const [deleted, setDeleted] = useState(false);
return (
}>Delete project
Delete “support-triage”?
Its deployments, logs and metrics will be removed for every member. This action cannot be undone.
Cancel
setDeleted(true)}>
Delete
{deleted ? 'Project deleted' : ''}
);
}
```
### BottomSheet (`bottom-sheet`)
Mobile sheet that rests at snap points (a peek height and expanded by default): drag or swipe it between heights or down to close, or use the handle from the keyboard. Modal like a dialog: focus is trapped, Escape closes and focus returns to the trigger. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `BottomSheet` · group Overlays · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/BottomSheet/BottomSheet.doc.ts
- Primitive: `@base-ui/react/drawer (snapPoints)`
- ARIA pattern: modal dialog with a resize handle button
- Registry dependencies: ui-utils
- Install: `gntik-ui add bottom-sheet`; own the source with `gntik-ui eject bottom-sheet`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space (trigger) | Opens the sheet at its first snap point and moves focus inside |
| ArrowUp / ArrowDown (handle) | Next taller / shorter snap point |
| Home / End (handle) | Smallest / tallest snap point |
| Enter / Space (handle) | Cycles through the snap points |
| Tab / Shift+Tab | Moves focus within the sheet (trapped) |
| Escape | Closes the sheet and returns focus to the trigger |
Tokens: popover, popover-foreground, background, border, muted-foreground, secondary, focus-ring
Usage:
```tsx
import { Button } from '@gntik-ai/ui';
import { BottomSheet, BottomSheetContent, BottomSheetTrigger } from '@gntik-ai/ui';
const rows = [
['Invoice', 'INV-2041'],
['Period', 'Sep 1 – Sep 30'],
['Projects', '12'],
['Compute', '$1,284.00'],
['Storage', '$212.40'],
['Total', '$1,496.40'],
];
export default function BottomSheetDetails() {
return (
}>Invoice details
{rows.map(([k, v]) => (
{k}
{v}
))}
);
}
```
### CommandPalette (`command-palette`)
⌘K / Ctrl+K command palette: a modal dialog with a search input over grouped commands (icon, label, description, shortcut keys). Commands are data with an onSelect. Fuzzy-ish ranking (prefix, word start, substring, subsequence; keywords and descriptions count), recent items while the query is empty, an empty state and footer hints. useCommandPaletteShortcut() exposes the shortcut on its own.
- Package: `@gntik-ai/ui` · export `CommandPalette` · group Overlays · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/CommandPalette/CommandPalette.doc.ts
- Primitive: `@base-ui/react/dialog + @base-ui/react/autocomplete (inline)`
- ARIA pattern: modal dialog with a combobox and listbox
- Registry dependencies: kbd, ui-utils
- Install: `gntik-ui add command-palette`; own the source with `gntik-ui eject command-palette`
Keyboard:
| Key | Behaviour |
| --- | --- |
| ⌘K / Ctrl+K | Opens the palette (and closes it when open) |
| Typing | Filters and ranks the commands |
| ArrowDown / ArrowUp | Moves the highlight between commands across groups |
| Enter | Runs the highlighted command and closes the palette |
| Escape | Closes the palette and returns focus |
Tokens: popover, popover-foreground, background, border, accent, accent-foreground, primary-text, muted-foreground, card, ring, focus-ring
Usage:
```tsx
import {
BarChart3,
CreditCard,
Download,
FolderKanban,
FolderPlus,
Home,
Rocket,
Settings,
UserPlus,
Users,
} from '@gntik-ai/icons';
import { useState } from 'react';
import { CommandPalette, CommandPaletteTrigger } from '@gntik-ai/ui';
import type { CommandGroup, CommandItem } from '@gntik-ai/ui';
export default function CommandPaletteBasic() {
const [last, setLast] = useState('');
const go = (label: string) => () => setLast(label);
const groups: CommandGroup[] = [
{
label: 'Actions',
items: [
{
id: 'new-project',
label: 'Create project',
icon: FolderPlus,
shortcut: ['⌘', 'N'],
keywords: ['new'],
onSelect: go('Create project'),
},
{
id: 'deploy',
label: 'Deploy to production',
icon: Rocket,
shortcut: ['⌘', 'D'],
description: 'Promote the latest build of the current project',
onSelect: go('Deploy to production'),
},
{
id: 'invite',
label: 'Invite member',
icon: UserPlus,
keywords: ['team', 'people'],
onSelect: go('Invite member'),
},
{ id: 'export', label: 'Export usage (CSV)', icon: Download, onSelect: go('Export usage (CSV)') },
],
},
{
label: 'Go to',
items: [
{ id: 'overview', label: 'Overview', icon: Home, shortcut: ['G', 'O'], onSelect: go('Overview') },
{ id: 'projects', label: 'Projects', icon: FolderKanban, shortcut: ['G', 'P'], onSelect: go('Projects') },
{ id: 'analytics', label: 'Analytics', icon: BarChart3, onSelect: go('Analytics') },
{ id: 'members', label: 'Members', icon: Users, onSelect: go('Members') },
{ id: 'billing', label: 'Billing', icon: CreditCard, keywords: ['invoices', 'plan'], onSelect: go('Billing') },
{ id: 'settings', label: 'Settings', icon: Settings, shortcut: ['⌘', ','], onSelect: go('Settings') },
],
},
];
const recent: CommandItem[] = [
{
id: 'web-frontend',
label: 'web-frontend',
icon: FolderKanban,
mono: true,
description: 'Project',
onSelect: go('web-frontend'),
},
{
id: 'billing-api',
label: 'billing-api',
icon: FolderKanban,
mono: true,
description: 'Project',
onSelect: go('billing-api'),
},
];
return (
{last ? (
<>
Last command: {last}
>
) : (
'Press ⌘K (Ctrl+K) or the button to open'
)}
);
}
```
### ContextMenu (`context-menu`)
Menu that opens at the pointer on right click or long press, and from the keyboard with Shift+F10 or the ContextMenu key on the focused area. Uses the Menu item parts (icons, shortcuts, destructive tone, checkbox/radio items, groups, submenus).
- Package: `@gntik-ai/ui` · export `ContextMenu` · group Overlays · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/ContextMenu/ContextMenu.doc.ts
- Primitive: `@base-ui/react/context-menu`
- ARIA pattern: menu
- Registry dependencies: menu, ui-utils
- Install: `gntik-ui add context-menu`; own the source with `gntik-ui eject context-menu`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Shift+F10 / ContextMenu | On the focused area: opens the menu |
| ArrowDown / ArrowUp | Moves between items (wraps) |
| Enter | Activates the highlighted item and closes the menu |
| ArrowRight | Opens the submenu of a submenu trigger; ArrowLeft closes it |
| Escape | Closes the menu |
Tokens: popover, popover-foreground, border, secondary, muted-foreground, destructive, destructive-text, focus-ring
Usage:
```tsx
import { Copy, ExternalLink, FolderInput, Pencil, Trash2 } from '@gntik-ai/icons';
import { useState } from 'react';
import { MenuItem, MenuSeparator, MenuSub, MenuSubContent, MenuSubTrigger } from '@gntik-ai/ui';
import { ContextMenu, ContextMenuContent, ContextMenuTrigger } from '@gntik-ai/ui';
export default function ContextMenuCard() {
const [last, setLast] = useState('');
return (
billing-api
Right click or long press
setLast('Opened billing-api')}>
Open
setLast('Renaming billing-api')}>
Rename
setLast('Duplicated billing-api')}>
Duplicate
Move to
setLast('Moved to Platform')}>Platform
setLast('Moved to Archive')}>Archive
setLast('Deleted billing-api')}>
Delete project
{last}
);
}
```
### Dialog (`dialog`)
Modal window for focused tasks: forms, details, confirmations. Compound parts (Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose) in four sizes.
- Package: `@gntik-ai/ui` · export `Dialog` · group Overlays · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Dialog/Dialog.doc.ts
- Primitive: `@base-ui/react/dialog`
- ARIA pattern: dialog (modal)
- Registry dependencies: ui-utils
- Install: `gntik-ui add dialog`; own the source with `gntik-ui eject dialog`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space | On the trigger: opens the dialog and moves focus inside |
| Tab / Shift+Tab | Cycles focus within the dialog (focus is trapped) |
| Escape | Closes the dialog and returns focus to the trigger |
Tokens: popover, popover-foreground, border, background, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Button } from '@gntik-ai/ui';
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@gntik-ai/ui';
export default function DialogForm() {
const [open, setOpen] = useState(false);
const [name, setName] = useState('support-triage');
return (
}>New project
);
}
```
### Drawer (`drawer`)
Panel that slides in from the right, left or bottom edge for details, edit forms and navigation. Fixed header and footer, scrollable body, swipe to dismiss. Under RTL (I18nProvider) `right` and `left` mirror to the inline end and start. Parts: Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerBody, DrawerFooter, DrawerClose.
- Package: `@gntik-ai/ui` · export `Drawer` · group Overlays · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Drawer/Drawer.doc.ts
- Primitive: `@base-ui/react/drawer`
- ARIA pattern: dialog (modal)
- Registry dependencies: ui-utils
- Install: `gntik-ui add drawer`; own the source with `gntik-ui eject drawer`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space | On the trigger: opens the drawer and moves focus inside |
| Tab / Shift+Tab | Cycles focus within the drawer (focus is trapped) |
| Escape | Closes the drawer and returns focus to the trigger |
Tokens: popover, popover-foreground, border, background, muted-foreground, secondary, focus-ring
Usage:
```tsx
import { Copy, Share2 } from '@gntik-ai/icons';
import { Button } from '@gntik-ai/ui';
import {
Drawer,
DrawerBody,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@gntik-ai/ui';
export default function DrawerBottomSheet() {
return (
}>Share project
Share project
Anyone with the link and a workspace seat can view it.
https://example.com/p/support-triage
Copy link
}>Done
);
}
```
### HoverCard (`hover-card`)
Rich preview of a link destination — a member, project or resource — shown on hover or keyboard focus after a short delay. Parts: HoverCard, HoverCardTrigger (a real link), HoverCardContent. The preview is supplementary: never put the only copy of an action or fact inside it.
- Package: `@gntik-ai/ui` · export `HoverCard` · group Overlays · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/HoverCard/HoverCard.doc.ts
- Primitive: `@base-ui/react/preview-card`
- ARIA pattern: link + non-modal preview
- Registry dependencies: popover, ui-utils
- Install: `gntik-ui add hover-card`; own the source with `gntik-ui eject hover-card`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Focusing the link opens the preview after the delay |
| Escape | Closes the preview; focus stays on the link |
| Enter | Follows the link |
Tokens: popover, popover-foreground, border, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { Avatar } from '@gntik-ai/ui';
import { StatusDot } from '@gntik-ai/ui';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@gntik-ai/ui';
export default function HoverCardMember() {
return (
Last deployed by{' '}
Maria Ruiz
Maria Ruiz
Staff engineer · Platform team
Projects
12
Deployments this week
34
{' '}
3 minutes ago.
);
}
```
### InfoTip (`info-tip`)
Inline "i" help button that opens a small popover with an explanation, next to a form label, a stat or a table header. Named "More information about {label}"; works with click, keyboard and (optionally) hover, so the help is never hover-only.
- Package: `@gntik-ai/ui` · export `InfoTip` · group Overlays · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/InfoTip/InfoTip.doc.ts
- Primitive: `@base-ui/react/popover`
- ARIA pattern: disclosure (non-modal dialog)
- Registry dependencies: popover, ui-utils
- Install: `gntik-ui add info-tip`; own the source with `gntik-ui eject info-tip`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Focuses the info button |
| Enter | Opens the help popover (Space too) |
| Escape | Closes the popover and returns focus to the button |
Tokens: muted-foreground, foreground, popover, popover-foreground, border, focus-ring
Usage:
```tsx
import { InfoTip } from '@gntik-ai/ui';
export default function InfoTipLabel() {
return (
Log retention
Logs older than this are deleted every night. Longer retention counts towards your storage quota.
7 days
30 days
90 days
);
}
```
### Lightbox (`lightbox`)
Full-screen image and video viewer: previous / next, zoom (buttons, keys or double click) with a scrollable stage, captions, a thumbnail strip and an announced position. Modal: focus is trapped, Escape closes and focus returns to the opener. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `Lightbox` · group Overlays · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Lightbox/Lightbox.doc.ts
- Primitive: `@base-ui/react/dialog`
- ARIA pattern: modal dialog with a labelled thumbnail group
- Registry dependencies: ui-utils
- Install: `gntik-ui add lightbox`; own the source with `gntik-ui eject lightbox`
Keyboard:
| Key | Behaviour |
| --- | --- |
| ArrowRight / ArrowLeft | Next / previous item (mirrored in RTL) |
| Home / End | First / last item |
| + / - | Zoom in / out (images) |
| 0 | Reset zoom |
| Tab / Shift+Tab | Moves through zoom, close, previous / next and the thumbnails (trapped) |
| Enter / Space on a thumbnail | Shows that item |
| Escape | Closes the viewer and returns focus |
Tokens: background, chrome, card, border, foreground, muted-foreground, primary, secondary, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Lightbox } from '@gntik-ai/ui';
import { media } from '@gntik-ai/ui';
export default function LightboxGallery() {
const [open, setOpen] = useState(false);
const [index, setIndex] = useState(0);
return (
Release screenshots
{media.map((m, i) => (
{
setIndex(i);
setOpen(true);
}}
>
))}
);
}
```
### Menu (`menu`)
Dropdown list of actions or options: items with icon, shortcut hint and destructive tone, checkbox and radio items, labelled groups, separators and submenus. MoreMenu is the row-actions shorthand (icon button + items array).
- Package: `@gntik-ai/ui` · export `Menu` · group Overlays · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Menu/Menu.doc.ts
- Primitive: `@base-ui/react/menu`
- ARIA pattern: menu button
- Registry dependencies: button, ui-utils
- Install: `gntik-ui add menu`; own the source with `gntik-ui eject menu`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space / ArrowDown | On the trigger: opens the menu and focuses the first item |
| ArrowDown / ArrowUp | Moves between items (wraps) |
| Enter | Activates the highlighted item |
| ArrowRight | Opens the submenu of a submenu trigger; ArrowLeft closes it |
| Escape | Closes the menu and returns focus to the trigger |
Tokens: popover, popover-foreground, border, secondary, muted-foreground, destructive, destructive-text, primary, primary-text, focus-ring
Usage:
```tsx
import { ChevronDown, FolderKanban, LogOut, Settings, Shield, User } from '@gntik-ai/icons';
import { Button } from '@gntik-ai/ui';
import {
Menu,
MenuContent,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuSeparator,
MenuSub,
MenuSubContent,
MenuSubTrigger,
MenuTrigger,
} from '@gntik-ai/ui';
export default function MenuAccount() {
return (
}>Account
Account
Profile
Preferences
Workspace
Switch workspace
Personal
Design team
Platform
Access policies
Sign out
);
}
```
### NotificationsPopover (`notifications-popover`)
Bell icon button with an unread-count badge that opens a popover of notifications: title, body, relative time and a tone dot for unread items, a mark-all-read action, an optional footer (View all) and an empty state.
- Package: `@gntik-ai/ui` · export `NotificationsPopover` · group Overlays · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/NotificationsPopover/NotificationsPopover.doc.ts
- Primitive: `@base-ui/react/popover`
- ARIA pattern: disclosure (non-modal dialog)
- Registry dependencies: button, popover, ui-utils
- Install: `gntik-ui add notifications-popover`; own the source with `gntik-ui eject notifications-popover`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space | On the bell: opens the panel and moves focus into it |
| Tab | Moves through mark-all-read, the notification rows and the footer |
| Enter / Space | On a row: runs onSelect |
| Escape | Closes the panel and returns focus to the bell |
Tokens: popover, border, destructive, destructive-foreground, chrome, primary, primary-text, info, warning, secondary, muted-foreground, focus-ring
Usage:
```tsx
import { NotificationsPopover } from '@gntik-ai/ui';
export default function NotificationsEmpty() {
return ;
}
```
### Popover (`popover`)
Non-modal floating panel anchored to a trigger, for short forms, details and filters. Optional arrow, four sides, three paddings. Parts: Popover, PopoverTrigger, PopoverContent, PopoverTitle, PopoverDescription, PopoverClose.
- Package: `@gntik-ai/ui` · export `Popover` · group Overlays · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Popover/Popover.doc.ts
- Primitive: `@base-ui/react/popover`
- ARIA pattern: dialog (non-modal disclosure)
- Registry dependencies: ui-utils
- Install: `gntik-ui add popover`; own the source with `gntik-ui eject popover`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space | On the trigger: toggles the popover and moves focus inside |
| Tab | Moves through the popover content |
| Escape | Closes the popover and returns focus to the trigger |
Tokens: popover, popover-foreground, border, muted-foreground, secondary, focus-ring
Usage:
```tsx
import { Info } from '@gntik-ai/icons';
import { IconButton } from '@gntik-ai/ui';
import { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger } from '@gntik-ai/ui';
export default function PopoverDetails() {
return (
} />
Usage limits
Each project can run up to 20 deployments a day. Limits reset at midnight UTC and are shared by all members.
);
}
```
### Tooltip (`tooltip`)
Short visual hint that appears on hover or keyboard focus. Solid inverted chip for labels (optional shortcut), rich variant on the popover surface for a few lines. Parts: TooltipProvider, Tooltip, TooltipTrigger, TooltipContent, plus the SimpleTooltip shorthand. Never the only source of essential information.
- Package: `@gntik-ai/ui` · export `Tooltip` · group Overlays · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Tooltip/Tooltip.doc.ts
- Primitive: `@base-ui/react/tooltip`
- ARIA pattern: tooltip
- Registry dependencies: ui-utils
- Install: `gntik-ui add tooltip`; own the source with `gntik-ui eject tooltip`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Focusing the trigger shows the tooltip; moving focus away hides it |
| Escape | Hides the tooltip while focus stays on the trigger |
Tokens: foreground, background, popover, popover-foreground, border
Usage:
```tsx
import { Button } from '@gntik-ai/ui';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@gntik-ai/ui';
export default function TooltipRich() {
return (
}>Seats: 18 of 20
Seats almost full
Two seats left on this plan. Billing admins can add more from Settings.
);
}
```
### Tour (`tour`)
Onboarding steps anchored to elements (selector, ref or getter): a non-modal popover with a step counter and Back / Next / Skip, positioned at each target and outlined with the focus ring (no overlay, no glow). Focus moves into the step and returns when the tour ends; missing targets centre the step. Built-in labels follow I18nProvider (en/es); caller overrides take precedence. See the TransferList catalog defaults example for a live Spanish preview of this component.
- Package: `@gntik-ai/ui` · export `Tour` · group Overlays · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/Tour/Tour.doc.ts
- Primitive: `@base-ui/react/popover`
- ARIA pattern: non-modal dialog (popover) per step
- Registry dependencies: button, popover, ui-utils
- Install: `gntik-ui add tour`; own the source with `gntik-ui eject tour`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Cycles through the close button, Skip, Back and Next (focus stays in the step) |
| Enter / Space | Activates the focused button (Next moves focus to the next step) |
| ArrowRight / ArrowLeft | Next / previous step (mirrored in RTL) |
| Escape | Ends the tour and returns focus |
Tokens: popover, popover-foreground, border, primary, primary-text, muted-foreground, focus-ring
Usage:
```tsx
import { Bell, Plus, Search } from '@gntik-ai/icons';
import { useRef, useState } from 'react';
import { Button, IconButton } from '@gntik-ai/ui';
import { Tour, type TourStep } from '@gntik-ai/ui';
export default function TourOnboarding() {
const [open, setOpen] = useState(false);
const [step, setStep] = useState(0);
const [done, setDone] = useState(null);
const newProject = useRef(null);
const steps: TourStep[] = [
{
target: '#tour-search',
title: 'Find anything',
content: 'Search projects, members and invoices from one place. ⌘K opens it anywhere.',
},
{
target: newProject,
title: 'Create a project',
content: 'Projects group deployments, members and billing. Start with one per product.',
side: 'bottom',
align: 'end',
},
{
target: '#tour-alerts',
title: 'Stay informed',
content: 'Failed deployments and invoice reminders land here.',
side: 'left',
},
];
return (
Search
New project
{
setStep(0);
setOpen(true);
}}
>
Start tour
{done && {done} }
setDone(completed ? 'Tour completed.' : 'Tour skipped.')}
/>
);
}
```
### CSPProvider (`csp-provider`)
CSPProvider passes the app server nonce to Base UI and kit inline elements; useCSP exposes the settings for custom elements. Disable style elements to use static CSS.
- Package: `@gntik-ai/ui` · export `CSPProvider` · group Theme · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/csp/CSPProvider.doc.ts
- Primitive: `@base-ui/react/csp-provider`
- Install: `gntik-ui add csp-provider`; own the source with `gntik-ui eject csp-provider`
Usage:
```tsx
import { useState } from 'react';
import { CSPProvider, useCSP } from '@gntik-ai/ui';
import { useI18n } from '@gntik-ai/ui';
import { Button } from '@gntik-ai/ui';
import { ScrollArea } from '@gntik-ai/ui';
import { SimpleSelect } from '@gntik-ai/ui';
import { Slider } from '@gntik-ai/ui';
import { Tabs, TabsList, TabsTab, TabsIndicator } from '@gntik-ai/ui';
function Preview() {
const { t } = useI18n();
const { disableStyleElements } = useCSP();
return (
{t(disableStyleElements ? 'csp.disabled' : 'csp.enabled')}
{Array.from({ length: 20 }, (_, i) => (
{t('csp.content')} {i + 1}
))}
{t('theme.light')}
{t('theme.dark')}
);
}
export default function CSPProviderPreview() {
const { nonce } = useCSP();
const { t } = useI18n();
const [disabled, setDisabled] = useState(false);
return (
setDisabled(!disabled)}>{t(disabled ? 'csp.enable' : 'csp.disable')}
);
}
```
### ThemeProvider (`theme`)
ThemeProvider (persisted page mode, optional forcedMode), ThemeScope (nested themes and themed portals), themeScript, Logo and brand presets.
- Package: `@gntik-ai/ui` · export `ThemeProvider` · group Theme · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/theme/Logo.tsx
- Registry dependencies: ui-utils
- Install: `gntik-ui add theme`; own the source with `gntik-ui eject theme`
Usage:
```tsx
import { ThemeProvider } from '@gntik-ai/ui';
```
### ThemeSwitcher (`theme-switcher`)
Theme control bound to useTheme(): a segmented Light · Dark · High contrast · System control (labels or icon-only), and ThemeCycleButton, a compact icon button that cycles modes for toolbars.
- Package: `@gntik-ai/ui` · export `ThemeSwitcher` · group Theme · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/components/ThemeSwitcher/ThemeSwitcher.doc.ts
- Primitive: `@base-ui/react/toggle-group`
- ARIA pattern: toggle button group
- Registry dependencies: button, theme, toggle-group, ui-utils
- Install: `gntik-ui add theme-switcher`; own the source with `gntik-ui eject theme-switcher`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Moves focus into the group and out of it |
| ArrowRight / ArrowLeft | Moves focus between options (wraps) |
| Enter / Space | Selects the focused option and applies the theme |
| Enter / Space (cycle button) | Switches to the next mode |
Tokens: card, border, secondary, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { ThemeProvider, useTheme } from '@gntik-ai/ui';
import { ThemeCycleButton } from '@gntik-ai/ui';
function CurrentMode() {
const { mode, resolved } = useTheme();
return (
mode: {mode} · resolved: {resolved}
);
}
export default function ThemeSwitcherCycle() {
return (
);
}
```
### UI utilities (`ui-utils`)
cn() class merging and tv() variants helpers used by every component.
- Package: `@gntik-ai/ui` · export `cn` · group Utilities · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/i18n/I18nProvider.tsx
- Install: `gntik-ui add ui-utils`; own the source with `gntik-ui eject ui-utils`
Usage:
```tsx
import { cn } from '@gntik-ai/ui';
```
## Layouts
### AuthLayout (`auth-layout`)
Sign-in, sign-up and reset shell in three variants: `split` (brand panel on the chrome surface + form; the panel hides below `lg` and the logo moves above the form), `card` (form centred in a card) and `full-bleed` (form straight on the chrome surface). The logo defaults to the ThemeProvider brand preset (Logo with wordmark). The form is first in the DOM, so it is first in tab order on every screen size. Fills its container; `fullScreen` switches to `h-dvh`.
- Package: `@gntik-ai/ui` · export `AuthLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/AuthLayout/AuthLayout.doc.ts
- ARIA pattern: landmarks (main, labelled brand aside) + skip link
- Registry dependencies: theme, ui-utils, visually-hidden
- Install: `gntik-ui add auth-layout`; own the source with `gntik-ui eject auth-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Skip link first, then the form fields in order, then the footer links |
| Enter on the skip link | Moves focus to the form region |
Tokens: background, chrome, card, card-foreground, border, muted-foreground, primary-text, focus-ring
Usage:
```tsx
import { Mail } from '@gntik-ai/icons';
import { useState } from 'react';
import { Button } from '@gntik-ai/ui';
import { Field, FieldDescription, FieldLabel } from '@gntik-ai/ui';
import { Input } from '@gntik-ai/ui';
import { Link } from '@gntik-ai/ui';
import { AuthLayout } from '@gntik-ai/ui';
export default function AuthLayoutCard() {
const [sent, setSent] = useState(false);
return (
Back to sign in}
>
Reset your password
Enter the email on your account and we will send you a reset link.
);
}
```
### CanvasLayout (`canvas-layout`)
Editor shell for a full-bleed canvas (flow editor, code editor, map): top bar · left palette · canvas with a floating toolbar · right inspector · collapsible bottom console. Palette and inspector toggle from the top bar; each panel is controllable (`paletteOpen`, `inspectorOpen`, `consoleOpen`). Below `lg` the side panels and toggles hide and a read-only notice ("Open on a larger screen to edit") shows over the canvas. Fills its container; `fullScreen` switches to `h-dvh`.
- Package: `@gntik-ai/ui` · export `CanvasLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/CanvasLayout/CanvasLayout.doc.ts
- ARIA pattern: landmarks (header, main, two labelled asides, labelled console region) + disclosure buttons
- Registry dependencies: button, ui-utils, visually-hidden
- Install: `gntik-ui add canvas-layout`; own the source with `gntik-ui eject canvas-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Skip link first, then the top bar, the panel toggles, the palette, the canvas, the console and the inspector |
| Enter / Space on a panel toggle | Shows or hides the palette or inspector (aria-expanded reflects the state) |
| Enter / Space on the console bar | Collapses or expands the console (aria-expanded) |
Tokens: background, foreground, chrome, card, border, secondary, muted-foreground, focus-ring
Usage:
```tsx
import { Database, GitBranch, Globe, Maximize, Minus, Play, Plus, Save, Trash2, Webhook } from '@gntik-ai/icons';
import { useState } from 'react';
import { Badge } from '@gntik-ai/ui';
import { Button, IconButton } from '@gntik-ai/ui';
import { Field, FieldLabel } from '@gntik-ai/ui';
import { Input } from '@gntik-ai/ui';
import { Toolbar, ToolbarButton, ToolbarSeparator } from '@gntik-ai/ui';
import { CanvasLayout } from '@gntik-ai/ui';
const PALETTE = [
{ label: 'HTTP trigger', icon: Globe },
{ label: 'Webhook', icon: Webhook },
{ label: 'Branch', icon: GitBranch },
{ label: 'Database query', icon: Database },
];
const NODES = [
{ id: 'trigger', label: 'HTTP trigger', kind: 'Trigger', x: 48, y: 72 },
{ id: 'branch', label: 'Route by plan', kind: 'Branch', x: 260, y: 150 },
{ id: 'query', label: 'Load project', kind: 'Query', x: 480, y: 80 },
];
const LOGS = [
['12:04:31', 'info', 'Validated 3 nodes and 2 edges'],
['12:04:32', 'info', 'Dry run started for deployment “billing-sync”'],
['12:04:33', 'warn', 'Node “Load project” has no timeout; using 30s'],
] as const;
export default function CanvasLayoutFlowEditor() {
const [selected, setSelected] = useState('branch');
const node = NODES.find((n) => n.id === selected);
return (
billing-sync
Draft
Save
Run
>
}
toolbar={
}
palette={
{PALETTE.map(({ label, icon: Icon }) => (
{label}
))}
}
inspector={
node ? (
) : (
Select a node to edit it.
)
}
console={
{LOGS.map(([time, level, text]) => (
{time}
{level}
{text}
))}
}
consoleActions={ }
>
{NODES.map((n) => (
setSelected(n.id)}
style={{ left: n.x, top: n.y }}
className="absolute w-40 rounded-lg border border-border bg-card px-3 py-2 text-start shadow-sm transition-colors aria-pressed:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring motion-reduce:transition-none"
>
{n.kind}
{n.label}
))}
);
}
```
### DocsLayout (`docs-layout`)
Documentation shell: top bar · left nav (a NavList) · article · right table of contents. The `Outline` sub-component is the table of contents: scroll-spy with IntersectionObserver (rooted in the article column, guarded where it is missing) marks the heading in view with aria-current="location", and activating a link moves focus to the heading. Below `lg` the nav hides and the table of contents moves into a Collapsible above the article. Fills its container; `fullScreen` switches to `h-dvh`.
- Package: `@gntik-ai/ui` · export `DocsLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/DocsLayout/DocsLayout.doc.ts
- Primitive: `@base-ui/react/collapsible (small-screen table of contents)`
- ARIA pattern: landmarks (header, nav, main, "On this page" nav) + disclosure
- Registry dependencies: collapsible, ui-utils, visually-hidden
- Install: `gntik-ui add docs-layout`; own the source with `gntik-ui eject docs-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Skip link, top bar, left nav, the article, then the table of contents links |
| Enter on a table of contents link | Scrolls to the heading and moves focus to it |
| Enter / Space on "On this page" | Small screens: expands or collapses the table of contents |
Tokens: background, foreground, chrome, card, border, primary, primary-text, muted-foreground, focus-ring
Usage:
```tsx
import { ArrowLeft, ArrowRight, BookOpen, Rocket, Settings, Shield } from '@gntik-ai/icons';
import { Badge } from '@gntik-ai/ui';
import { Link } from '@gntik-ai/ui';
import { NavList, type NavGroup } from '@gntik-ai/ui';
import { Logo } from '@gntik-ai/ui';
import { DocsLayout } from '@gntik-ai/ui';
const NAV: NavGroup[] = [
{
label: 'Get started',
items: [
{ label: 'Introduction', href: '#docs-intro', icon: BookOpen },
{ label: 'Deploy a project', href: '#docs-deploy', icon: Rocket, current: true },
],
},
{
label: 'Guides',
items: [
{ label: 'Environments', href: '#docs-environments', icon: Settings },
{ label: 'Access control', href: '#docs-access', icon: Shield },
],
},
];
const TOC = [
{ id: 'deploy-prerequisites', label: 'Prerequisites' },
{ id: 'deploy-configure', label: 'Configure the project' },
{ id: 'deploy-variables', label: 'Environment variables', level: 3 as const },
{ id: 'deploy-ship', label: 'Ship it' },
];
const PARAGRAPH = 'text-[14px] leading-7 text-muted-foreground';
const H2 = 'mt-10 mb-3 scroll-mt-6 text-[18px] font-semibold tracking-tight text-foreground';
export default function DocsLayoutGuide() {
return (
Docs
>
}
nav={ }
toc={TOC}
footer={
Introduction
Environments
}
>
Get started
Deploy a project
Go from a repository to a running deployment in a few minutes.
Prerequisites
A workspace where you are an owner or a member with deploy rights, and a repository with a build script.
Pick the region closest to your users. Builds run in the same region, so caches stay warm between deployments.
Environment variables
Secrets are encrypted at rest and only exposed to the build and runtime of the environment they belong to.
Ship it
Push to the default branch. Each push creates a deployment with its own preview URL; promote it when the
checks pass.
);
}
```
### InspectorLayout (`inspector-layout`)
Main area with a pinnable right panel (properties, details, comments). Pinned, the panel docks beside the content; unpinned, it overlays it (flat shadow) and Escape closes it. Open and pinned states are controllable; an InspectorToggle placed in the main area opens and closes it. Below lg the panel opens as a bottom Drawer. Fills its container.
- Package: `@gntik-ai/ui` · export `InspectorLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/InspectorLayout/InspectorLayout.doc.ts
- Primitive: `@base-ui/react/drawer (small screens)`
- ARIA pattern: disclosure (toggle → complementary panel) + toggle button (pin)
- Registry dependencies: button, drawer, ui-utils
- Install: `gntik-ui add inspector-layout`; own the source with `gntik-ui eject inspector-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Enter / Space | On the panel toggle: opens or closes the panel (aria-expanded); an overlaying panel takes focus |
| Enter / Space | On Pin panel: docks or un-docks the panel (aria-pressed) |
| Escape | Closes the unpinned panel and returns focus to the toggle |
| Escape | Small screens: closes the bottom drawer |
Tokens: background, card, border, foreground, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { InspectorLayout, InspectorToggle } from '@gntik-ai/ui';
const SERVICES = ['web-frontend', 'billing-api', 'auth-service', 'search-indexer'];
export default function InspectorLayoutProperties() {
const [selected, setSelected] = useState('billing-api');
return (
Service
{selected}
Region
eu-west
Replicas
3
Owner
Platform team
}
>
Services
{SERVICES.map((name) => (
setSelected(name)}
className="w-full rounded-[10px] border border-border bg-card p-4 text-start font-mono text-[13px] hover:bg-accent/45 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring aria-pressed:border-primary"
>
{name}
))}
);
}
```
### Page (`page`)
The page inside any shell: a header (a custom slot such as the blocks PageHeader, or a simple h1 + description + actions), the body, and an optional sticky action bar (a labelled region) pinned to the bottom of the scroll container. Widths: narrow 640 · default 1120 · wide 1440 · full. It renders no ; the shell owns it.
- Package: `@gntik-ai/ui` · export `Page` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/Page/Page.doc.ts
- ARIA pattern: document structure (h1 + region for the action bar)
- Registry dependencies: ui-utils
- Install: `gntik-ui add page`; own the source with `gntik-ui eject page`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Follows the reading order: header actions, body, then the sticky action bar |
Tokens: card, border, foreground, muted-foreground
Usage:
```tsx
import { Plus } from '@gntik-ai/icons';
import { useState } from 'react';
import { Button } from '@gntik-ai/ui';
import { Page } from '@gntik-ai/ui';
export default function PageSettings() {
const [saved, setSaved] = useState(false);
return (
Add domain
}
actionBar={
<>
{saved ? 'Changes saved' : 'Unsaved changes'}
setSaved(false)}>
Discard
setSaved(true)}>Save changes
>
}
>
{['General', 'Domains', 'Environment variables', 'Danger zone'].map((section) => (
{section}
Settings for {section.toLowerCase()}.
))}
);
}
```
### PrintLayout (`print-layout`)
Printable document frame for invoices, reports and receipts: an A4 or Letter page (`size`) with margins (`margin`), header and footer slots and a `PageBreak` helper. On screen it is a paper sheet (bg-card / text-card-foreground, border, shadow-sm) on a muted backdrop in the current theme; `print:` variants drop the chrome (no backdrop, border, shadow or outer padding) and an `@page` rule sets paper size and margins (`pageRule`). The header and footer print once, at the top and end of the document. The consuming app should print with the light theme (ThemeProvider `defaultMode="light"`, or `setMode('light')` before `window.print()`); no print-only colours are defined.
- Package: `@gntik-ai/ui` · export `PrintLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/PrintLayout/PrintLayout.doc.ts
- ARIA pattern: landmark (main) + labelled article
- Registry dependencies: csp-provider, ui-utils, visually-hidden
- Install: `gntik-ui add print-layout`; own the source with `gntik-ui eject print-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| — | None: a static document (the skip link still moves focus to it) |
Tokens: card, card-foreground, secondary, border, muted-foreground
Usage:
```tsx
import { Badge } from '@gntik-ai/ui';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from '@gntik-ai/ui';
import { Logo } from '@gntik-ai/ui';
import { PageBreak, PrintLayout } from '@gntik-ai/ui';
const LINES = [
{ item: 'Team plan · 12 seats', qty: '12', amount: 240 },
{ item: 'Compute · 1,840 vCPU-hours', qty: '1,840', amount: 92 },
{ item: 'Bandwidth · 420 GB', qty: '420', amount: 21 },
{ item: 'Log retention · 30 days', qty: '1', amount: 15 },
];
const money = (n: number) => `$${n.toFixed(2)}`;
const total = LINES.reduce((sum, l) => sum + l.amount, 0);
export default function PrintLayoutInvoice() {
return (
Example Cloud Ltd · 1 Market Street
billing@example.com
Invoice
INV-2026-0042
Paid
>
}
footer="Thank you for your business. Questions about this invoice: billing@example.com"
>
Billed to
Acme Workspace
Period
1 – 30 Sep 2026
Issued
1 Oct 2026
Invoice line items
Item
Quantity
Amount
{LINES.map((l) => (
{l.item}
{l.qty}
{money(l.amount)}
))}
Total
{money(total)}
Usage detail
Compute is billed per vCPU-hour across all deployments in the workspace, rounded to the nearest hour.
);
}
```
### SettingsLayout (`settings-layout`)
Settings shell: section nav (NavList groups) on the left · a content column with the page title and form sections · a sticky save bar slot (Discard · Save changes). Set saveBar='none' to remove the bar wrapper and its reserved space. Omitted or null slots render no wrapper; empty strings and fragments retain their empty wrapper. The current section is controllable with `value` / `onValueChange`; `href` items navigate as links and still report the change. Below `lg` the nav becomes a Select at the top of the page. Fills its container; `fullScreen` switches to `h-dvh`.
- Package: `@gntik-ai/ui` · export `SettingsLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/SettingsLayout/SettingsLayout.doc.ts
- Primitive: `@base-ui/react/select (small screens)`
- ARIA pattern: landmarks (labelled nav, main) + select-only combobox on small screens
- Registry dependencies: nav-list, select, ui-utils, visually-hidden
- Install: `gntik-ui add settings-layout`; own the source with `gntik-ui eject settings-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Skip link, then the section nav items, then interactive content and the save bar; saveBar='none' has no bar focus stops |
| Enter | Opens the focused section (aria-current="page" moves to it) |
| Enter / Space / ↓ on the section Select | Small screens: opens the list; arrows move, Enter picks a section |
Tokens: background, foreground, card, border, muted-foreground, primary, focus-ring
Usage:
```tsx
import { Bell, CreditCard, KeyRound, User, Users } from '@gntik-ai/icons';
import { useState } from 'react';
import { Button } from '@gntik-ai/ui';
import { Card, CardBody, CardDescription, CardHeader, CardTitle } from '@gntik-ai/ui';
import { Field, FieldDescription, FieldLabel } from '@gntik-ai/ui';
import { Input } from '@gntik-ai/ui';
import type { NavGroup } from '@gntik-ai/ui';
import { Switch } from '@gntik-ai/ui';
import { SettingsLayout } from '@gntik-ai/ui';
const GROUPS: NavGroup[] = [
{
label: 'Account',
items: [
{ id: 'profile', label: 'Profile', icon: User },
{ id: 'notifications', label: 'Notifications', icon: Bell },
{ id: 'security', label: 'Security', icon: KeyRound },
],
},
{
label: 'Workspace',
items: [
{ id: 'members', label: 'Members', icon: Users, badge: 12 },
{ id: 'billing', label: 'Billing', icon: CreditCard },
],
},
];
const TITLES: Record = {
profile: ['Profile', 'How other members see you across the workspace.'],
notifications: ['Notifications', 'Choose what we email you about.'],
security: ['Security', 'Sign-in methods and active sessions.'],
members: ['Members', 'Invite people and manage their roles.'],
billing: ['Billing', 'Plan, payment method and invoices.'],
};
export default function SettingsLayoutWorkspace() {
const [section, setSection] = useState('profile');
const [dirty, setDirty] = useState(false);
const [saved, setSaved] = useState(false);
const [title, description] = TITLES[section] ?? ['Settings', ''];
const edit = () => {
setDirty(true);
setSaved(false);
};
return (
);
}
```
### SidebarLayout (`sidebar-layout`)
Application shell: a collapsible sidebar (expanded, 64px icon rail or off-canvas) with header, body and footer slots, a topbar (workspace switcher, breadcrumb, ⌘K, notifications, theme, user menu) and the main region. Below lg the sidebar moves into a left Drawer opened from the topbar menu button. The collapsed state is controllable and can persist in localStorage (storageKey). Fills its container; fullScreen switches to h-dvh.
- Package: `@gntik-ai/ui` · export `SidebarLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/SidebarLayout/SidebarLayout.doc.ts
- Primitive: `@base-ui/react/drawer`
- ARIA pattern: landmarks (complementary sidebar, banner topbar, main) + skip link + disclosure toggle
- Registry dependencies: button, drawer, ui-utils, visually-hidden
- Install: `gntik-ui add sidebar-layout`; own the source with `gntik-ui eject sidebar-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab (first stop) | Reveals the skip link; Enter moves focus to |
| Enter / Space | On the collapse toggle: collapses or expands the sidebar (aria-expanded reflects it) |
| Enter / Space | On the menu button (small screens): opens the navigation drawer and moves focus inside |
| Escape | Closes the navigation drawer and returns focus to the menu button |
Tokens: chrome, background, foreground, border, card, primary, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { LinkProvider } from '@gntik-ai/ui';
import { NavList } from '@gntik-ai/ui';
import { NavigateContext, DemoRouterLink } from '@gntik-ai/ui';
import { TooltipProvider } from '@gntik-ai/ui';
import { WorkspaceSwitcher } from '@gntik-ai/ui';
import { Logo } from '@gntik-ai/ui';
import { ThemeProvider } from '@gntik-ai/ui';
import { SidebarLayout } from '@gntik-ai/ui';
import { DemoTopbar, NAV_GROUPS, WORKSPACES, pageTitle } from '@gntik-ai/ui';
export default function SidebarLayoutApp() {
const [path, setPath] = useState('/projects');
const [workspace, setWorkspace] = useState('acme');
const title = pageTitle(path);
return (
// Apps mount ThemeProvider / LinkProvider once at the root; they are local so the example stands alone.
collapsed ? (
) : (
setWorkspace(w.id)}
/>
)
}
sidebar={({ collapsed, closeDrawer }) => (
)}
sidebarFooter={({ collapsed }) =>
collapsed ? null : (
Control plane
Operational · 99.98%
)
}
topbar={ }
>
{title}
12 projects · 3 environments · eu-west
{['web-frontend', 'billing-api', 'auth-service', 'search-indexer', 'media-worker', 'reports'].map(
(name) => (
),
)}
);
}
```
### SplitLayout (`split-layout`)
Master–detail layout: a resizable list pane beside a detail pane, built on ResizablePanelGroup (sizes can persist with autoSaveId). Below lg only one pane shows: the list, or the detail with a Back button; `showDetail` is controllable so selecting an item can switch panes. Both panes are labelled regions. Fills its container.
- Package: `@gntik-ai/ui` · export `SplitLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/SplitLayout/SplitLayout.doc.ts
- Primitive: `Resizable (role="separator")`
- ARIA pattern: window splitter + two labelled regions
- Registry dependencies: button, resizable, ui-utils
- Install: `gntik-ui add split-layout`; own the source with `gntik-ui eject split-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Arrow Left / Right | On the resize handle: shrinks / grows the list pane (from Resizable) |
| Home / End | On the resize handle: list pane to its minimum / maximum size |
| Enter / Space | On Back (small screens): returns to the list and moves focus to it |
Tokens: background, card, border, accent, muted-foreground, primary, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { StatusTag } from '@gntik-ai/ui';
import { SplitLayout } from '@gntik-ai/ui';
const STATUSES = {
paid: { label: 'Paid', tone: 'success' },
due: { label: 'Due', tone: 'warning' },
overdue: { label: 'Overdue', tone: 'destructive' },
} as const;
const INVOICES = [
{ id: 'INV-2044', customer: 'Northbeam Labs', amount: '$4,210.00', status: 'paid', due: 'Oct 12' },
{ id: 'INV-2043', customer: 'Vega Robotics', amount: '$1,940.00', status: 'due', due: 'Oct 08' },
{ id: 'INV-2042', customer: 'Acme Industries', amount: '$862.50', status: 'paid', due: 'Oct 02' },
{ id: 'INV-2041', customer: 'Orbit Freight', amount: '$318.00', status: 'overdue', due: 'Sep 28' },
] as const;
export default function SplitLayoutInvoices() {
const [selected, setSelected] = useState(INVOICES[0].id);
const [showDetail, setShowDetail] = useState(false);
const invoice = INVOICES.find((i) => i.id === selected) ?? INVOICES[0];
return (
Invoices}
detailHeader={{invoice.id} }
list={
{INVOICES.map((inv) => (
{
setSelected(inv.id);
setShowDetail(true);
}}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-start hover:bg-accent/55 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring aria-[current=true]:bg-accent"
>
{inv.id}
{inv.customer}
{inv.amount}
))}
}
detail={
{invoice.amount}
Customer
{invoice.customer}
Due
{invoice.due}
}
/>
);
}
```
### StackedLayout (`stacked-layout`)
Top-navigation shell: a chrome navbar (brand · primary links or a custom `nav` slot such as a MegaMenu · actions), an optional tab sub-nav row, an optional page-header band and centered content (narrow 640 · default 1120 · wide 1440 · full) with an optional footer. Below lg the links collapse into a MobileNav drawer. Fills its container; fullScreen switches to h-dvh.
- Package: `@gntik-ai/ui` · export `StackedLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/StackedLayout/StackedLayout.doc.ts
- Primitive: `@base-ui/react/drawer`
- ARIA pattern: landmarks (banner, navigation, main, contentinfo) + skip link
- Registry dependencies: link, mobile-nav, nav-list, ui-utils, visually-hidden
- Install: `gntik-ui add stacked-layout`; own the source with `gntik-ui eject stacked-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab (first stop) | Reveals the skip link; Enter moves focus to |
| Tab | Moves through the navbar links (the current one has aria-current="page") and actions |
| Enter (nav slot) | In the `nav` slot: follows the slot component's own contract (e.g. MegaMenu opens a panel) |
| Enter / Space | On the menu button (small screens): opens the navigation drawer |
| Escape | Closes the navigation drawer and returns focus to the menu button |
Tokens: chrome, background, card, accent, accent-foreground, border, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { MegaMenu, megaMenuNavItems, type MegaMenuItem } from '@gntik-ai/ui';
import { Logo } from '@gntik-ai/ui';
import { ThemeProvider } from '@gntik-ai/ui';
import { ThemeCycleButton } from '@gntik-ai/ui';
import { StackedLayout } from '@gntik-ai/ui';
const items: MegaMenuItem[] = [
{ label: 'Overview', href: '#overview' },
{
label: 'Workspace',
sections: [
{
title: 'Build',
links: [
{ label: 'Projects', href: '#projects', description: 'Every project, its environments and owners.' },
{ label: 'Deployments', href: '#deployments', description: 'Releases, rollbacks and deploy gates.' },
],
},
],
},
{ label: 'Docs', href: '#docs' },
];
export default function StackedLayoutMegaMenu() {
const [href, setHref] = useState('#projects');
return (
}
nav={
link.href && setHref(link.href)} />}
links={megaMenuNavItems(items)}
currentHref={href}
onNavigate={(item) => item.href && setHref(item.href)}
actions={ }
>
Projects
The `nav` slot puts a MegaMenu in the top bar on large screens; the same items feed the drawer on small
ones.
);
}
```
### StatusLayout (`status-layout`)
Full-page status for 404, 403, 500 and maintenance: a centred message with a `heading` slot replacing `title` for exactly one page h1; `titleRef` on the built-in h1 enables focus() with tabIndex=-1 outside the Tab order. With no heading, `title` remains required and renders as today. Supply either title or a non-empty heading, never both; the slot must contain exactly one h1. The slot is full width within the centred column and uses the foreground token. For a PageHeader slot, set as="div", breadcrumbs={null}, meta={[]}, tabs={null}, actions={[]}, status="" and description={null} to disable demo fixtures; keep the description on StatusLayout so it appears once. Pass titleRef to PageHeader (or a ref and tabIndex=-1 to a generic h1) and call ref.current.focus() on route load when needed. Keep slot links and actions empty so the primary action stays first in main’s Tab order. Code stays above the heading, with description, illustration or icon halo, primary and secondary actions, optional Logo header and footer unchanged. `tone` colours the code and halo with severity tokens; brand green is only for positive states. Fills its container; `fullScreen` switches to `h-dvh`.
- Package: `@gntik-ai/ui` · export `StatusLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/StatusLayout/StatusLayout.doc.ts
- ARIA pattern: landmarks (header, main, footer) + skip link
- Registry dependencies: ui-utils, visually-hidden
- Install: `gntik-ui add status-layout`; own the source with `gntik-ui eject status-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Skip link, header links, then the primary action first and the secondary action next, then the footer |
| Programmatic focus | titleRef.current.focus() focuses the built-in h1; for a heading slot, use the heading node’s own ref (PageHeader titleRef). Neither h1 is a Tab stop |
Tokens: background, foreground, secondary, border, muted-foreground, primary-text, warning-text, destructive-text, focus-ring
Usage:
```tsx
import { useEffect, useRef } from 'react';
import { Button } from '@gntik-ai/ui';
import { StatusLayout } from '@gntik-ai/ui';
export default function StatusLayoutHeading() {
const titleRef = useRef(null);
useEffect(() => {
titleRef.current?.focus();
}, []);
return (
Page not found
}
description="Check the address or head back to the dashboard."
primaryAction={
} nativeButton={false}>
Go to dashboard
}
secondaryAction={
window.history.back()}>
Go back
}
/>
);
}
```
### WizardLayout (`wizard-layout`)
Focus-mode shell: title and Exit, Stepper, step body, Back / Next and Finish. mode="page" is the unchanged default (container height, or fullScreen for h-dvh). mode="overlay" is a controlled Base UI modal: open/onOpenChange keep the parent page mounted without navigation; size defaults to "fullscreen" at every viewport, while "dialog" reuses Dialog xl sizing and fills mobile viewports. Use page for a dedicated flow, fullscreen for a focused in-page flow, or the dialog-wizard block (default "dialog") for a compact create flow. Closing never resets the consumer-controlled current step or data. Exit, Escape and Cancel on step zero use confirmExit (default true). Outside presses are ignored unless closeOnInteractOutside is true, then use the same exit confirmation. returnFocusRef overrides focus return to the opener. Each step focuses its heading, first control or body, then announces via a separate polite live region. Supply stepAnnouncement({position,total,label}) from your i18n (required for rich step labels); positions are one-based. All action labels, stepsSummaryLabel and pendingLabel are overridable with i18n fallbacks. finishPending or nextLoading disables Finish and shows pendingLabel in overlay mode. stepsInteractive defaults to true; false omits onStepClick so completed steps are display-only. stepStatusText and formatStepCount pass through to both Stepper variants for localized status words and position text. backDisabled locks Back using aria-disabled while keeping it focusable, including on step zero; it cannot activate Cancel. This does not affect Exit. Below lg, Stepper uses its compact summary. An empty stepAnnouncement suppresses the overlay live region so a composition can announce through its nearest LiveAnnouncer instead; heading focus is preserved.
- Package: `@gntik-ai/ui` · export `WizardLayout` · group Layout · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/ui/src/layouts/WizardLayout/WizardLayout.doc.ts
- Primitive: `@base-ui/react/dialog + @base-ui/react/alert-dialog (exit confirmation)`
- ARIA pattern: page landmarks or modal dialog with labelled step nav + nested alertdialog
- Registry dependencies: alert-dialog, button, dialog, stepper, ui-utils, visually-hidden
- Install: `gntik-ui add wizard-layout`; own the source with `gntik-ui eject wizard-layout`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab | Skip link, Exit, completed steps, the step body, then Back and Next |
| Enter / Space on Back / Next | Moves to the previous / next step (Finish on the last one) |
| Escape | Opens the exit confirmation (keys inside portalled popups are ignored) |
| Enter on Exit | Opens the exit confirmation; Leave confirms, Stay or Escape closes it |
| Tab / Enter on the Stepper | Moves between completed steps; Enter goes back to one |
| Tab / Shift+Tab in overlay | Wraps inside the modal; closing returns focus to returnFocusRef or the opener |
| Next / Back in overlay | Moves focus to the new step heading or first control, then politely announces the step |
Tokens: background, foreground, card, border, primary, muted-foreground, focus-ring
Usage:
```tsx
import { useState } from 'react';
import { Field, FieldDescription, FieldLabel } from '@gntik-ai/ui';
import { Input } from '@gntik-ai/ui';
import { RadioGroup, Radio } from '@gntik-ai/ui';
import { Logo } from '@gntik-ai/ui';
import { WizardLayout } from '@gntik-ai/ui';
const STEPS = [{ label: 'Details' }, { label: 'Region' }, { label: 'Members' }, { label: 'Review' }];
export default function WizardLayoutNewProject() {
const [step, setStep] = useState(0);
const [name, setName] = useState('billing-sync');
const [status, setStatus] = useState('');
return (
}
title="New project"
steps={STEPS}
current={step}
onStepChange={setStep}
nextDisabled={step === 0 && name.trim() === ''}
onFinish={() => setStatus(`Project “${name}” created`)}
onExit={() => setStatus('Setup cancelled')}
exitTitle="Leave project setup?"
exitDescription="The project has not been created yet. Your answers will be discarded."
footerStart={
{status} }
>
{step === 0 && (
)}
{step === 1 && (
)}
{step === 2 && (
)}
{step === 3 && (
Review
{name} will be created in EU West with 1 member.
)}
);
}
```
## Blocks
### Assistant card grid (`assistant-card-grid`)
Responsive grid of clickable assistant cards: name, description, model and runtime, status and last update, with a create action.
- Package: `@gntik-ai/blocks` · export `AssistantCardGrid` · group ai · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/ai/AssistantCardGrid/block.meta.ts
- Composes: ClickableCard, StatusTag, Button, EmptyState
- Preview: /preview.html?kind=block&id=assistant-card-grid
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add assistant-card-grid`
Usage:
```tsx
import { AssistantCardGrid } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Evaluation scorecard (`evaluation-scorecard`)
Evaluation run as a datasets × metrics table: pass rate, scores against thresholds, deltas vs the baseline and flagged regressions, with a regressions-only filter.
- Package: `@gntik-ai/blocks` · export `EvaluationScorecard` · group ai · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/ai/EvaluationScorecard/block.meta.ts
- Composes: Table, Badge, ToggleGroup, Toggle
- Preview: /preview.html?kind=block&id=evaluation-scorecard
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add evaluation-scorecard`
Usage:
```tsx
import { EvaluationScorecard } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Model keys list (`model-keys-list`)
Model provider keys with masked secrets and status: test the connection (spinner while it runs, result announced) and revoke behind a confirmation dialog.
- Package: `@gntik-ai/blocks` · export `ModelKeysList` · group ai · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/ai/ModelKeysList/block.meta.ts
- Composes: Card, StatusTag, Button, Spinner, AlertDialog, EmptyState
- Preview: /preview.html?kind=block&id=model-keys-list
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add model-keys-list`
Usage:
```tsx
import { ModelKeysList } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Prompt editor (`prompt-editor`)
System and user prompt editors that detect {{variables}} as chips with value inputs, plus temperature and max-token parameters and a Run action. Variable values can be controlled (variables, onVariablesChange).
- Package: `@gntik-ai/blocks` · export `PromptEditor` · group ai · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/ai/PromptEditor/block.meta.ts
- Composes: Textarea, Token, Field, Input, Slider, Button
- Preview: /preview.html?kind=block&id=prompt-editor
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add prompt-editor`
Usage:
```tsx
import { PromptEditor } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Token cost card (`token-cost-card`)
Input and output token counts, their split, total cost and a budget meter, with a period switch.
- Package: `@gntik-ai/blocks` · export `TokenCostCard` · group ai · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/ai/TokenCostCard/block.meta.ts
- Composes: Card, ToggleGroup, Toggle, Meter
- Preview: /preview.html?kind=block&id=token-cost-card
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add token-cost-card`
Usage:
```tsx
import { TokenCostCard } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Trace waterfall (`trace-waterfall`)
Span tree of a trace with indentation and duration bars positioned on a shared time axis; spans expand and collapse; the list is one tab stop with arrow-key navigation (←/→ collapse and expand), selection is controlled or uncontrolled, the details pane is optional or custom, and running spans show an in-progress bar (pulse stops under reduced motion) with a visible "running" label.
- Package: `@gntik-ai/blocks` · export `TraceWaterfall` · group ai · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/ai/TraceWaterfall/block.meta.ts
- Composes: Badge, StatusTag
- Preview: /preview.html?kind=block&id=trace-waterfall
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add trace-waterfall`
Usage:
```tsx
import { TraceWaterfall } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Forgot password form (`forgot-password-form`)
Email (default) or username-or-email password recovery; onSubmit receives the trimmed identifier. Supports identifier (type, autoComplete, label, help/helpId, placeholder, minLength/maxLength), messages (identifierRequired/identifierInvalid/identifierTooShort/passwordRequired), and React 19 ref.focus. The kit owns inline validation and focuses the first invalid field; consumers supply messages, not a second inline validator. Server errors use error + fieldInvalid + feedbackId; aria-describedby describes the form, while flagged inputs also retain their help and error ids. headingLevel (h1/h2/h3/null) suppresses the heading without losing the form name; aria-label/aria-labelledby override the default name. ref.focus accepts identifier. title/description and sentTitle/sentDescription configure request and sent copy; sentTitle, sentDescription, sentStatus and resendActions accept null to hide inbox copy, announcements and resend/change-identifier actions. Preview: forgot-password-form--username-or-email. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=forgot-password-form--labels.
- Package: `@gntik-ai/blocks` · export `ForgotPasswordForm` · group auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/auth/ForgotPasswordForm/block.meta.ts
- Composes: Field, FieldLabel, FieldDescription, FieldError, Input, Button, Alert, Link
- Preview: /preview.html?kind=block&id=forgot-password-form
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add forgot-password-form`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `formLabel` | `string` | Default content and accessible names |
| `eyebrow` | `string` | Default content and accessible names |
| `title` | `string` | Default content and accessible names |
| `description` | `string` | Default content and accessible names |
| `sentTitle` | `string` | Async feedback |
| `sentDescription` | `(email: string) => ReactNode` | Async feedback |
| `addressFallback` | `string` | Async feedback |
| `sentStatus` | `string` | Async feedback |
| `fallbackError` | `string` | Validation feedback |
| `backToSignIn` | `string` | Default content and accessible names |
| `resend` | `string` | Default content and accessible names |
| `changeEmail` | `string` | Default content and accessible names |
| `identifierLabel` | `string` | Default content and accessible names |
| `identifierPlaceholder` | `string` | Default content and accessible names |
| `submit` | `string` | Default content and accessible names |
| `identifierRequired` | `string` | Validation feedback |
| `identifierInvalid` | `string` | Validation feedback |
| `identifierTooShort` | `string` | Validation feedback |
Usage:
```tsx
import { ForgotPasswordForm } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### MFA challenge (`mfa-challenge`)
Second-factor step with a one-time code (auto-submit), a backup-code mode and resend with a countdown. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=mfa-challenge--labels.
- Package: `@gntik-ai/blocks` · export `MfaChallenge` · group auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/auth/MfaChallenge/block.meta.ts
- Composes: OtpInput, Field, Input, Button, Alert
- Preview: /preview.html?kind=block&id=mfa-challenge
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add mfa-challenge`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `eyebrow` | `string` | Default content and accessible names |
| `title` | `string` | Default content and accessible names |
| `backupTitle` | `string` | Default content and accessible names |
| `backupDescription` | `string` | Default content and accessible names |
| `destinationDescription` | `(length: number, destination: string) => string` | Default content and accessible names |
| `description` | `(length: number) => string` | Default content and accessible names |
| `formLabel` | `string` | Default content and accessible names |
| `codeLabel` | `string` | Default content and accessible names |
| `codeRequired` | `(length: number) => string` | Validation feedback |
| `backupLabel` | `string` | Default content and accessible names |
| `backupPlaceholder` | `string` | Default content and accessible names |
| `backupHint` | `string` | Default content and accessible names |
| `backupRequired` | `string` | Validation feedback |
| `submit` | `string` | Default content and accessible names |
| `useCode` | `string` | Default content and accessible names |
| `resendIn` | `(remaining: number) => string` | Default content and accessible names |
| `resend` | `string` | Default content and accessible names |
| `resent` | `string` | Async feedback |
| `backToSignIn` | `string` | Default content and accessible names |
| `fallbackError` | `string` | Validation feedback |
Usage:
```tsx
import { MfaChallenge } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Reset password form (`reset-password-form`)
New password with a strength meter and a confirmation field with mismatch validation, then a success state. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=reset-password-form--labels.
- Package: `@gntik-ai/blocks` · export `ResetPasswordForm` · group auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/auth/ResetPasswordForm/block.meta.ts
- Composes: Field, PasswordInput, Button, Alert
- Preview: /preview.html?kind=block&id=reset-password-form
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add reset-password-form`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `eyebrow` | `string` | Default content and accessible names |
| `title` | `string` | Default content and accessible names |
| `description` | `(minimum: number) => string` | Default content and accessible names |
| `newPassword` | `string` | Default content and accessible names |
| `confirmPassword` | `string` | Default content and accessible names |
| `passwordTooShort` | `(minimum: number) => string` | Validation feedback |
| `passwordMismatch` | `string` | Validation feedback |
| `submit` | `string` | Default content and accessible names |
| `successTitle` | `string` | Async feedback |
| `successDescription` | `string` | Async feedback |
| `successStatus` | `string` | Async feedback |
| `continue` | `string` | Default content and accessible names |
| `fallbackError` | `string` | Validation feedback |
Usage:
```tsx
import { ResetPasswordForm } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Sign-in form (`sign-in-form`)
Email (default) or text identifier and password sign-in. Supports identifier (type, autoComplete, label, help/helpId, placeholder, minLength/maxLength), messages (identifierRequired/identifierInvalid/identifierTooShort/passwordRequired), and React 19 ref.focus. The kit owns inline validation and focuses the first invalid field; consumers supply messages, not a second inline validator. Server errors use error + fieldInvalid + feedbackId; aria-describedby describes the form, while flagged inputs also retain their help and error ids. headingLevel (h1/h2/h3/null) suppresses the heading without losing the form name; aria-label/aria-labelledby override the default name. passwordHelp/passwordHelpId describe the password. Text submissions add values.identifier and retain values.email; email-mode payloads are unchanged. sso and signUpPrompt accept replacements or null. Preview: sign-in-form--username. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=sign-in-form--labels.
- Package: `@gntik-ai/blocks` · export `SignInForm` · group auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/auth/SignInForm/block.meta.ts
- Composes: Field, FieldLabel, FieldDescription, FieldError, Input, PasswordInput, Checkbox, Button, Alert, Link, SsoButtons
- Registry dependencies: sso-buttons
- Preview: /preview.html?kind=block&id=sign-in-form
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add sign-in-form`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `eyebrow` | `string` | Default content and accessible names |
| `title` | `string` | Default content and accessible names |
| `description` | `string` | Default content and accessible names |
| `formLabel` | `string` | Default content and accessible names |
| `identifierLabel` | `string` | Default content and accessible names |
| `identifierPlaceholder` | `string` | Default content and accessible names |
| `password` | `string` | Default content and accessible names |
| `forgotPassword` | `string` | Default content and accessible names |
| `passwordRequired` | `string` | Validation feedback |
| `remember` | `string` | Default content and accessible names |
| `submit` | `string` | Default content and accessible names |
| `divider` | `string` | Default content and accessible names |
| `signUpPrompt` | `string` | Default content and accessible names |
| `signUp` | `string` | Default content and accessible names |
| `fallbackError` | `string` | Validation feedback |
| `identifierRequired` | `string` | Validation feedback |
| `identifierInvalid` | `string` | Validation feedback |
| `identifierTooShort` | `string` | Validation feedback |
Usage:
```tsx
import { SignInForm } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Sign-up form (`sign-up-form`)
Account creation with default name/email fields or ordered text/email fields (values.fields), a password strength meter and required consent (terms=false opts out). minPasswordLength defaults to 8 and drives length validation and the insufficient-length meter; optional passwordHelp can receive the minimum. Configured fields support help, required, autoComplete and validation messages, with a linked validation summary on invalid submit. policyLoading renders skeletons without a form; unavailable replaces the form with consumer title, message and optional action. loading remains submitting; error is form-level feedback. Examples: custom-fields, no-terms, unavailable, loading, submitting and error. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=sign-up-form--labels.
- Package: `@gntik-ai/blocks` · export `SignUpForm` · group auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/auth/SignUpForm/block.meta.ts
- Composes: Field, Input, PasswordInput, Checkbox, Button, Alert, Link, SsoButtons, Skeleton, FieldDescription, ValidationSummary
- Registry dependencies: sso-buttons, validation-summary
- Preview: /preview.html?kind=block&id=sign-up-form
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add sign-up-form`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `eyebrow` | `string` | Default content and accessible names |
| `title` | `string` | Default content and accessible names |
| `description` | `string` | Default content and accessible names |
| `name` | `string` | Default content and accessible names |
| `namePlaceholder` | `string` | Default content and accessible names |
| `nameRequired` | `string` | Validation feedback |
| `email` | `string` | Default content and accessible names |
| `emailPlaceholder` | `string` | Default content and accessible names |
| `password` | `string` | Default content and accessible names |
| `passwordTooShort` | `(minimum: number) => string` | Validation feedback |
| `termsRequired` | `string` | Validation feedback |
| `terms` | `string` | Default content and accessible names |
| `privacy` | `string` | Default content and accessible names |
| `termsConsent` | `(terms: ReactNode, privacy: ReactNode) => ReactNode` | Async feedback |
| `submit` | `string` | Default content and accessible names |
| `divider` | `string` | Default content and accessible names |
| `signInPrompt` | `string` | Default content and accessible names |
| `signIn` | `string` | Default content and accessible names |
| `policyLoading` | `string` | Async feedback |
| `fallbackError` | `string` | Validation feedback |
| `emailInvalid` | `string` | Validation feedback |
| `fieldRequired` | `(label: string) => string` | Validation feedback |
| `fieldInvalid` | `string` | Validation feedback |
Usage:
```tsx
import { SignUpForm } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### SSO buttons (`sso-buttons`)
Neutral "Continue with …" provider buttons with lucide icons and a per-provider loading state. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=sso-buttons--labels.
- Package: `@gntik-ai/blocks` · export `SsoButtons` · group auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/auth/SsoButtons/block.meta.ts
- Composes: Button
- Preview: /preview.html?kind=block&id=sso-buttons
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add sso-buttons`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `label` | `string` | Default content and accessible names |
Usage:
```tsx
import { SsoButtons } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Cost breakdown (`cost-breakdown`)
Cost composition: a donut with the total in the hole and the categories ranked by amount with share bars, in categorical (non-severity) colours.
- Package: `@gntik-ai/blocks` · export `CostBreakdown` · group billing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/billing/CostBreakdown/block.meta.ts
- Composes: Card, DonutChart
- Preview: /preview.html?kind=block&id=cost-breakdown
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add cost-breakdown`
Usage:
```tsx
import { CostBreakdown } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Invoice table (`invoice-table`)
Invoice history table with number, date, amount, a status badge (paid, due, overdue, void) and a per-row PDF download.
- Package: `@gntik-ai/blocks` · export `InvoiceTable` · group billing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/billing/InvoiceTable/block.meta.ts
- Composes: Table, Badge, Link, Button
- Preview: /preview.html?kind=block&id=invoice-table
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add invoice-table`
Usage:
```tsx
import { InvoiceTable } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Payment method card (`payment-method-card`)
The card on file as text (brand, last four digits, expiry, holder) with default/expiry badges and update/remove actions; no card artwork.
- Package: `@gntik-ai/blocks` · export `PaymentMethodCard` · group billing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/billing/PaymentMethodCard/block.meta.ts
- Composes: Card, Badge, Button
- Preview: /preview.html?kind=block&id=payment-method-card
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add payment-method-card`
Usage:
```tsx
import { PaymentMethodCard } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Plan card (`plan-card`)
Default billing header retains sample plan, Active status fallback, required monthly price and renewal, monthly/annual per-month pricing and built-in Manage/Upgrade actions. Opt-in variant="summary" uses only consumer data: optional name or emptyLabel, statusLabel, price totals (including zero), cycle, renewal, features and action slots; no sample fallback. See README.md for props, ARIA, keyboard and examples.
| Prop | Contract |
| --- | --- |
| variant | billing (default) \| summary |
| billing plan | BillingPlan: name, priceMonthly and renewsOn required; status, priceAnnual, currency, summary and features optional. Omitted plan uses samplePlan. |
| summary plan | SummaryPlan: all optional: name, statusLabel, statusTone, priceMonthly, priceAnnual, currency, cycle, renewsOn, features. Prices are supplied cycle totals, never multiplied or discounted. |
| emptyLabel | Summary only: consumer label used when the name is absent. |
| actions | Summary only: ReactNode slot of kit Buttons or native links, absent by default. |
| defaultCycle / onCycleChange | Initial monthly (default) or annual selection and cycle callback. |
| titleAs / className | h2 \| h3 (default) \| h4; additional token-backed classes. |
| onUpgrade / onManage / upgradeLabel / manageLabel | Billing only: existing action handlers and labels; both buttons always render. |
Examples:
- [summary-full](/preview.html?kind=block&id=plan-card--summary-full)
- [summary-minimal](/preview.html?kind=block&id=plan-card--summary-minimal)
- [summary-unassigned](/preview.html?kind=block&id=plan-card--summary-unassigned)
- [summary-unassigned-no-label](/preview.html?kind=block&id=plan-card--summary-unassigned-no-label)
- [summary-price-only](/preview.html?kind=block&id=plan-card--summary-price-only)
- [summary-zero-price](/preview.html?kind=block&id=plan-card--summary-zero-price)
- [summary-renewal-only](/preview.html?kind=block&id=plan-card--summary-renewal-only)
- [summary-features-only](/preview.html?kind=block&id=plan-card--summary-features-only)
- [summary-actions-only](/preview.html?kind=block&id=plan-card--summary-actions-only)
- Package: `@gntik-ai/blocks` · export `PlanCard` · group billing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/billing/PlanCard/block.meta.ts
- ARIA pattern: Heading (titleAs h2/h3/h4), text-labelled Badge, optional features list and native consumer actions. Summary omits absent regions; no heading plus a development warning when name and emptyLabel are absent. ToggleGroup has an accessible Billing cycle label and aria-pressed buttons, rendered only with both prices. Axe coverage in dark, light and high_contrast.
- Composes: Card, Badge, Button, ToggleGroup, Toggle
- Preview: /preview.html?kind=block&id=plan-card
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add plan-card`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Move through the cycle toggle (only with both prices) and supplied buttons or links. |
| Arrow keys / Home / End | Move focus within the monthly/annual toggle group. |
| Enter / Space | Activate focused buttons and toggles; Enter activates links. |
Tokens: card, foreground, muted-foreground, border, focus-ring
Usage:
```tsx
import { PlanCard } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Plan definition card (`plan-definition-card`)
Administrative plan definition without billing fields: name, slug, optional description, labelled draft/active/deprecated/archived status, enabled/disabled capabilities, quotas, assigned tenants and formatted update date. Compact and full densities, consumer action slots and statusLabels overrides. See README.md for props, ARIA, keyboard and examples.
| Prop | Contract |
| --- | --- |
| plan | PlanDefinition: name, slug, status, tenantCount and updatedAt required; optional description, capabilities (Record) and quotas (Record). Omitted plan uses the neutral definition fixture. |
| density | compact \| full (default); both show all supplied definition data. |
| statusLabels | Partial> overrides English lifecycle labels. |
| actions | ReactNode slot of kit Buttons or native links; absent by default. |
| titleAs / className | h2 \| h3 (default) \| h4; additional token-backed classes. |
Examples:
- [compact](/preview.html?kind=block&id=plan-definition-card--compact)
- [full](/preview.html?kind=block&id=plan-definition-card--full)
- Package: `@gntik-ai/blocks` · export `PlanDefinitionCard` · group billing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/billing/PlanDefinitionCard/block.meta.ts
- ARIA pattern: Heading (titleAs h2/h3/h4), visible lifecycle Badge, Capabilities list with Enabled/Disabled text, quota/tenant/update description list, and consumer native actions. No price, billing cycle, renewal or built-in upgrade action. Axe coverage in dark, light and high_contrast at compact and full densities.
- Composes: Card, Badge, Button
- Preview: /preview.html?kind=block&id=plan-definition-card
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add plan-definition-card`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Move between consumer-supplied buttons and links. |
| Enter | Activate a focused button or link. |
| Space | Activate a focused button. |
Tokens: card, foreground, muted-foreground, border, focus-ring
Usage:
```tsx
import { PlanDefinitionCard } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Quota meters (`quota-meters`)
Plan quotas as usage meters with used/limit labels, amber/red thresholds, overage notes and a current/projected toggle.
- Package: `@gntik-ai/blocks` · export `QuotaMeters` · group billing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/billing/QuotaMeters/block.meta.ts
- Composes: Meter, ToggleGroup, Toggle
- Preview: /preview.html?kind=block&id=quota-meters
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add quota-meters`
Usage:
```tsx
import { QuotaMeters } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Spend vs budget (`spend-vs-budget`)
Cumulative spend line with the end-of-period forecast and a flat budget line, a summary row and a within/over budget status.
- Package: `@gntik-ai/blocks` · export `SpendVsBudget` · group billing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/billing/SpendVsBudget/block.meta.ts
- Composes: Card, Badge, LineChart
- Preview: /preview.html?kind=block&id=spend-vs-budget
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add spend-vs-budget`
Usage:
```tsx
import { SpendVsBudget } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Code panel (`code-panel`)
Multi-file code panel with optional readOnly, owned line markers, onValidityChange and onMonacoReady, merging every Monaco marker owner into Problems; the lazy loader example injects inline YAML schemas and same-origin yaml workers under CSP. The open file can be controlled (activeFile, onActiveFileChange) and the heading level set (headingLevel). Optional readOnly prevents editor edits. markers accepts CodePanelMarker entries {id,owner,file,line,column?,endLine?,endColumn?,severity,message,source?}; each owner is reserved for its consumer diagnostics service. The Problems list merges problems, consumer markers, and Monaco model markers from every owner, including worker-backed YAML schema errors. onValidityChange(valid) fires only when errors cross zero (initially clean emits nothing); warnings do not invalidate. onMonacoReady(monaco) configures consumer services once per Monaco instance and may return cleanup. loader can configure monaco-yaml before mounting; the only supported YAML path is consumer-injected, with enableSchemaRequest:false and an inline schema. examples/diagnostics.tsx demonstrates a semantic-worker owner next to schema diagnostics. See packages/editor/README.md for the lazy Vite yaml.worker recipe, Monaco 0.57 compatibility alias, and CSP worker-src self blob: with same-origin scripts and assets. Supplying markers (including []), onMonacoReady or onValidityChange enables model-marker collection; readOnly controls editing independently. Omitting diagnostics extensions preserves the original supplied-problems-only view. No kit runtime dependency on monaco-yaml.
- Package: `@gntik-ai/blocks` · export `CodePanel` · group builders · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/builders/CodePanel/block.meta.ts
- Composes: Tabs, ToggleGroup, Toggle, CodeEditor, DiffEditor
- Preview: /preview.html?kind=block&id=code-panel
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add code-panel`
Usage:
```tsx
import { CodePanel } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Flow builder (`flow-builder`)
FlowGraph is {nodes,edges}; FlowGraphChange names the edit type and affected nodeIds/edgeIds. Optional onGraphChange(graph, change) reports each committed add, delete, move, connect or inspector update once, omitting selected, dragging and measured from nodes. Inspector updates report every value change: hosts persisting every call should debounce. Selection, measurement, panel resizing, rejected connections and run mode report nothing. Optional graph controls content: render the parent graph without applying edits locally, while selection, measured sizes and active drag positions stay internal by stable node id. nodes/edges remain initial values when uncontrolled. onRun keeps the canvas node objects. builderNodes and builderEdges expose the shared starter graph; cleanFlowGraph(graph) returns a graph without transient node fields for persistence. examples/ControlledGraph.tsx shows graph={graph} onGraphChange={setGraph}. Palette ids skip existing kind-n ids. Visual flow editor with optional nodeTypes for named handles (branch-arm, parallel and sequence examples), onConnectEdge for edge type/data mapping, isValidConnection and onConnectionRejected for rules, onConnect for accepted connections, validationIssues for node/flow problems, mode="run" for a read-only canvas, nodeStatus for live badges, and onNodeSelect for pointer/keyboard selection. Optional nodeTypes registers kit-owned {type, handles:[{id,kind,position?,label?}], render, decorations?} definitions (no React Flow NodeProps). Keep nodeTypes and its renderer definitions stable (module scope or useMemo) across nodeStatus updates: recreating the array rebuilds custom node components and remounts them, defeating per-node memoization. onConnectEdge({source,target,sourceType,targetType,sourceHandle,targetHandle}) returns {type,data}; examples/extensions.tsx maps branch arm-0 to {kind: "branch-arm", armIndex: 0}, parallel branch to {kind: "parallel"}, and sequence next to {kind: "sequence"}. isValidConnection returns true, false, or {code,nodeId?,message}; onConnectionRejected reports {code,nodeId,message} once (nodeId defaults to the source) so consumers can forward it into validationIssues and control its lifetime, while onConnect reports accepted connections only. validationIssues decorates nodes with an icon and accessible text; node issues select and focus their node, and flow issues have no node link. mode="run" removes the Run action, palette, inspector and graph mutation while keeping labelled keyboard-selectable nodes, pan/zoom, fit controls and the wide-screen minimap. nodeStatus maps node IDs to FlowTone badges (running, degraded, failed, paused, done, handoff, review); onNodeSelect(id) handles pointer and keyboard selection. RunFlowExample shows live updates with per-node memoization. FlowNode defaults to the built-in FlowNodeKind union; use FlowNode (or a custom kind union) for custom graphs, inferred by FlowBuilder and WorkflowBuilderPage. Custom renderers use kit status overlays and issue decorations by default; set decorations:"consumer" when render owns them, using data.decoration with accessible text and icons (for example, render:({data}) => ). FlowCanvas showInteractiveControls=false retains zoom/fit without the editing toggle. Named handles are keyboard controls: Enter or Space starts or completes a connection in edit mode; handles are disabled in run mode.
- Package: `@gntik-ai/blocks` · export `FlowBuilder` · group builders · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/builders/FlowBuilder/block.meta.ts
- Composes: FlowCanvas, useFlowState, ResizablePanelGroup, ResizablePanel, ResizeHandle, Button, Field, Input, SimpleSelect
- Preview: /preview.html?kind=block&id=flow-builder
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add flow-builder`
Usage:
```tsx
import { FlowBuilder } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Log viewer (`log-viewer`)
Windowed log stream (role="log"): level filter, search with highlighted matches, follow-tail and per-line copy. Renders only the visible rows. Optional wrap (default false) shows full messages in measured, variable-height rows; rowHeight estimates unmeasured rows. Optional labels?: Partial overrides each built-in label, including per-level chip and filter names through labels.levels. Explicit content props win over labels, then I18nProvider (en/es); no provider keeps English. Copied log text and imported fixtures stay unchanged. See the Catalog labels and Full messages and level names live previews. Copy actions await the clipboard write; denied or unavailable access shows common.copyFailed (overridden by labels.copyFailed), and only a successful write calls onCopyLine. Both results reset after 1500ms. See the Catalog labels live preview.
- Package: `@gntik-ai/blocks` · export `LogViewer` · group builders · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/builders/LogViewer/block.meta.ts
- Composes: ToggleGroup, Toggle, Input, Switch, IconButton
- Preview: /preview.html?kind=block&id=log-viewer
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add log-viewer`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Visible heading and log region name; title prop wins |
| `levelFilter` | `string` | Level filter group name |
| `levels` | `Partial>` | Per-level chip and filter names; omitted levels use the catalog |
| `searchLabel` | `string` | Searchbox name |
| `searchPlaceholder` | `string` | Searchbox placeholder |
| `follow` | `string` | Follow switch name |
| `count` | `(visible: number, total: number) => string` | Filtered line count |
| `empty` | `string` | No matching lines |
| `inspect` | `(timestamp: string) => string` | Inspect button name when onSelectLine exists |
| `copy` | `(timestamp: string) => string` | Copy button before copying |
| `copyFailed` | `string` | Copy button after a failed write, only on the affected line |
| `copied` | `string` | Copy button after copying |
Usage:
```tsx
import { LogViewer } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Activity feed (`activity-feed`)
Who did what, when: actor avatar, action, target and Timestamp, grouped by day (Today, Yesterday, date), with optional Load more.
- Package: `@gntik-ai/blocks` · export `ActivityFeed` · group data-display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/data-display/ActivityFeed/block.meta.ts
- Composes: Avatar, Timestamp, Button
- Preview: /preview.html?kind=block&id=activity-feed
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add activity-feed`
Usage:
```tsx
import { ActivityFeed } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Chart card (`chart-card`)
A Card with chart and legend: default 7d/30d/90d or custom ranges via ranges, controlled range/onRangeChange, and showRangeToggle={false} when a toolbar owns the range (pass its window as range). Supports title, optional headline and delta, area/line/bar/heatmap charts, and loading, empty and error states. Supply matching dataByRange keys and use defaultRange for initial uncontrolled selection; the toolbar is the only range control when the toggle is hidden. Unknown range/defaultRange keys fall back to the first option for selection, data and label, with a development-only warning. Chart states follow the kit I18nProvider locale automatically through ChartLocaleProvider; standalone charts need their own ChartLocaleProvider (en/es, per-key labels overrides). Number/date formatting stays caller-controlled.
- Package: `@gntik-ai/blocks` · export `ChartCard` · group data-display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/data-display/ChartCard/block.meta.ts
- Composes: Card, ToggleGroup, AreaChart, LineChart, BarChart, Heatmap, ChartLegend
- Preview: /preview.html?kind=block&id=chart-card
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add chart-card`
Usage:
```tsx
import { ChartCard } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Comment thread (`comment-thread`)
Plain-text comments with avatar, author and Timestamp, plus a reply composer (Textarea + Button, ⌘/Ctrl+Enter posts).
- Package: `@gntik-ai/blocks` · export `CommentThread` · group data-display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/data-display/CommentThread/block.meta.ts
- Composes: Avatar, Timestamp, Textarea, Button
- Preview: /preview.html?kind=block&id=comment-thread
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add comment-thread`
Usage:
```tsx
import { CommentThread } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Description list card (`description-list-card`)
Key–value detail card with mono, status and rich values, copy-to-clipboard buttons and an Edit action. Optional labels?: Partial overrides each built-in label. Explicit content props win over labels, then I18nProvider (en/es); no provider keeps English. Imported fixtures and machine data stay unchanged. See the Catalog labels live preview. wrap?: boolean (default false) shows full values and breaks long unbroken strings. columns?: 1 | 2 | 3 (default 1) stacks terms above values in two or three columns from sm, with one column below sm; items fill contiguous top-to-bottom chunks of ceil(item count / columns), and dividers stay within each column. Defaults preserve the original single-column rows. See the Spanish live preview for wrapping and both column layouts. Copy actions await the clipboard write; denied or unavailable access shows common.copyFailed (overridden by labels.copyFailed), and only a successful write calls onCopy. Both results reset after 1500ms. See the Catalog labels live preview.
- Package: `@gntik-ai/blocks` · export `DescriptionListCard` · group data-display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/data-display/DescriptionListCard/block.meta.ts
- Composes: Card, Button, IconButton, StatusTag
- Preview: /preview.html?kind=block&id=description-list-card
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add description-list-card`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Card heading; title prop wins |
| `description` | `string` | Card description; description prop wins |
| `edit` | `string` | Edit button; editLabel prop wins |
| `copy` | `(label: string) => string` | Copy button name; original item label |
| `copied` | `(label: string) => string` | Copy button name after copying; original item label |
| `copyFailed` | `string` | Copy button name and polite announcement after a failed write |
| `copiedToClipboard` | `string` | Polite copy announcement |
Usage:
```tsx
import { DescriptionListCard } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### KPI row (`kpi-row`)
Three to five stat tiles: label, value, a delta whose arrow and colour are independent, and an optional sparkline. Cards or a divided strip.
- Package: `@gntik-ai/blocks` · export `KpiRow` · group data-display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/data-display/KpiRow/block.meta.ts
- Composes: Sparkline
- Preview: /preview.html?kind=block&id=kpi-row
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add kpi-row`
Usage:
```tsx
import { KpiRow } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Resource card grid (`resource-card-grid`)
Responsive grid of resource cards with icon or avatar, name, status, meta figures and a row-actions menu.
- Package: `@gntik-ai/blocks` · export `ResourceCardGrid` · group data-display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/data-display/ResourceCardGrid/block.meta.ts
- Composes: Avatar, StatusTag, MoreMenu
- Preview: /preview.html?kind=block&id=resource-card-grid
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add resource-card-grid`
Usage:
```tsx
import { ResourceCardGrid } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Stat card (`stat-card`)
One rich stat: icon tile, value and delta that recalculate per range (7d/30d/90d), a trend chart and a footer action.
- Package: `@gntik-ai/blocks` · export `StatCard` · group data-display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/data-display/StatCard/block.meta.ts
- Composes: Card, ToggleGroup, Sparkline, Button
- Preview: /preview.html?kind=block&id=stat-card
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add stat-card`
Usage:
```tsx
import { StatCard } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Status timeline (`status-timeline`)
Vertical timeline of state changes with a StatusDot per state, details, actor and Timestamp; collapses older events and renders an accessible empty state for an explicit empty events array, with a localizable emptyLabel. The label, showFewerLabel, showEarlierLabel(hidden) and actorLabel props override the localized defaults.
- Package: `@gntik-ai/blocks` · export `StatusTimeline` · group data-display · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/data-display/StatusTimeline/block.meta.ts
- Composes: StatusDot, Timestamp, Button, EmptyState
- Preview: /preview.html?kind=block&id=status-timeline
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add status-timeline`
Usage:
```tsx
import { StatusTimeline } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Confirm destructive (`confirm-destructive`)
AlertDialog for irreversible actions with exact typed-name and optional canConfirm gating, extra form fields, typed failure states, guarded retry and focus return. See the plain deletion and secret revocation examples.
- Package: `@gntik-ai/blocks` · export `ConfirmDestructive` · group feedback · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/feedback/ConfirmDestructive/block.meta.ts
- Composes: AlertDialog, Button, Field, Input, EmptyState
- Registry dependencies: empty-states, error-panel
- Preview: /preview.html?kind=block&id=confirm-destructive
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add confirm-destructive`
Usage:
```tsx
import { ConfirmDestructive } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Empty states (`empty-states`)
Four canonical empty states: first run (with checklist), no results (clear filters/search), no access (request access) and not found (optional action). FirstRunEmpty, NoResultsEmpty and NoAccessEmpty accept labels?: Partial, Partial and Partial respectively. Each type has its own keys below; explicit title, description, actionLabel and secondary props win. Defaults follow I18nProvider (en/es). NoResultsEmpty keeps the query in a mono span unless queryTitle replaces the heading. NotFoundEmpty and all imported fixtures stay English. See the Catalog labels live preview. NoAccessEmpty accepts showRequestAccess?: boolean (default true): false hides both the request button and its status announcement, so defaultRequested has no visible effect. action?: { label: string; href: string } renders one kit Link, styled like FirstRunEmpty’s secondary link; it is secondaryAction after the request button, or primaryAction when showRequestAccess is false. With showRequestAccess=false and no action, it renders no actions. Omitting onRequestAccess still shows the button by default. See the Spanish live preview for no actions, a link alone, and a link with the request button.
- Package: `@gntik-ai/blocks` · export `NotFoundEmpty` · group feedback · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/feedback/EmptyStates/block.meta.ts
- Composes: EmptyState, Button, Link
- Preview: /preview.html?kind=block&id=empty-states
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add empty-states`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `FirstRunEmpty.labels.title` | `string` | FirstRunEmpty: per-key override; formatters receive original data. Explicit props win. |
| `FirstRunEmpty.labels.description` | `string` | FirstRunEmpty: per-key override; formatters receive original data. Explicit props win. |
| `FirstRunEmpty.labels.action` | `string` | FirstRunEmpty: per-key override; formatters receive original data. Explicit props win. |
| `FirstRunEmpty.labels.guide` | `string` | FirstRunEmpty: per-key override; formatters receive original data. Explicit props win. |
| `NoResultsEmpty.labels.queryTitle` | `(entity: string, query: string) => string` | NoResultsEmpty: per-key override; formatters receive original data. Explicit props win. |
| `NoResultsEmpty.labels.title` | `(entity: string) => string` | NoResultsEmpty: per-key override; formatters receive original data. Explicit props win. |
| `NoResultsEmpty.labels.description` | `(filterCount: number) => string` | NoResultsEmpty: per-key override; formatters receive original data. Explicit props win. |
| `NoResultsEmpty.labels.clearFilters` | `string` | NoResultsEmpty: per-key override; formatters receive original data. Explicit props win. |
| `NoResultsEmpty.labels.clearSearch` | `string` | NoResultsEmpty: per-key override; formatters receive original data. Explicit props win. |
| `NoAccessEmpty.labels.title` | `(resource: string) => string` | NoAccessEmpty: per-key override; formatters receive original data. Explicit props win. |
| `NoAccessEmpty.labels.description` | `(resource: string, owner: string) => string` | NoAccessEmpty: per-key override; formatters receive original data. Explicit props win. |
| `NoAccessEmpty.labels.request` | `string` | NoAccessEmpty: per-key override; formatters receive original data. Explicit props win. |
| `NoAccessEmpty.labels.requested` | `string` | NoAccessEmpty: per-key override; formatters receive original data. Explicit props win. |
| `NoAccessEmpty.labels.sent` | `string` | NoAccessEmpty: per-key override; formatters receive original data. Explicit props win. |
Usage:
```tsx
import { NotFoundEmpty } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Error panel (`error-panel`)
Error summary with status code, copyable request id, retry with loading state and a technical-details disclosure. Optional labels?: Partial overrides each built-in label. Explicit content props win over labels, then I18nProvider (en/es); no provider keeps English. Imported fixtures and machine data stay unchanged. See the Catalog labels live preview.
- Package: `@gntik-ai/blocks` · export `ErrorPanel` · group feedback · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/feedback/ErrorPanel/block.meta.ts
- Composes: Button, IconButton, Collapsible
- Preview: /preview.html?kind=block&id=error-panel
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add error-panel`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Panel heading; explicit title prop wins, including null |
| `message` | `string` | Summary; explicit message prop wins, including null |
| `support` | `string` | Default secondary action; explicit secondaryAction wins, including null |
| `retry` | `string` | Retry button; retryLabel prop wins |
| `retrying` | `string` | Busy retry button |
| `retryingStatus` | `string` | Busy status announcement |
| `requestId` | `string` | Request ID caption |
| `copyRequestId` | `string` | Copy request ID button |
| `copied` | `string` | Copy button after copying |
| `requestIdCopied` | `string` | Copied status announcement |
| `details` | `string` | Technical details disclosure |
Usage:
```tsx
import { ErrorPanel } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Inline callout (`inline-callout`)
Alert-based in-content callout with a tone, title, body, action buttons or links and optional dismiss. Optional labels?: Partial overrides each built-in label. Explicit content props win over labels, then I18nProvider (en/es); no provider keeps English. Imported fixtures and machine data stay unchanged. See the Catalog labels live preview.
- Package: `@gntik-ai/blocks` · export `InlineCallout` · group feedback · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/feedback/InlineCallout/block.meta.ts
- Composes: Alert, Button, Link
- Preview: /preview.html?kind=block&id=inline-callout
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add inline-callout`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Callout title; explicit title prop wins, including null |
| `description` | `string` | Callout body; explicit description prop wins, including null |
| `upgrade` | `string` | Default primary action; explicit actions win |
| `viewUsage` | `string` | Default usage link; explicit actions win |
Usage:
```tsx
import { InlineCallout } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Skeletons (`skeletons`)
Loading placeholders for a whole page, a table and a list; each announces loading once via role="status".
- Package: `@gntik-ai/blocks` · export `TableSkeleton` · group feedback · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/feedback/Skeletons/block.meta.ts
- Composes: Skeleton
- Preview: /preview.html?kind=block&id=skeletons
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add skeletons`
Usage:
```tsx
import { TableSkeleton } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### System banner (`system-banner`)
Full-width, dismissible banner for maintenance windows, incidents and system-wide notices.
- Package: `@gntik-ai/blocks` · export `SystemBanner` · group feedback · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/feedback/SystemBanner/block.meta.ts
- Composes: Button, Link
- Preview: /preview.html?kind=block&id=system-banner
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add system-banner`
Usage:
```tsx
import { SystemBanner } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Create API key dialog (`create-api-key-dialog`)
Two-step dialog: name, scopes and expiry, then the secret shown once in a copyable CodeBlock with a warning. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=create-api-key-dialog--labels.
- Package: `@gntik-ai/blocks` · export `CreateApiKeyDialog` · group forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/forms/CreateApiKeyDialog/block.meta.ts
- Composes: Dialog, Field, Input, CheckboxGroup, Checkbox, Select, CodeBlock, Alert, Button
- Preview: /preview.html?kind=block&id=create-api-key-dialog
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add create-api-key-dialog`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Default content and accessible names |
| `description` | `string` | Default content and accessible names |
| `name` | `string` | Default content and accessible names |
| `namePlaceholder` | `string` | Default content and accessible names |
| `nameRequired` | `string` | Validation feedback |
| `scopes` | `string` | Default content and accessible names |
| `scopesRequired` | `string` | Validation feedback |
| `expiration` | `string` | Default content and accessible names |
| `errorTitle` | `string` | Validation feedback |
| `fallbackError` | `string` | Validation feedback |
| `cancel` | `string` | Default content and accessible names |
| `submit` | `string` | Default content and accessible names |
| `successTitle` | `string` | Async feedback |
| `successDescription` | `string` | Async feedback |
| `warningTitle` | `string` | Default content and accessible names |
| `warningDescription` | `string` | Default content and accessible names |
| `secretLabel` | `string` | Default content and accessible names |
| `done` | `string` | Async feedback |
Usage:
```tsx
import { CreateApiKeyDialog } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Credential disclosure dialog (`credential-disclosure-dialog`)
Controlled disclosure for an existing credential. Set disclosure ({variant: stored | fresh, credentialId, expiresAt: caller-formatted ReactNode, secret}) only after a successful response; null renders nothing while pending or on failure. Clear disclosure, additionalSecrets and secret-bearing revealContent in onClose to drop every secret; a fresh secret requires a new successful response before showing it again. No pending/error state or internal secret retention. Stored values start masked; each null-to-value opening or changed credentialId/secret resets masking and copy feedback. Fresh values use a non-copyable CodeBlock with a kit CopyButton and a replacement warning, or a masked SecretField when maskFresh is set at session start. Optional additionalSecrets ({id, label, value}) are always masked and use independent toggles and serialized copy actions; ids must be non-empty, unique and not secret, with the first duplicate winning. Multi-secret groups announce label plus copiedLabel, or copiedAnnouncementFor(label); onCopy(id) never receives a value. Every secret and the optional revealContent slot have data-kit-redact. The session key includes additional ids and values. presentation is dialog by default or inline, fixed until the next opening: inline is a named section, never traps focus or handles Escape, and its warning is a non-live note. Surviving focus stays; removing the focused slot or session control focuses the popup or inline heading. Focused inline unmounts return focus through the same resolvers. Dismissed retained props stay hidden; clear and re-set or change a session value to reopen. Optional English defaults through en/es i18n for title, description, credentialIdLabel, expiryLabel, secretLabel, showLabel, hideLabel, copyLabel, copiedLabel, successAnnouncement, failureAnnouncement, warningTitle, warningText and closeLabel. Copy results are announced politely, including manual-selection fallback. Popup initial focus and focus trapping use Base UI; at close, resolveReturnFocus() is validated for connection, visibility, enabled state and focusability, then fallbackFocus(), then the original opener. Synthetic fixtures only; the default Stored preview is masked and the Fresh preview starts closed. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=credential-disclosure-dialog--labels.
- Package: `@gntik-ai/blocks` · export `CredentialDisclosureDialog` · group forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/forms/CredentialDisclosureDialog/block.meta.ts
- Composes: Dialog, SecretField, CodeBlock, CopyButton, Alert, LiveAnnouncer, Button
- Preview: /preview.html?kind=block&id=credential-disclosure-dialog
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add credential-disclosure-dialog`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Default content and accessible names |
| `description` | `string` | Default content and accessible names |
| `credentialIdLabel` | `string` | Default content and accessible names |
| `expiryLabel` | `string` | Default content and accessible names |
| `secretLabel` | `string` | Default content and accessible names |
| `showLabel` | `string` | Default content and accessible names |
| `hideLabel` | `string` | Default content and accessible names |
| `copyLabel` | `string` | Default content and accessible names |
| `copiedLabel` | `string` | Async feedback |
| `successAnnouncement` | `string` | Async feedback |
| `failureAnnouncement` | `string` | Async feedback |
| `warningTitle` | `string` | Default content and accessible names |
| `warningText` | `string` | Default content and accessible names |
| `closeLabel` | `string` | Default content and accessible names |
| `copiedAnnouncementFor` | `(label: string, copied: string) => string` | Async feedback |
Usage:
```tsx
import { Button } from '@gntik-ai/ui';
import { useRef, useState } from 'react';
import { CredentialDisclosureDialog, type CredentialDisclosure } from '@gntik-ai/blocks';
import { freshDisclosure } from '@gntik-ai/blocks';
/** Closed on load: no preview or visual baseline exposes the synthetic fresh value. */
export default function Fresh() {
const [disclosure, setDisclosure] = useState(null);
const heading = useRef(null);
const trigger = useRef(null);
return (
<>
Credentials
{
void Promise.resolve({ ...freshDisclosure }).then(setDisclosure);
}}
>
Disclose fresh secret
setDisclosure(null)}
resolveReturnFocus={() => trigger.current}
fallbackFocus={() => heading.current}
/>
>
);
}
```
### Danger zone (`danger-zone`)
Bordered destructive section whose actions open an alert dialog that can require typing the resource name. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=danger-zone--labels.
- Package: `@gntik-ai/blocks` · export `DangerZone` · group forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/forms/DangerZone/block.meta.ts
- Composes: Button, AlertDialog, Field, Input, Alert
- Preview: /preview.html?kind=block&id=danger-zone
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add danger-zone`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Default content and accessible names |
| `description` | `string` | Default content and accessible names |
| `confirmationTitle` | `(title: string) => string` | Destructive confirmation dialog |
| `confirmPrompt` | `(text: ReactNode) => ReactNode` | Destructive confirmation dialog |
| `cancel` | `string` | Destructive confirmation dialog |
| `fallbackError` | `string` | Destructive confirmation dialog |
Usage:
```tsx
import { DangerZone } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Date range filter (`date-range-filter`)
Compact date range picker for toolbars: small trigger with short dates, presets and a clear button. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=date-range-filter--labels.
- Package: `@gntik-ai/blocks` · export `DateRangeFilter` · group forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/forms/DateRangeFilter/block.meta.ts
- Composes: DateRangePicker, IconButton
- Preview: /preview.html?kind=block&id=date-range-filter
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add date-range-filter`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `label` | `string` | Default content and accessible names |
| `placeholder` | `string` | Default content and accessible names |
| `clear` | `(label: string) => string` | Default content and accessible names |
Usage:
```tsx
import { DateRangeFilter } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Dialog wizard (`dialog-wizard`)
Modal variant of create-wizard for create flows over their parent page, without a route change. Defaults to dialog size (fullscreen on mobile); size="fullscreen" fills every viewport. Consumer-controlled step state survives closing. Use the create-wizard page template for a dedicated page flow, or WizardLayout mode="overlay" for a fullscreen flow. Supply all labels, pendingLabel and stepAnnouncement from your own i18n; stepError accepts ValidationSummary and reviewStep accepts DescriptionListCard. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=dialog-wizard--labels.
- Package: `@gntik-ai/blocks` · export `DialogWizard` · group forms · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/forms/DialogWizard/block.meta.ts
- Composes: WizardLayout, Dialog, Stepper, Button
- Preview: /preview.html?kind=block&id=dialog-wizard
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add dialog-wizard`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Default content and accessible names |
| `details` | `string` | Default content and accessible names |
| `review` | `string` | Default content and accessible names |
Usage:
```tsx
import { DialogWizard } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### File upload panel (`file-upload-panel`)
Drag-and-drop zone with a Browse button, type and size validation, and a file list with progress, retry and remove. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=file-upload-panel--labels.
- Package: `@gntik-ai/blocks` · export `FileUploadPanel` · group forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/forms/FileUploadPanel/block.meta.ts
- Composes: Button, IconButton, Progress
- Preview: /preview.html?kind=block&id=file-upload-panel
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add file-upload-panel`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `label` | `string` | Default content and accessible names |
| `drop` | `string` | Default content and accessible names |
| `browse` | `string` | Default content and accessible names |
| `files` | `string` | Default content and accessible names |
| `failed` | `string` | Default content and accessible names |
| `typeRejected` | `string` | Default content and accessible names |
| `hint` | `(types: string, size: string) => string` | Default content and accessible names |
| `tooLarge` | `(size: string) => string` | Default content and accessible names |
| `done` | `(name: string) => string` | Async feedback |
| `error` | `(name: string) => string` | Validation feedback |
| `added` | `(count: number) => string` | Default content and accessible names |
| `rejected` | `(count: number) => string` | Default content and accessible names |
| `removed` | `(name: string) => string` | Default content and accessible names |
| `uploading` | `(name: string) => string` | Async feedback |
| `notUploaded` | `(error: string) => string` | Async feedback |
| `uploaded` | `(size: string) => string` | Async feedback |
| `retry` | `(name: string) => string` | Default content and accessible names |
| `remove` | `(name: string) => string` | Default content and accessible names |
Usage:
```tsx
import { FileUploadPanel } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Form section (`form-section`)
Two-column settings section: title and help on the left, fields on the right; stacks on small screens. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=form-section--labels.
- Package: `@gntik-ai/blocks` · export `FormSection` · group forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/forms/FormSection/block.meta.ts
- Composes: Field, Input, Textarea
- Preview: /preview.html?kind=block&id=form-section
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add form-section`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Default content and accessible names |
| `description` | `string` | Default content and accessible names |
Usage:
```tsx
import { FormSection } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Invite members dialog (`invite-members-dialog`)
Dialog to invite people by email (validated chips) with a role and an optional message; submits with a loading state. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=invite-members-dialog--labels.
- Package: `@gntik-ai/blocks` · export `InviteMembersDialog` · group forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/forms/InviteMembersDialog/block.meta.ts
- Composes: Dialog, TokenInput, Select, Textarea, Field, Button, Alert
- Preview: /preview.html?kind=block&id=invite-members-dialog
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add invite-members-dialog`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Default content and accessible names |
| `description` | `string` | Default content and accessible names |
| `emails` | `string` | Default content and accessible names |
| `placeholder` | `string` | Default content and accessible names |
| `listLabel` | `string` | Default content and accessible names |
| `role` | `string` | Default content and accessible names |
| `message` | `string` | Default content and accessible names |
| `messagePlaceholder` | `string` | Default content and accessible names |
| `emailRequired` | `string` | Validation feedback |
| `emailInvalid` | `(email: string) => string` | Validation feedback |
| `fixInvalid` | `(count: number) => string` | Validation feedback |
| `errorTitle` | `string` | Validation feedback |
| `fallbackError` | `string` | Validation feedback |
| `cancel` | `string` | Default content and accessible names |
| `submit` | `string` | Default content and accessible names |
| `submitMany` | `(count: number) => string` | Default content and accessible names |
Usage:
```tsx
import { InviteMembersDialog } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Settings row (`settings-row`)
A setting with its label and description on the left and a control (Switch by default) on the right. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=settings-row--labels.
- Package: `@gntik-ai/blocks` · export `SettingsRow` · group forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/forms/SettingsRow/block.meta.ts
- Composes: Switch
- Preview: /preview.html?kind=block&id=settings-row
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add settings-row`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `label` | `string` | Default content and accessible names |
| `description` | `string` | Default content and accessible names |
Usage:
```tsx
import { SettingsRow } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Validation summary (`validation-summary`)
Error summary that lists every problem with a link to its field and takes focus after a failed submit. Optional labels?: Partial overrides built-in copy; explicit props and validation messages win, then labels, then I18nProvider (en/es). No provider preserves English; fixtures and caller content stay unchanged. Interactive preview: /preview.html?kind=block&id=validation-summary--labels.
- Package: `@gntik-ai/blocks` · export `ValidationSummary` · group forms · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/forms/ValidationSummary/block.meta.ts
- Composes: Alert, Link
- Preview: /preview.html?kind=block&id=validation-summary
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add validation-summary`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `(count: number) => string` | Default content and accessible names |
Usage:
```tsx
import { ValidationSummary } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Comparison table (`comparison-table`)
Plans or options as columns and feature rows grouped by section (one tbody per section with a rowgroup header). Values are booleans (check or dash with screen-reader text), text or numbers; an optional recommended column is marked with a text badge, a primary top rule and a tint. Scrolls sideways on narrow screens with a sticky feature column.
- Package: `@gntik-ai/blocks` · export `ComparisonTable` · group marketing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/marketing/ComparisonTable/block.meta.ts
- Composes: Table, Badge, Button
- Preview: /preview.html?kind=block&id=comparison-table
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add comparison-table`
Usage:
```tsx
import { ComparisonTable } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### FAQ (`faq`)
Frequently asked questions in an accordion with a heading block, stacked or split layout, single or multiple open items.
- Package: `@gntik-ai/blocks` · export `Faq` · group marketing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/marketing/Faq/block.meta.ts
- Composes: Accordion, AccordionItem, AccordionTrigger, AccordionPanel
- Preview: /preview.html?kind=block&id=faq
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add faq`
Usage:
```tsx
import { Faq } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Feature grid (`feature-grid`)
Features section: eyebrow, title and description over a responsive grid of icon tile, title and description.
- Package: `@gntik-ai/blocks` · export `FeatureGrid` · group marketing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/marketing/FeatureGrid/block.meta.ts
- Composes: Icon (lucide)
- Preview: /preview.html?kind=block&id=feature-grid
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add feature-grid`
Usage:
```tsx
import { FeatureGrid } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Hero (`hero`)
Landing hero in centered or split layout: eyebrow badge, title, description, primary/secondary calls to action and a screenshot slot with a neutral placeholder.
- Package: `@gntik-ai/blocks` · export `Hero` · group marketing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/marketing/Hero/block.meta.ts
- Composes: Badge, Button, buttonVariants
- Preview: /preview.html?kind=block&id=hero
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add hero`
Usage:
```tsx
import { Hero } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Pricing table (`pricing-table`)
Pricing tiers with a monthly/annual toggle, a highlighted tier (primary border, no fill), price per cycle, CTA and feature checklist.
- Package: `@gntik-ai/blocks` · export `PricingTable` · group marketing · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/marketing/PricingTable/block.meta.ts
- Composes: ToggleGroup, Toggle, Badge, Button, RadioGroup, Radio
- Preview: /preview.html?kind=block&id=pricing-table
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add pricing-table`
Usage:
```tsx
import { PricingTable } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Bulk action bar (`bulk-action-bar`)
Toolbar that appears while table rows are selected: selection count, bulk actions and clear selection.
- Package: `@gntik-ai/blocks` · export `BulkActionBar` · group page-chrome · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/page-chrome/BulkActionBar/block.meta.ts
- Composes: Toolbar, ToolbarButton
- Preview: /preview.html?kind=block&id=bulk-action-bar
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add bulk-action-bar`
Usage:
```tsx
import { BulkActionBar } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Page header (`page-header`)
Screen header with breadcrumb, title and status, description, meta row, actions that fold into a MoreMenu on small screens, and optional navigation tabs. titleRef targets the h1 for titleRef.current.focus(), adding tabIndex=-1 only when supplied so the title stays outside the Tab order. In a StatusLayout heading slot use as="div" and disable breadcrumb, meta, tabs, actions, status and description fixtures; the slot supplies the page’s only h1. The tablist name follows I18nProvider (en/es); the Spanish preview keeps caller content unchanged. Optional labels?: Partial overrides each built-in label. Explicit content props win over labels, then I18nProvider (en/es); no provider keeps English. Imported fixtures and machine data stay unchanged. See the Catalog labels live preview. wrapTitle?: boolean (default false) wraps the full title, including long words, while preserving titleRef focus behavior. The default title remains truncated. See the Spanish live preview for a wrapping title.
- Package: `@gntik-ai/blocks` · export `PageHeader` · group page-chrome · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/page-chrome/PageHeader/block.meta.ts
- Composes: Breadcrumbs, Button, MoreMenu, StatusTag, Tabs
- Preview: /preview.html?kind=block&id=page-header
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add page-header`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `title` | `string` | Heading; explicit title prop wins, including null |
| `description` | `string` | Description; explicit description prop wins, including null |
| `moreActions` | `string` | Compact actions menu name |
| `sections` | `string` | Tablist name |
Usage:
```tsx
import { PageHeader } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Page toolbar (`page-toolbar`)
Toolbar above a list or table: search, a filters slot, a view toggle and the primary action, with roving focus.
- Package: `@gntik-ai/blocks` · export `PageToolbar` · group page-chrome · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/page-chrome/PageToolbar/block.meta.ts
- Composes: Toolbar, ToolbarInput, ToolbarButton, ToggleGroup, Toggle
- Preview: /preview.html?kind=block&id=page-toolbar
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add page-toolbar`
Usage:
```tsx
import { PageToolbar } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Section header (`section-header`)
Heading for a section within a page: title with optional count, description and small actions.
- Package: `@gntik-ai/blocks` · export `SectionHeader` · group page-chrome · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/page-chrome/SectionHeader/block.meta.ts
- Composes: Button
- Preview: /preview.html?kind=block&id=section-header
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add section-header`
Usage:
```tsx
import { SectionHeader } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Sticky action bar (`sticky-action-bar`)
Sticky bottom bar for long forms with an unsaved-changes status, Discard and Save (with saving state).
- Package: `@gntik-ai/blocks` · export `StickyActionBar` · group page-chrome · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/page-chrome/StickyActionBar/block.meta.ts
- Composes: Button
- Preview: /preview.html?kind=block&id=sticky-action-bar
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add sticky-action-bar`
Usage:
```tsx
import { StickyActionBar } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Agenda list (`agenda-list`)
Upcoming scheduled work grouped by day (Today/Tomorrow labels), each row with a kind tile, locale-formatted time range, location and kind badge; empty state included. hourCycle overrides the provider/runtime clock. timeZone defaults to I18nProvider (system when unset); TimeZone examples demonstrate UTC, an explicit New York override and a Spanish Madrid provider. Use the same zone on server and client. Locale examples show en-US, es-ES, then en-US with the 24-hour override.
- Package: `@gntik-ai/blocks` · export `AgendaList` · group scheduling · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/scheduling/AgendaList/block.meta.ts
- Composes: Badge, EmptyState
- Preview: /preview.html?kind=block&id=agenda-list
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add agenda-list`
Usage:
```tsx
import { AgendaList } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Month calendar (`month-calendar`)
Locale-first month grid of scheduled work with per-day event lines, today/selected states, month navigation and full grid keyboard support. weekStartsOn and hourCycle override provider/runtime defaults; data-weekend marks locale weekends without styling. timeZone defaults to I18nProvider (system when unset); TimeZone examples demonstrate UTC, an explicit New York override and a Spanish Madrid provider. Use the same zone on server and client. Locale examples show en-US, es-ES, then en-US with Monday-first and 24-hour overrides.
- Package: `@gntik-ai/blocks` · export `MonthCalendar` · group scheduling · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/scheduling/MonthCalendar/block.meta.ts
- Composes: Button, IconButton
- Preview: /preview.html?kind=block&id=month-calendar
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add month-calendar`
Usage:
```tsx
import { MonthCalendar } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Time slot picker (`time-slot-picker`)
Grid of selectable time slots with native radio semantics (arrow keys, form name), taken slots struck through and disabled, controlled or uncontrolled.
- Package: `@gntik-ai/blocks` · export `TimeSlotPicker` · group scheduling · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/scheduling/TimeSlotPicker/block.meta.ts
- Preview: /preview.html?kind=block&id=time-slot-picker
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add time-slot-picker`
Usage:
```tsx
import { TimeSlotPicker } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Week calendar (`week-calendar`)
Locale-first seven-day time grid with positioned events (overlaps side by side), a now line, week navigation and optional clickable events. weekStartsOn and hourCycle override provider/runtime defaults; data-weekend marks locale weekends without styling. timeZone defaults to I18nProvider (system when unset); TimeZone examples demonstrate UTC, an explicit New York override and a Spanish Madrid provider. Use the same zone on server and client. Locale examples show en-US, es-ES, then en-US with Monday-first and 24-hour overrides.
- Package: `@gntik-ai/blocks` · export `WeekCalendar` · group scheduling · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/scheduling/WeekCalendar/block.meta.ts
- Composes: Button, IconButton
- Preview: /preview.html?kind=block&id=week-calendar
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add week-calendar`
Usage:
```tsx
import { WeekCalendar } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### App sidebar (`app-sidebar`)
Application sidebar: workspace switcher (or logo) header, NavList body with icon-rail support, and a footer with plan usage, help link and user menu. Exposes header / nav / footer parts for the SidebarLayout slots.
- Package: `@gntik-ai/blocks` · export `AppSidebar` · group shell · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/shell/AppSidebar/block.meta.ts
- Composes: WorkspaceSwitcher, Logo, NavList, Meter, Link, UserMenu
- Preview: /preview.html?kind=block&id=app-sidebar
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add app-sidebar`
Usage:
```tsx
import { AppSidebar } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### App topbar (`app-topbar`)
Topbar contents for the application shell: breadcrumb and environment chip, ⌘K global search, notifications popover, theme control and user menu.
- Package: `@gntik-ai/blocks` · export `AppTopbar` · group shell · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/shell/AppTopbar/block.meta.ts
- Composes: Breadcrumbs, Badge, CommandPalette, CommandPaletteTrigger, NotificationsPopover, ThemeCycleButton, ThemeSwitcher, UserMenu
- Registry dependencies: app-sidebar, environment-badge, global-search
- Preview: /preview.html?kind=block&id=app-topbar
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add app-topbar`
Usage:
```tsx
import { AppTopbar } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Environment badge (`environment-badge`)
Environment + region chip ("production · eu-west") for the topbar or a page header; tone follows the environment.
- Package: `@gntik-ai/blocks` · export `EnvironmentBadge` · group shell · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/shell/EnvironmentBadge/block.meta.ts
- Composes: Badge
- Preview: /preview.html?kind=block&id=environment-badge
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add environment-badge`
Usage:
```tsx
import { EnvironmentBadge } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Global search (`global-search`)
⌘K search button opening a CommandPalette pre-wired with recent items and grouped results: pages, records and actions.
- Package: `@gntik-ai/blocks` · export `GlobalSearch` · group shell · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/shell/GlobalSearch/block.meta.ts
- Composes: CommandPalette, CommandPaletteTrigger, Kbd
- Preview: /preview.html?kind=block&id=global-search
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add global-search`
Usage:
```tsx
import { GlobalSearch } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Cursor pager footer (`cursor-pager-footer`)
Responsive stateless cursor-pagination footer for data-table paginated={false}. Forwards CursorPaginationProps: hasPrevious, hasNext, onPrevious, onNext, loading, busy, summary, shownCount, total, unavailableBehaviour, pageSize, pageSizeOptions, onPageSizeChange, previousLabel, nextLabel, ariaLabel, loadingLabel, labels and className. Consumer-owned opaque cursors stay outside the kit. summary accepts plain text or null; the count form requires labels.shownSummary and labels.totalSummary; select options require labels.rowsPerPage and labels.rowsPerPageLabel. Flat names override labels.previous, labels.next and labels.navLabel, then catalog defaults. loadingLabel=null preserves the summary while busy. One persistent polite atomic status; buttons remain focusable when aria-disabled. Removal of a focused control moves focus to Next, Previous, select, then nav root, including while busy. No page numbers or ranges. No props renders fixtures.
- Package: `@gntik-ai/blocks` · export `CursorPagerFooter` · group tables · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/tables/CursorPagerFooter/block.meta.ts
- Composes: CursorPagination
- Preview: /preview.html?kind=block&id=cursor-pager-footer
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add cursor-pager-footer`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `previous` | `string` | Previous name, below previousLabel and above catalog defaults |
| `next` | `string` | Next name, below nextLabel and above catalog defaults |
| `navLabel` | `string` | Navigation name, below ariaLabel and above catalog defaults |
| `shownSummary` | `(shownCount: number) => string` | Required in count form: no positive exact total |
| `totalSummary` | `(total: number) => string` | Required in count form: positive exact total |
| `rowsPerPage` | `string` | Required with pageSizeOptions: visible select label |
| `rowsPerPageLabel` | `string` | Required with pageSizeOptions: select accessible name |
Usage:
```tsx
import { CursorPagerFooter } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Data table (`data-table`)
Generic typed table: sortable headers with aria-sort, row selection with select-all and indeterminate by default (selectionMode="multiple"), selectionMode="none" to hide selection, column visibility menu, density toggle, row actions, loading and empty states, client-side sort and pagination, optional row groups (groupBy) with rowgroup header rows, counts, keyboard-accessible collapse buttons and group selection; column resize (pointer drag or a focusable role="separator" handle with arrow keys, min/max widths, controlled columnWidths), column pinning to either edge with sticky cells (column menu or pinnedColumns), row virtualization for large datasets (aria-rowcount/aria-rowindex, works with selection and grouping), inline cell editing (text, number and select editors; Enter/F2 to edit, Enter/Tab to commit, Escape to cancel, onCellEdit can reject with an inline error) and saved views (filters, sort, visible columns, widths, pinning and grouping) with a view switcher to save as, rename, delete and reset. Opt-in selectionMode="single" uses native radios in a labelled fieldset (never virtualized; client pagination defaults to false, and paginated=true or paginationMode="server" enables pagination). Single mode uses selectedId/onSelectedIdChange or defaultSelectedId; isRowDisabled skips unavailable rows and preserves a checked disabled choice without callbacks. Supply legend, aria-label or aria-labelledby for the group and getRowLabel for each radio (otherwise the first column labels it). selectionName is used verbatim and must be unique per page; omitted names use an instance-unique useId name, and duplicate supplied names or missing group labels warn in development. Tab enters once on the checked enabled row or first enabled row; arrows wrap and skip disabled rows, Space selects, Shift+Tab exits, and all-disabled groups have no tab stop. No select-all, group checkboxes, bulk actions or selection summary appear in single mode. Examples in examples/single-selection.tsx: an uncontrolled non-paginated list with legend="Available items" and a controlled versions table with legend="Available versions", selectedId="v2" and a checked disabled version. Opt-in paginationMode="server" accepts a controlled 1-based page, pageSize and required rowCount (known total), with onPageChange/onPageSizeChange requests. Supplied rows render in order without client sorting or slicing, and headers have no sort affordance. Single selection keeps its chosen id across pages. Off-page choices leave every visible radio unchecked and the first enabled radio is the tab stop; name the current choice in the legend. See examples/paginated-single-selection.tsx for client and server pagination. The footer stays visible for empty results, clamps stale pages for display without callbacks, and hides during loading. Page-size changes may also request a page to keep the first visible row on screen. The consumer owns fetching and reconciles stale pages. See examples/server-pagination.tsx for an items/total/page/pageSize envelope consumer that handles both callbacks. Multiple selection preserves ids from other server pages by default; preserveSelection=false restores page-only reporting, and client tables can opt in with preserveSelection=true. selectedIds remains supported. A controlled selectionModel takes precedence and uses { type: "include", ids: string[] } for selected ids or { type: "exclude", ids: string[] } for every match except those ids. bulkActions(ids, model) receives the full include list; in exclude mode ids contains enabled selections in the current rows and the model is authoritative. One-argument actions remain compatible. selectAllMatching defaults to false and offers a strip after the page is fully selected: client mode includes every enabled row, while server mode requires onSelectionModelChange and reports an exclude model. The same button becomes Clear selection without moving focus; clearing returns focus to the page checkbox, or to the table when the checkbox is hidden or unavailable. An exclude model resets through both callbacks when filters changes by JSON value; filters must include every narrowing input, including search text. Paging, page size and sorting preserve selection. An exclude action covers the matching result set at action time, including later-created rows; the consumer executes it. isRowDisabled disables multiple-selection checkboxes and page/group/all-matching toggles skip known disabled rows. Server counts use rowCount minus explicit excluded ids, including disabled ids on the page where Select all is activated. isRowDisabled cannot evaluate unloaded rows, so the server must enforce eligibility for the complete matching result set; return only eligible matches and their rowCount when counts must exclude all unavailable rows. hideSelectAll defaults to false and hides just the header checkbox, retaining an accessible Selection heading. See examples/client-selection.tsx for client preservation, disabled rows and select-all-matching, and examples/server-pagination.tsx for controlled models, cross-page actions and the preservation/header options.
- Package: `@gntik-ai/blocks` · export `DataTable` · group tables · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/tables/DataTable/block.meta.ts
- Composes: Table, Checkbox, Menu, MoreMenu, ToggleGroup, Button, Skeleton, EmptyState, Pagination, SimpleSelect, StatusTag, Input, Dialog
- Registry dependencies: pagination-footer
- Preview: /preview.html?kind=block&id=data-table
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add data-table`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `booleanTrue` | `string` | Default cell text for a true boolean value |
| `booleanFalse` | `string` | Default cell text for a false boolean value |
| `scrollRegion` | `string` | Non-virtual table overflow: focusable scroll region name; virtualized regions use the caller caption |
| `selectionCount` | `(count: number) => string` | Multiple selection: toolbar count and polite live region |
| `pageSelected` | `(count: number) => string` | Matching-selection strip: selected page announcement |
| `selectAllMatching` | `(count: number) => string` | Matching-selection strip: select every enabled match |
| `allMatchingSelected` | `(count: number) => string` | Matching-selection strip: all-matching announcement |
| `clearSelection` | `string` | Matching-selection strip: clear action |
| `columns` | `string` | Column visibility trigger text and name |
| `visibleColumns` | `string` | Open column menu: visibility heading |
| `pinColumns` | `string` | Open column menu with pinning: pin heading |
| `pinnedLeft` | `string` | Open pin submenu trigger: left side suffix |
| `pinnedRight` | `string` | Open pin submenu trigger: right side suffix |
| `pinLeft` | `string` | Open pin submenu: left option |
| `pinRight` | `string` | Open pin submenu: right option |
| `unpinned` | `string` | Open pin submenu: unpinned option |
| `density` | `string` | Density group accessible name |
| `comfortable` | `string` | Comfortable density button name |
| `compact` | `string` | Compact density button name |
| `resizeColumn` | `(label: string) => string` | Resizable header: separator name; label is caller column header |
| `columnWidth` | `(width: number) => string` | Resizable header: separator aria-valuetext in pixels |
| `defaultView` | `string` | View trigger when no active view |
| `defaultViewOption` | `string` | Open views menu: default option |
| `modified` | `string` | Dirty view trigger: screen reader suffix |
| `views` | `string` | Open views menu: heading |
| `saveChanges` | `string` | Open views menu: save current view action |
| `saveAs` | `string` | Open views menu: save as action |
| `renameView` | `string` | Open views menu: rename action |
| `deleteView` | `string` | Open views menu: immediate delete action; no confirmation dialog |
| `resetChanges` | `string` | Open views menu: reset action |
| `saveViewTitle` | `string` | Save as dialog: title and accessible name |
| `renameViewTitle` | `string` | Rename dialog: title and accessible name |
| `viewName` | `string` | Save as and Rename dialogs: input label |
| `cancel` | `string` | Save as and Rename dialogs: cancel button |
| `saveView` | `string` | Save as dialog: submit button |
| `rename` | `string` | Rename dialog: submit button |
| `closeDialog` | `string` | Save as and Rename dialogs: close button name |
| `selection` | `string` | Single selection or hideSelectAll: screen reader column heading |
| `selectAll` | `string` | Multiple selection: header checkbox name |
| `selectRow` | `(label: string) => string` | Multiple selection: row checkbox name; label is caller row name |
| `selectGroup` | `(group: string) => string` | Grouped multiple selection: group checkbox name |
| `groupRows` | `(count: number) => string` | Grouped header: screen reader row-count unit; numeric count stays unchanged |
| `noGroup` | `string` | Grouped header: fallback when group key is empty |
| `actions` | `string` | Row actions present: screen reader column heading |
| `rowActions` | `(label: string) => string` | Row action menu trigger name; label is caller row name |
| `emptyTitle` | `string` | Empty rows: default empty-state title |
| `emptyDescription` | `string` | Empty rows: default empty-state description |
| `loading` | `(caption: string) => string` | Loading: status announcement; receives original caller caption |
| `editHint` | `string` | Editable table: shared keyboard instruction description |
| `editLabel` | `(column: string, row: string) => string` | Editing cell: input accessible name |
| `editError` | `string` | Rejected cell edit without caller error message: alert |
| `enterNumber` | `string` | Editing number: invalid numeric value alert |
| `minimumValue` | `(min: number) => string` | Editing number below minimum: alert |
| `maximumValue` | `(max: number) => string` | Editing number above maximum: alert |
| `rowsPerPage` | `string` | Always: visible page-size text |
| `rowsPerPageLabel` | `string` | Always: page-size Select accessible name |
| `rangeSummary` | `(from: number, to: number, total: number) => string` | Every page and page-size change: visible summary and polite live region |
| `paginationNav` | `(noun: string) => string` | Always: navigation accessible name; noun is the caller caption |
| `previous` | `string` | Always: previous-page button name |
| `next` | `string` | Always: next-page button name |
| `page` | `(page: number) => string` | Always: numbered-page button name; number stays caller content |
Usage:
```tsx
import { DataTable } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Filter bar (`filter-bar`)
Search Input, saved views Select, an Add filter Menu with multi-select per field, removable filter chips and Clear all.
- Package: `@gntik-ai/blocks` · export `FilterBar` · group tables · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/tables/FilterBar/block.meta.ts
- Composes: Input, SimpleSelect, Menu, MenuCheckboxItem, Token, Button
- Preview: /preview.html?kind=block&id=filter-bar
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add filter-bar`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `searchLandmark` | `string` | Always: search landmark name |
| `searchLabel` | `string` | Always: search input accessible name |
| `searchPlaceholder` | `string` | Always: search input placeholder |
| `savedViewLabel` | `string` | When views exist: Select accessible name |
| `savedViewPlaceholder` | `string` | When no view selected: Select placeholder |
| `addFilter` | `string` | Always: Add filter trigger text and name |
| `activeFilters` | `string` | When filters or search exist: active-filters list name |
| `filters` | `string` | When filters or search exist: chip section heading |
| `clearAll` | `string` | When filters or search exist: clear button text and name |
| `removeFilter` | `(field: string, value: string) => string` | When chip exists: remove button name; field and value are caller content |
| `resultCount` | `(count: number) => string` | When resultCount supplied: visible count and polite live region |
Usage:
```tsx
import { FilterBar } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Inline edit row (`inline-edit-row`)
A table row that switches into edit mode with inputs, Save and Cancel; Enter saves, Escape cancels and focus returns to Edit. InlineEditTable is a ready-made editable table.
- Package: `@gntik-ai/blocks` · export `InlineEditTable` · group tables · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/tables/InlineEditRow/block.meta.ts
- Composes: Table, Input, IconButton
- Preview: /preview.html?kind=block&id=inline-edit-row
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add inline-edit-row`
Usage:
```tsx
import { InlineEditTable } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Load more footer (`load-more-footer`)
A load more button and loaded-count summary for appended lists without a total or page count. Cursor mode uses nextCursor and optional hasMore; flag mode omits nextCursor and uses hasMore. Compose with DataTable paginated={false} or Collapsible and JsonViewer cards. The consumer owns rows, filters and cursor normalization. Each attempt must settle loading=false together with its result in one update; split settles are unsupported. Keep cursor, hasMore, counts and lastAppend unchanged on failure. Cursor failures replace the button with ErrorPanel; flag failures keep the same button. Exhaustion removes every control even on failure. Focus moves only when the focused control is removed, to its replacement or exhaustedFocusTarget (summary by default, root optionally). lastAppend uses increasing seq values for successful appends, never initial loads, replacements or failures. New appended counts are announced politely; settled errors assertively. Self-wraps in LiveAnnouncer when needed. Every label has translated defaults and per-key overrides.
- Package: `@gntik-ai/blocks` · export `LoadMoreFooter` · group tables · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/tables/LoadMoreFooter/block.meta.ts
- Composes: Button, ErrorPanel, LiveAnnouncer
- Registry dependencies: error-panel
- Preview: /preview.html?kind=block&id=load-more-footer
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add load-more-footer`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `loadMore` | `string` | Available and idle: button name |
| `loading` | `string` | Loading: button name; ErrorPanel uses its own translated busy text |
| `retry` | `string` | Failed and available: retry button name |
| `group` | `string` | Always: root group accessible name |
| `moreAvailable` | `string` | Available: visible summary note |
| `summary` | `(count: number) => string` | Always: loaded-count summary and default exhaustion focus target |
| `loadedAnnouncement` | `(count: number) => string` | New lastAppend.seq with positive count: polite announcement |
Usage:
```tsx
import { LoadMoreFooter } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Pagination footer (`pagination-footer`)
Table footer with a rows-per-page Select, a "1–10 of 97" summary and numbered Pagination; controlled or uncontrolled.
- Package: `@gntik-ai/blocks` · export `PaginationFooter` · group tables · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/tables/PaginationFooter/block.meta.ts
- Composes: SimpleSelect, Pagination
- Preview: /preview.html?kind=block&id=pagination-footer
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add pagination-footer`
Labels (optional Partial per instance; explicit keys override provider translations and defaults):
| Key | Type | State / surface |
| --- | --- | --- |
| `rowsPerPage` | `string` | Always: visible page-size text |
| `rowsPerPageLabel` | `string` | Always: page-size Select accessible name |
| `rangeSummary` | `(from: number, to: number, total: number) => string` | Every page and page-size change: visible summary and polite live region |
| `paginationNav` | `(noun: string) => string` | Always: navigation accessible name; noun is caller content or defaults to the provider translation of rows |
| `previous` | `string` | Always: previous-page button name |
| `next` | `string` | Always: next-page button name |
| `page` | `(page: number) => string` | Always: numbered-page button name; number stays caller content |
Usage:
```tsx
import { PaginationFooter } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Subordinate list (`subordinate-list`)
Read-only list section inside a resource-form host, composing section-header, data-table, pagination-footer, empty-states, skeletons and error-panel. All props are required: title (string), headingLevel (h2 or h3), count (number, including 0), columns (typed DataTableColumn[]), rows (readonly T[]), getRowId, pagination ({limit, offset, total, onPageChange(offset)} or null), loading (boolean), error ({title, message, code: string | number | null} or null), onRetry and emptyState (host-supplied ReactNode). Every supplied row renders; server pagination converts the zero-based offset to a 1-based footer page and fixes pageSizeOptions to [limit]. No filtering, sorting, selection, create action, toolbar, virtualization, pinning or sticky region. Error takes precedence over loading, then empty and data; retry uses the current i18n locale. Error headings use h3 for either section heading level. Composition example: packages/blocks/src/tables/SubordinateList/examples/ResourceForm.tsx uses the resource-form form sections and sticky save bar with a server-paginated History section and an unpaginated Consumers section.
- Package: `@gntik-ai/blocks` · export `SubordinateList` · group tables · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/tables/SubordinateList/block.meta.ts
- Composes: section-header, data-table, pagination-footer, empty-states, skeletons, error-panel
- Registry dependencies: data-table, error-panel, pagination-footer, section-header, skeletons
- Preview: /preview.html?kind=block&id=subordinate-list
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add subordinate-list`
Usage:
```tsx
import { Field, FieldLabel, Input, Page, Stack } from '@gntik-ai/ui';
import { useState } from 'react';
import { FormSection } from '@gntik-ai/blocks';
import { NoResultsEmpty } from '@gntik-ai/blocks';
import { StickyActionBar } from '@gntik-ai/blocks';
import { SubordinateList } from '@gntik-ai/blocks';
import type { DataTableColumn } from '@gntik-ai/blocks';
interface RecordRow {
id: string;
name: string;
status: string;
}
const columns: DataTableColumn[] = [
{ id: 'name', header: 'Name', accessor: (row) => row.name },
{ id: 'status', header: 'Status', accessor: (row) => row.status },
];
const history: RecordRow[] = Array.from({ length: 37 }, (_, i) => ({
id: `record-${i}`,
name: `Revision ${i + 1}`,
status: 'Completed',
}));
const consumers: RecordRow[] = [
{ id: 'worker', name: 'Worker', status: 'Current' },
{ id: 'scheduler', name: 'Scheduler', status: 'Current' },
];
export type ExampleState = 'default' | 'loading' | 'empty' | 'error';
/** A resource-form host owns the form and sticky save bar; its two lists load independently. */
export default function ResourceFormExample({ state = 'default' }: { state?: ExampleState }) {
const [name, setName] = useState('Shared resource');
const [savedName, setSavedName] = useState(name);
const [offset, setOffset] = useState(0);
const [failed, setFailed] = useState(state === 'error');
return (
);
}
```
### Truncated collection footer (`truncated-collection-footer`)
Presentation-only summary of a loaded first cursor page without a total or pager. N+ is a lower bound when hasMore is true; the note explains that more items exist. Compose below data-table (DataTable with paginated={false}). First-page recipe: use hasMore={true} for N+ plus a note, and supply noteId so a consumer table can reference it with aria-describedby. DataTable does not forward aria-describedby; its named containing region can reference the note instead. Count-only recipe: use hasMore={false} for the visible count without a note. PageToolbar has no count prop; this block carries the count. No navigation or focusable elements. Every visible string is overridable through noun, summaryLabel, formatCount and truncationNote; ReactNode overrides must remain plain text without interactive or hidden content. The i18n provider supplies translated defaults; locale overrides number formatting. Required inputs are visibleCount and hasMore, with meaningful runtime defaults for the JavaScript catalogue preview.
- Package: `@gntik-ai/blocks` · export `TruncatedCollectionFooter` · group tables · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/tables/TruncatedCollectionFooter/block.meta.ts
- Preview: /preview.html?kind=block&id=truncated-collection-footer
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add truncated-collection-footer`
Usage:
```tsx
import { TruncatedCollectionFooter } from '@gntik-ai/blocks';
/** Count-only recipe: retain the visible count without introducing truncation messaging. */
export default function ExactExample() {
return ;
}
```
### Members table (`members-table`)
Workspace members with avatar, email, an inline role select, last activity and a row menu to remove.
- Package: `@gntik-ai/blocks` · export `MembersTable` · group team · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/team/MembersTable/block.meta.ts
- Composes: Table, Avatar, Badge, Select, Timestamp, MoreMenu
- Registry dependencies: invite-members-dialog
- Preview: /preview.html?kind=block&id=members-table
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add members-table`
Usage:
```tsx
import { MembersTable } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Notification matrix (`notification-matrix`)
Event × channel table of checkboxes with column and row toggles (mixed state) and always-on cells.
- Package: `@gntik-ai/blocks` · export `NotificationMatrix` · group team · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/team/NotificationMatrix/block.meta.ts
- Composes: Table, Checkbox
- Preview: /preview.html?kind=block&id=notification-matrix
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add notification-matrix`
Usage:
```tsx
import { NotificationMatrix } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Pending invitations (`pending-invitations`)
List of pending invitations with role, sender, age and expiry, and resend / revoke actions.
- Package: `@gntik-ai/blocks` · export `PendingInvitations` · group team · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/team/PendingInvitations/block.meta.ts
- Composes: Badge, StatusTag, Timestamp, Button, EmptyState
- Preview: /preview.html?kind=block&id=pending-invitations
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add pending-invitations`
Usage:
```tsx
import { PendingInvitations } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Profile header (`profile-header`)
Profile header with a large avatar and change action, name, role badge, headline, meta row and edit button.
- Package: `@gntik-ai/blocks` · export `ProfileHeader` · group team · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/team/ProfileHeader/block.meta.ts
- Composes: Avatar, Badge, IconButton, Button
- Preview: /preview.html?kind=block&id=profile-header
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add profile-header`
Usage:
```tsx
import { ProfileHeader } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
### Sessions and devices (`sessions-devices`)
Signed-in sessions with device, client, location, last seen and a current-device badge; revoke one or all others.
- Package: `@gntik-ai/blocks` · export `SessionsDevices` · group team · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/blocks/src/team/SessionsDevices/block.meta.ts
- Composes: Badge, Timestamp, Button
- Preview: /preview.html?kind=block&id=sessions-devices
- Install: import from `@gntik-ai/blocks`, or copy the source with `gntik-ui add sessions-devices`
Usage:
```tsx
import { SessionsDevices } from '@gntik-ai/blocks';
// Props default to the block fixtures; pass your data.
```
## Templates
### Assistant chat (`assistant-chat`)
Workspace assistant in the console: @gntik-ai/chat ChatLayout with suggestions, ChatMessage turns, a ToolCallCard, a mock-streamed markdown reply with Citations, Stop / Retry / feedback and a ChatComposer; a details panel lists the context sources. Streaming timers are cleared on Stop and unmount.
- Package: `@gntik-ai/templates` · export `AssistantChatPage` · group AI · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/assistant-chat/template.meta.ts
- Layout: SidebarLayout · Page · ChatLayout
- Blocks: app-sidebar, app-topbar, page-header
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=assistant-chat
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add assistant-chat`
Usage:
```tsx
import { AssistantChatPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Evaluations (`evaluations`)
Evaluation run review: EvaluationScorecard with one row per dataset, a diverging Heatmap of the score change vs baseline (metric × dataset), a regressions DataTable and a sample viewer (Drawer + JsonViewer, failures first) per dataset.
- Package: `@gntik-ai/templates` · export `EvaluationsPage` · group AI · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/evaluations/template.meta.ts
- Layout: SidebarLayout
- Blocks: app-sidebar, app-topbar, page-header, evaluation-scorecard, data-table
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=evaluations
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add evaluations`
Usage:
```tsx
import { EvaluationsPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Model providers settings (`model-providers`)
Settings page (shared settings nav plus a "Model providers" item) with the ModelKeysList — masked keys, test connection, confirmed revoke — an add-key dialog (provider, endpoint for compatible / self-hosted, secret) and workspace usage limits as Meters.
- Package: `@gntik-ai/templates` · export `ModelProvidersPage` · group AI · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/model-providers/template.meta.ts
- Layout: SettingsLayout
- Blocks: app-sidebar, app-topbar, page-header, model-keys-list
- Console shell: no
- Registry dependencies: settings-profile
- Preview: /preview.html?kind=template&id=model-providers
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add model-providers`
Usage:
```tsx
import { ModelProvidersPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Prompt playground (`prompt-playground`)
Prompt playground inside the console (CanvasLayout embedded in ConsoleShell, so the app nav stays): shared variables in a ListInput palette, the PromptEditor, and 2–3 model columns side by side with a Diff switch that compares each column against the first in a DiffEditor.
- Package: `@gntik-ai/templates` · export `PromptPlaygroundPage` · group AI · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/prompt-playground/template.meta.ts
- Layout: CanvasLayout
- Blocks: app-sidebar, app-topbar, prompt-editor
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=prompt-playground
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add prompt-playground`
Usage:
```tsx
import { PromptPlaygroundPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Run trace (`run-trace`)
Trace of one AI run inside the console: run header with a live elapsed Timer, TraceWaterfall of spans, token cost and the run log; a pinnable inspector shows the selected span with a live Timer while it runs and its input / output in JsonViewers.
- Package: `@gntik-ai/templates` · export `RunTracePage` · group AI · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/run-trace/template.meta.ts
- Layout: InspectorLayout
- Blocks: app-sidebar, app-topbar, page-header, trace-waterfall, token-cost-card, log-viewer
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=run-trace
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add run-trace`
Usage:
```tsx
import { RunTracePage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Accept invitation (`accept-invitation`)
Centred card invitation: inviter, workspace, role and member count, the invited email with a switch-account link, and Join / Decline with result states.
- Package: `@gntik-ai/templates` · export `AcceptInvitationPage` · group Auth · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/accept-invitation/template.meta.ts
- Layout: AuthLayout
- Console shell: no
- Preview: /preview.html?kind=template&id=accept-invitation
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add accept-invitation`
Usage:
```tsx
import { AcceptInvitationPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Forgot password (`forgot-password`)
Centred card recovery with default email, inbox feedback and resend. Supports identifier (type, autoComplete, label, help/helpId, placeholder, minLength/maxLength), messages (identifierRequired/identifierInvalid/identifierTooShort/passwordRequired), and React 19 ref.focus. The kit owns inline validation and focuses the first invalid field; consumers supply messages, not a second inline validator. Server errors use error + fieldInvalid + feedbackId; aria-describedby describes the form, while flagged inputs also retain their help and error ids. headingLevel (h1/h2/h3/null) suppresses the heading without losing the form name; aria-label/aria-labelledby override the default name. Forwards ForgotPasswordForm props unchanged, including ref.focus(identifier), title/description, sentTitle/sentDescription, sentStatus and resendActions (null hides sent copy or actions). suppressAllHeadings suppresses headings in request and sent states. footer accepts replacement content or null. Preview: forgot-password--username-or-email.
- Package: `@gntik-ai/templates` · export `ForgotPasswordPage` · group Auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/forgot-password/template.meta.ts
- Layout: AuthLayout
- Blocks: forgot-password-form
- Console shell: no
- Preview: /preview.html?kind=template&id=forgot-password
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add forgot-password`
Usage:
```tsx
import { ForgotPasswordPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Pending activation (`pending-activation`)
Centred card for registered accounts awaiting approval or activation: polite status, optional request reference, allowed actions and recovery links. Use instead of verify-email when activation requires approval rather than an email verification link.
- Package: `@gntik-ai/templates` · export `PendingActivationPage` · group Auth · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/pending-activation/template.meta.ts
- Layout: AuthLayout
- Blocks: inline-callout, error-panel
- Console shell: no
- Preview: /preview.html?kind=template&id=pending-activation
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add pending-activation`
Usage:
```tsx
import { PendingActivationPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Public hub (`public-hub`)
Unauthenticated entry hub on AuthLayout full-bleed with PageHeader, explanatory cards and navigation actions. Use public-hub instead of landing for an account entry point without a marketing hero or screenshot; use sign-in-card for a single sign-in form. Recommend 2 to 4 cards; any number is supported. Cards use the kit sm breakpoint: one column below sm, two from sm. ARIA: a section labelled by the text span inside the single PageHeader h1, h2 card headings and a ul/li card list. The title span owns the aria-labelledby target id in server HTML and after hydration; PageHeader has no heading-id prop. Loading replaces content with decorative kit Skeletons in an aria-busy section named by loadingLabel. Keyboard: Tab and Shift+Tab move through the primary, secondary and tertiary links in visual order; Enter follows href and invokes onClick, with visible focus and no trap. AuthLayout retains its skip link, named "Skip to main content", before the content in both states. The PageHeader title inherits truncation for long titles.
## Props
| Prop | Type | Default / behaviour |
| --- | --- | --- |
| eyebrow | string | Omitted; optional Badge before PageHeader |
| title | string | Welcome; PageHeader h1 |
| description | string | Omitted; PageHeader description |
| cards | { title: string; body: string }[] | Omitted; explanatory list with h2 headings |
| primaryAction | { label: string; href: string; onClick?: MouseEventHandler } | Sign in, #sign-in; Button rendered as an anchor |
| secondaryAction | Same action shape | Omitted; secondary Button rendered as an anchor |
| tertiaryLink | Same action shape | Omitted; one kit Link |
| footer | string | Omitted; Separator then supporting Text |
| loading | boolean | false; replaces header, cards and actions; hides eyebrow/footer copy |
| logo | ReactNode | Active preset logo from AuthLayout |
| loadingLabel | string | Loading; accessible name of the busy section |
Optional content has no default fixtures and creates no empty wrappers; an empty cards array also renders no list. Without footer there is no separator. Preview fixtures: public-hub--full, public-hub--minimal, public-hub--loading and public-hub--minimal-loading.
## Theme examples
Import ThemeProvider from @gntik-ai/ui and PublicHubPage from @gntik-ai/templates. Pass account routes and copy as props. Each theme supports the same full, minimal and loading composition.
```tsx
```
```tsx
```
```tsx
```
- Package: `@gntik-ai/templates` · export `PublicHubPage` · group Auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/public-hub/template.meta.ts
- ARIA pattern: Named section (aria-labelledby points to the title span inside the PageHeader h1, including in server HTML); h2 card headings in a ul/li list. Loading: aria-busy=true and aria-label=loadingLabel, with decorative Skeletons and no content links or headings.
- Layout: AuthLayout
- Blocks: page-header
- Console shell: no
- Preview: /preview.html?kind=template&id=public-hub
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add public-hub`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Move through primary, secondary and tertiary navigation links in reading order, after the AuthLayout skip link named "Skip to main content"; focus is visible and can leave the page. |
| Enter | Follow the focused anchor href and invoke its optional onClick handler. |
Usage:
```tsx
import { PublicHubPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Reset password (`reset-password`)
Centred card new-password step: password with a strength meter and confirmation, then a success state that continues to sign in.
- Package: `@gntik-ai/templates` · export `ResetPasswordPage` · group Auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/reset-password/template.meta.ts
- Layout: AuthLayout
- Blocks: reset-password-form
- Console shell: no
- Preview: /preview.html?kind=template&id=reset-password
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add reset-password`
Usage:
```tsx
import { ResetPasswordPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Sign in (`sign-in`)
Split sign-in: brand panel with a short value pitch, email and password form, forgot-password link and single sign-on options.
- Package: `@gntik-ai/templates` · export `SignInPage` · group Auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/sign-in/template.meta.ts
- Layout: AuthLayout
- Blocks: sign-in-form, sso-buttons
- Console shell: no
- Preview: /preview.html?kind=template&id=sign-in
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add sign-in`
Usage:
```tsx
import { SignInPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Sign in (card) (`sign-in-card`)
Centred card sign-in with default email and password, legal footer and no SSO. Supports identifier (type, autoComplete, label, help/helpId, placeholder, minLength/maxLength), messages (identifierRequired/identifierInvalid/identifierTooShort/passwordRequired), and React 19 ref.focus. The kit owns inline validation and focuses the first invalid field; consumers supply messages, not a second inline validator. Server errors use error + fieldInvalid + feedbackId; aria-describedby describes the form, while flagged inputs also retain their help and error ids. headingLevel (h1/h2/h3/null) suppresses the heading without losing the form name; aria-label/aria-labelledby override the default name. Forwards SignInForm props unchanged, including passwordHelp/passwordHelpId and ref.focus(identifier/password). Text submissions retain email and add identifier. suppressAllHeadings suppresses every internal heading. footer, sso and signUpPrompt accept replacements or null. Preview: sign-in-card--username.
- Package: `@gntik-ai/templates` · export `SignInCardPage` · group Auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/sign-in-card/template.meta.ts
- Layout: AuthLayout
- Blocks: sign-in-form
- Console shell: no
- Preview: /preview.html?kind=template&id=sign-in-card
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add sign-in-card`
Usage:
```tsx
import { SignInCardPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Sign up (`sign-up`)
Split sign-up: brand panel with the value pitch, name, work email and password with a strength meter, terms consent and single sign-on.
- Package: `@gntik-ai/templates` · export `SignUpPage` · group Auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/sign-up/template.meta.ts
- Layout: AuthLayout
- Blocks: sign-up-form, sso-buttons
- Console shell: no
- Registry dependencies: sign-in
- Preview: /preview.html?kind=template&id=sign-up
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add sign-up`
Usage:
```tsx
import { SignUpPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Single sign-on (`sso-login`)
Centred card SSO entry: work email lookup, then a "Redirecting to your identity provider" state with cancel and a password fallback.
- Package: `@gntik-ai/templates` · export `SsoLoginPage` · group Auth · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/sso-login/template.meta.ts
- Layout: AuthLayout
- Console shell: no
- Preview: /preview.html?kind=template&id=sso-login
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add sso-login`
Usage:
```tsx
import { SsoLoginPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Two-step verification (`mfa`)
Centred card second-factor step: one-time code that auto-submits, backup-code fallback, resend with countdown and a way back to sign in.
- Package: `@gntik-ai/templates` · export `MfaPage` · group Auth · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/mfa/template.meta.ts
- Layout: AuthLayout
- Blocks: mfa-challenge
- Console shell: no
- Preview: /preview.html?kind=template&id=mfa
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add mfa`
Usage:
```tsx
import { MfaPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Verify email (`verify-email`)
Centred card check-your-inbox step after sign-up: the address the link went to, resend with a countdown, change email and a support link.
- Package: `@gntik-ai/templates` · export `VerifyEmailPage` · group Auth · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/verify-email/template.meta.ts
- Layout: AuthLayout
- Console shell: no
- Preview: /preview.html?kind=template&id=verify-email
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add verify-email`
Usage:
```tsx
import { VerifyEmailPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Code editor (`code-editor`)
Full-screen code editor: CanvasLayout top bar (breadcrumb, title, status, branch, Save), a file TreeList with a context menu (open, copy path, close tab, delete) in the palette, and the CodePanel block (file tabs, Monaco editor or diff, problems list) as the canvas.
- Package: `@gntik-ai/templates` · export `CodeEditorPage` · group Builders · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/code-editor/template.meta.ts
- Layout: CanvasLayout
- Blocks: code-panel
- Console shell: no
- Preview: /preview.html?kind=template&id=code-editor
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add code-editor`
Usage:
```tsx
import { CodeEditorPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Data explorer (`data-explorer`)
Explore a large dataset inside the console: a PowerSearch (field filters, comparisons, free text) over a 10k-row DataTable with client sorting and pagination, a column chooser (TransferList in a Dialog: show, hide, reorder) and a row inspector (Drawer + JsonViewer).
- Package: `@gntik-ai/templates` · export `DataExplorerPage` · group Builders · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/data-explorer/template.meta.ts
- Layout: SidebarLayout
- Blocks: app-sidebar, app-topbar, page-header, data-table, empty-states
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=data-explorer
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add data-explorer`
Usage:
```tsx
import { DataExplorerPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Flow builder (`workflow-builder`)
onSave(graph) receives the graph on screen (controlled graph, latest reported edit, or initial starter graph), without selected, dragging or measured node fields. Zero-argument handlers remain valid. Optional graph and onGraphChange pass through to FlowBuilder. Committed edits show actionBar.unsaved, a resolved save shows actionBar.saved, and a rejected save retains unsaved status and releases loading. Edits during a pending save remain unsaved. examples/SaveGraph.tsx shows a controlled graph and onSave={(graph) => localStorage.setItem("workflow-builder-example", JSON.stringify(graph))}; hosts own persistence. Full-screen workflow editor with optional nodeTypes, onConnectEdge, onConnect, isValidConnection, onConnectionRejected, validationIssues, mode="run", nodeStatus and onNodeSelect passed to FlowBuilder; codePanel passes readOnly, markers, onValidityChange, onMonacoReady and loader to an optional CodePanel (branch, parallel, sequence and worker-backed YAML examples). The app must import '@gntik-ai/flow/styles.css' once for the canvas styles. Optional nodeTypes, onConnectEdge, onConnect, isValidConnection, onConnectionRejected, validationIssues, mode, nodeStatus and onNodeSelect pass to FlowBuilder unchanged (see its branch, parallel and sequence examples). mode="run" hides Save, Publish and the saved notice, and preserves focus and node selection. Optional codePanel renders a companion CodePanel and passes all its extensions: readOnly, markers, onValidityChange, onMonacoReady and loader; run mode forces its readOnly to true. For example, WorkflowBuilderPage mode="run" nodeStatus={{step:"running"}} onNodeSelect={openDetails}; codePanel={{files,problems:[],loader:yamlLoader,markers}} uses consumer-owned same-origin YAML workers and inline schemas.
- Package: `@gntik-ai/templates` · export `WorkflowBuilderPage` · group Builders · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/workflow-builder/template.meta.ts
- Layout: CanvasLayout
- Blocks: flow-builder, code-panel
- Console shell: no
- Preview: /preview.html?kind=template&id=workflow-builder
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add workflow-builder`
Usage:
```tsx
import { WorkflowBuilderPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Live event stream (`live-event-stream`)
Controlled subscription form inside FormSection, a visible StatusDot connection indicator, a quiet role=log stream with LogViewer windowing, search, follow-tail and copy, and a pinnable JSON inspector on InspectorLayout. No fetching or transport; no trace waterfall, token cost or timer.
## Props
LiveEventStreamPage accepts Partial; omitted props use generic fixtures.
| Prop | Type | Default / behavior |
| --- | --- | --- |
| title | string | Live event stream; PageHeader h1 |
| description | string | Subscribe to a source and inspect the events it emits. |
| form | ReactNode | Non-sensitive Source field inside FormSection; sensitive fields must use the kit PasswordInput |
| status | idle / connecting / live / error / stopped | idle; fully controlled |
| events | LiveEvent[] | Empty fixture; unique id, timestamp, type, optional summary and unknown payload; oldest first |
| maxEvents | number | 500; nonnegative integer; retains newest entries, evicts oldest; invalid values use 500 |
| eventTypes | string[] | created, updated, deleted; multiple ToggleGroup, initially all enabled; unlisted types remain visible |
| countAnnouncement | string | Omitted; optional count text throttled by the consumer, never inferred from events.length |
| onSubscribe | () => void | Subscribe callback in idle, stopped and error; disabled while connecting |
| onStop | () => void | Stop callback while live |
| onSelectEvent | (event: LiveEvent) => void | Mouse or keyboard selection callback |
| error | string | Generic subscription error; ErrorPanel shown when status=error, inside role=alert |
| validationMessage | string | Omitted; destructive InlineCallout |
| shell | Omit | ConsoleShell fixture defaults; explicit consumer navigation, workspace, user and topbar slots |
Status mapping: idle = neutral Not subscribed; connecting = info Connecting with pulse; live = success Live with pulse; error = destructive Error; stopped = warning Stopped. Subscribe is shown in idle/connecting/error/stopped and is disabled for connecting; live shows Stop. Idle has No events yet, live-empty has Listening for events, connecting replaces the log with decorative skeletons, and stopped keeps supplied buffered events visible.
## ARIA pattern
PageHeader h1 and labelled FormSection. One polite atomic live region announces connection status and explicit consumer-throttled count text only. LogViewer uses announceLines=false: role=log and aria-live=off; event additions are silent. Connecting uses an aria-busy section. Stream errors use ErrorPanel inside role=alert; validation uses InlineCallout. Inspect buttons have the name Inspect event and aria-pressed. InspectorLayout owns the complementary landmark on desktop, Drawer on small screens, docking, pinning and focus return. Pinning does not freeze the payload: subsequent selection updates it; eviction preserves the last inspected payload with the note This event is no longer in the buffer.
## Keyboard table
| Key | Behavior |
| --- | --- |
| Tab / Shift+Tab | Form fields, Subscribe/Stop, filters, search, Follow, row inspect/copy and panel controls |
| Enter / Space | Activate buttons; select an event; toggle Follow and pinning |
| Arrow keys | Navigate ToggleGroups and the JsonViewer tree |
| Escape | From unpinned panel or toggle, close and return focus to inspector toggle; pinned panel ignores Escape, Close panel closes it |
Follow pins the bottom, where the newest row is added. Scrolling up by more than one row turns Follow off and preserves the viewport offset; switching Follow on returns to the bottom.
## Themes and examples
Dark (default), light and high_contrast use only kit tokens. Connecting/live pulses stop under prefers-reduced-motion. State previews: live-event-stream--connecting, --live-empty, --live-with-events, --error and --stopped. Every state and the default template are covered by axe in all three themes.
- Package: `@gntik-ai/templates` · export `LiveEventStreamPage` · group Builders · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/live-event-stream/template.meta.ts
- ARIA pattern: PageHeader h1, labelled FormSection, one polite atomic connection live region, quiet role=log stream (aria-live=off), alert errors and complementary JSON inspector. Selected inspect buttons expose aria-pressed.
- Layout: InspectorLayout
- Blocks: app-sidebar, app-topbar, page-header, form-section, log-viewer, empty-states, skeletons, inline-callout, error-panel
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=live-event-stream
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add live-event-stream`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Navigate form, action, filters, search, Follow, row inspect/copy and panel controls. |
| Enter / Space | Activate buttons, select an event and toggle Follow or pinning. |
| Arrow keys | Navigate ToggleGroups and the JSON tree. |
| Escape | Close an unpinned inspector from panel or toggle and return focus to the toggle. Pinned panels ignore Escape. |
Usage:
```tsx
import { LiveEventStreamPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Changelog (`changelog`)
Release notes in the console: dated entries with version, tag badges and a “New” badge, a multi-select tag filter (ToggleGroup) with a live count, and a Subscribe action.
- Package: `@gntik-ai/templates` · export `ChangelogPage` · group Help · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/changelog/template.meta.ts
- Layout: SidebarLayout · Page
- Blocks: app-sidebar, app-topbar, page-header
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=changelog
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add changelog`
Usage:
```tsx
import { ChangelogPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Docs article (`docs-article`)
Documentation article on DocsLayout: left NavList, Breadcrumbs, scroll-spy Outline (Collapsible below lg), an underlined inline Link, a reference Table, CodeBlock, Blockquote, a tip callout, “Was this helpful?” feedback with an optional comment, and previous / next links.
- Package: `@gntik-ai/templates` · export `DocsArticlePage` · group Help · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/docs-article/template.meta.ts
- Layout: DocsLayout
- Blocks: inline-callout
- Console shell: no
- Preview: /preview.html?kind=template&id=docs-article
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add docs-article`
Usage:
```tsx
import { DocsArticlePage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Help home (`help-home`)
Help center home on DocsLayout: search with a live result count, category ClickableCards with article counts, popular articles with reading time, and a contact-support footer.
- Package: `@gntik-ai/templates` · export `HelpHomePage` · group Help · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/help-home/template.meta.ts
- Layout: DocsLayout
- Console shell: no
- Preview: /preview.html?kind=template&id=help-home
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add help-home`
Usage:
```tsx
import { HelpHomePage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Keyboard shortcuts (`keyboard-shortcuts`)
Searchable shortcut sheet grouped by area (KbdCombo, “G then P” sequences), as a console page and as a Dialog variant (KeyboardShortcutsDialog) for the ? shortcut.
- Package: `@gntik-ai/templates` · export `KeyboardShortcutsPage` · group Help · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/keyboard-shortcuts/template.meta.ts
- Layout: SidebarLayout · Page
- Blocks: app-sidebar, app-topbar, page-header
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=keyboard-shortcuts
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add keyboard-shortcuts`
Usage:
```tsx
import { KeyboardShortcutsPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Create workspace (`create-workspace`)
Workspace onboarding wizard: name with address preview, data region and plan as selectable cards, and inline team invitations with email chips and a role.
- Package: `@gntik-ai/templates` · export `CreateWorkspacePage` · group Onboarding · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/create-workspace/template.meta.ts
- Layout: WizardLayout
- Console shell: no
- Preview: /preview.html?kind=template&id=create-workspace
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add create-workspace`
Usage:
```tsx
import { CreateWorkspacePage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Getting started (`getting-started`)
Onboarding checklist in the console shell: setup progress, checkable steps with a completion message, and cards linking to the docs.
- Package: `@gntik-ai/templates` · export `GettingStartedPage` · group Onboarding · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/getting-started/template.meta.ts
- Layout: SidebarLayout + Page
- Blocks: app-sidebar, app-topbar, page-header
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=getting-started
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add getting-started`
Usage:
```tsx
import { GettingStartedPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Contact (`contact`)
Public contact page: StackedLayout with a MegaMenu (MobileNav below lg), a validated contact form (Field, Input, Select, Textarea, ValidationSummary) with a focused success state and a destructive Alert on failed sends. Failed sends keep all values and use submitErrorMessage or localized contact.sendFailed text without exposing backend errors. Includes office cards with address, email, phone and hours.
- Package: `@gntik-ai/templates` · export `ContactPage` · group Overview · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/contact/template.meta.ts
- Layout: StackedLayout
- Blocks: validation-summary
- Console shell: no
- Preview: /preview.html?kind=template&id=contact
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add contact`
Usage:
```tsx
import { ContactPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Home dashboard (`home-dashboard`)
Workspace home in the console shell: page header, KPI row, cost and traffic charts, recent activity and quick-action cards.
- Package: `@gntik-ai/templates` · export `HomeDashboardPage` · group Overview · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/home-dashboard/template.meta.ts
- Layout: SidebarLayout + Page
- Blocks: app-sidebar, app-topbar, page-header, kpi-row, chart-card, activity-feed
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=home-dashboard
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add home-dashboard`
Usage:
```tsx
import { HomeDashboardPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Incident board (`incident-board`)
Incident triage Kanban in the console: page header with Export / Declare incident, a KPI strip (open, SEV1, unassigned, resolved), a FilterBar (search, severity, service, owner) and a KanbanBoard by stage (Triage · Investigating · Mitigated · Resolved) whose cards show severity, owner and age; pointer and keyboard moves with live announcements, and a details Drawer whose stage select also moves the card.
- Package: `@gntik-ai/templates` · export `IncidentBoardPage` · group Overview · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/incident-board/template.meta.ts
- Layout: SidebarLayout · Page
- Blocks: app-sidebar, app-topbar, page-header, kpi-row, filter-bar
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=incident-board
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add incident-board`
Usage:
```tsx
import { IncidentBoardPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Incident console (`incident-console`)
Incident triage in the console: a SplitLayout alert rail filtered with PowerSearch (severity, status, service, assignee; default “not resolved”), sorted by severity, with a detail pane showing the elapsed Timer, Acknowledge / Resolve actions and the StatusTimeline of events.
- Package: `@gntik-ai/templates` · export `IncidentConsolePage` · group Overview · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/incident-console/template.meta.ts
- Layout: SidebarLayout · Page · SplitLayout
- Blocks: app-sidebar, app-topbar, page-header, status-timeline
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=incident-console
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add incident-console`
Usage:
```tsx
import { IncidentConsolePage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Landing (`landing`)
Public landing: Hero, FeatureGrid, PricingTable, Faq
- Package: `@gntik-ai/templates` · export `LandingPage` · group Overview · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/landing/template.meta.ts
- Layout: StackedLayout
- Blocks: hero, feature-grid, pricing-table, faq
- Console shell: no
- Preview: /preview.html?kind=template&id=landing
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add landing`
Usage:
```tsx
import { LandingPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Pricing (`pricing`)
Public pricing page: StackedLayout with a MegaMenu (MobileNav below lg), PricingTable with the monthly / annual toggle, FeatureGrid highlights, a plan comparison table grouped by section, and Faq.
- Package: `@gntik-ai/templates` · export `PricingPage` · group Overview · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/pricing/template.meta.ts
- Layout: StackedLayout
- Blocks: pricing-table, feature-grid, faq
- Console shell: no
- Preview: /preview.html?kind=template&id=pricing
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add pricing`
Usage:
```tsx
import { PricingPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Scorecard (`scorecard`)
Period-over-period scorecard: period select, KpiRow with deltas vs. the previous period, a targets table (change, target, attainment Meter, MiniBar trend), a RadarChart of dimension scores and a commentary rail (CommentThread).
- Package: `@gntik-ai/templates` · export `ScorecardPage` · group Overview · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/scorecard/template.meta.ts
- Layout: SidebarLayout · Page
- Blocks: app-sidebar, app-topbar, page-header, kpi-row, comment-thread
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=scorecard
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add scorecard`
Usage:
```tsx
import { ScorecardPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Usage analytics (`usage-analytics`)
Analytics page: date range and filter bar, spend and traffic charts, a sortable breakdown table and a CSV export action.
- Package: `@gntik-ai/templates` · export `UsageAnalyticsPage` · group Overview · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/usage-analytics/template.meta.ts
- Layout: SidebarLayout + Page
- Blocks: app-sidebar, app-topbar, page-header, date-range-filter, filter-bar, chart-card, data-table, empty-states
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=usage-analytics
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add usage-analytics`
Usage:
```tsx
import { UsageAnalyticsPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Board (`board`)
Kanban board in the console: columns with counts, cards with priority, labels, due date and assignee; pointer drag-and-drop between and within columns, keyboard moves (Space to pick up, arrows to move, Space to drop, Escape to cancel) with live announcements, and a card detail Drawer whose status select also moves the card.
- Package: `@gntik-ai/templates` · export `BoardPage` · group Resources · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/board/template.meta.ts
- Layout: SidebarLayout · Page
- Blocks: app-sidebar, app-topbar, page-header
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=board
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add board`
Usage:
```tsx
import { BoardPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Create wizard (`create-wizard`)
Focus-mode create flow on WizardLayout. Without flowSteps, the existing details, source, configuration and editable review flow, CreateWizardProps, copy and defaults are unchanged. Page mode is the default; mode="overlay" keeps the parent list mounted without a route change. Overlay size defaults to fullscreen; size="dialog" uses a popup (fullscreen on mobile). Legacy closing preserves answers; confirming exit in flow mode resets answers and the current step.
## Caller-defined flow mode
Import CreateWizardPage (or CreateWizardFlow) and CreateWizardFlowProps from @gntik-ai/templates. flowSteps selects the additive generic variant; legacy-only props (steps, copy, sources, regions, title, stepFeedback, onStepChange) cannot mix with it. V is the caller's object type. The kit owns current step, values and validation visibility. The display-only Stepper follows the supplied order and appends review. Zero caller steps renders review directly.
| Prop | Type / behaviour |
| --- | --- |
| flowSteps | readonly { id: string; label: string; readOnly?: boolean; render({ values, setValue, errors }): ReactNode; validate?(values): { fieldErrors?, blocking? } }[]; ids must be unique and stable |
| initialValues | V; used at mount and restored on confirmed exit |
| summary | (values: V) => readonly { stepId: string; label: string; value?: string or null }[] |
| labels | Required CreateWizardFlowLabels; no template-owned visible or accessible copy has a fallback |
| onFinish | (values: V, { signal: AbortSignal }) => void or Promise; one signal per attempt |
| onExit | Called once after confirmed exit, after reset and abort; Stay calls nothing |
| onComplete | Uncontrolled only; (result: unknown) => void, once after success is committed and announced |
| submitState | Controlled when provided at mount: {status:'idle'}, {status:'submitting'}, {status:'success', result?:unknown}, or {status:'error', error?:unknown, message?:string} |
| resourceHref | (result: unknown) => string or undefined; only called with a non-null success result |
| errorMessage | (error: unknown) => string or undefined; empty text uses labels.errorFallback |
| confirmExit | Defaults to true; Exit, Escape and overlay Cancel open confirmation. false exits directly |
| mode, size, open, onOpenChange, description, returnFocusRef, closeOnInteractOutside | WizardLayout presentation props; closing itself is not confirmation or reset |
labels supplies title, exit, cancel, back, next, finish, pending, review (Stepper label), reviewTitle (heading), edit(stepLabel), emptyValue, success(result), errorFallback, openResource, exitTitle, exitDescription, exitConfirm, exitCancel, steps (Stepper name), stepsSummary, skipLink, stepStatusText (complete/current/upcoming/error), formatStepCount(position,total) and stepAnnouncement({position,total,label}). Positions are one-based. Step labels, validation messages, summary labels/values and rendered body content also belong to the caller's namespace. Every DescriptionListCard receives the owning step label as its explicit title, labels.edit(stepLabel), explicit items and an empty description. All steps get a card, even with no matching rows; empty values use labels.emptyValue.
## Validation and navigation
validate runs on every render, including caller re-renders (for example members.count quota changes). A non-empty blocking message disables Next immediately. It is visible as ordinary text and announced politely once per new value, including entry and one frame after overlay opening. Clearing blocking re-enables Next without a press. fieldErrors is Partial>. Errors become visible after the first Next press on their step and remain live thereafter. With errors, Next does not advance and focuses the first aria-invalid="true" field. Render fields using Field invalid, FieldLabel, Input and FieldError to associate errors and controls. Blocking does not hide visible field errors. A readOnly step must render its fixed value as text, without editable controls; only its blocking result gates Next and its fieldErrors are ignored. Back, Next and review Edit preserve values. Edit returns to the owning step and focuses its heading. No direct Stepper jumps are available.
## Submit ownership and aborts
In uncontrolled mode, Finish shows labels.pending in both presentations. Resolve commits success, announces labels.success(result) politely, then calls onComplete(result) once. Reject keeps values and review, renders errorMessage(error) or labels.errorFallback, announces assertively and re-enables Finish. Identical repeated failures each announce; changing settled error text announces once, unchanged text does not. Visible success, error and blocking text is never a live region. All announcements use useAnnounce from the nearest LiveAnnouncer; the template provides one only when needed. Put LiveAnnouncer at page level when onComplete closes/unmounts the flow, so the message survives.
Controlled mode renders submitState only. Each enabled Finish press calls the existing onFinish once; its returned result never changes kit state and onComplete is never called. The caller must pass through submitting for every attempt, discard completions when signal.aborted, and reset submitState to idle in onExit before reopening. Switching between controlled and uncontrolled after mount is unsupported. AbortSignal is a cancellation contract, not a transport: callers must honor it. Confirmed exit and unmount abort the current attempt. Uncontrolled aborted settlements cannot update state, announce or complete, including an older attempt settling after reopening.
While submitting or after success, Back has backDisabled (aria-disabled but focusable), review cards have no Edit, and Finish is loading or disabled. Exit remains enabled. Pending Finish and locked Back retain focus. A focused control becoming disabled or removed sends focus on success to the resource link, or to the success heading (tabIndex=-1) if no result/href exists; in all other cases it goes to the current heading. Focus never moves to a live region. Page Back/Next preserve focus unless the pressed control becomes disabled. Overlay step changes focus the new heading and announce via LiveAnnouncer. Escape in a portalled Select or Menu stays local.
## Examples
examples/Invite.tsx is a generic single-recipient invite with a read-only workspace context, one email checked against /^[^\s@]+@[^\s@]+\.[^\s@]+$/, a members.count quota, exactly Owner/Admin/Member roles, and an optional message with maxLength=500. Its copy is one English page namespace. examples/labels.ts lists every required label. UncontrolledPending, UncontrolledSuccess and UncontrolledError cover a never-settling attempt, a success resource link and a failure fallback. ControlledIdle, ControlledSubmitting, ControlledSuccess and ControlledError show each caller-owned state. Every example has a create-wizard-- preview entry; Invite uses create-wizard--invite. The three-theme browser axe loop visits each step, review, submit state and exit confirmation. examples/Overlay.tsx retains the original list-to-overlay project flow with step-local error and empty surfaces.
A controlled caller's onFinish can use this pattern (with its own transport, values and state): setSubmitState({status:'submitting'}); try { const result = await save(values, {signal}); if (!signal.aborted) setSubmitState({status:'success',result}); } catch (error) { if (!signal.aborted) setSubmitState({status:'error',error}); }. onExit resets to idle before reopening. Keep the page-level LiveAnnouncer mounted when closing.
All flow surfaces reuse kit components and existing tokens; no new CSS variables. Themes: dark, light and high_contrast.
- Package: `@gntik-ai/templates` · export `CreateWizardPage` · group Resources · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/create-wizard/template.meta.ts
- ARIA pattern: Caller-defined steps and review with display-only progress; explicit labels; field-error associations; one LiveAnnouncer announcement mechanism; abortable submit lifecycle.
- Layout: WizardLayout
- Blocks: description-list-card, error-panel, empty-states
- Console shell: no
- Preview: /preview.html?kind=template&id=create-wizard
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add create-wizard`
Keyboard:
| Key | Behaviour |
| --- | --- |
| Tab / Shift+Tab | Move through Exit, fields, review Edit and footer controls; the Stepper is display-only. |
| Enter / Space on Next | Validate; focus the first invalid field, or advance. Blocking disables Next. |
| Enter / Space on Back | Return one step; disabled on the first page step and locked while submitting or after success. |
| Enter on Edit | Return to the owning step and focus its heading, preserving answers. |
| Escape / Exit | Open exit confirmation; Stay does nothing, Leave resets answers and aborts the attempt. Portalled popups handle their own Escape. |
| Finish | Call onFinish once when enabled; pending keeps focus, success hands focus to a resource link or the success heading if Finish becomes disabled. |
Usage:
```tsx
import { CreateWizardPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Resource detail (`resource-detail`)
Resource detail with editable Overview, Activity and Settings tabs by default; optional readOnly (boolean, default false) uses only explicit header and detail props and one Overview tab, with optional history (readonly StatusEvent[], default undefined): omitted hides the history card, [] shows the named empty state, and events render exactly as supplied without invented actors. Read-only ignores activity, events, dangerActions, defaultTab, onSaveSettings and onDangerAction; explicit actions and onEditDetails are honored. The Resource sections tablist retains Tab, arrow, Home/End and Enter/Space keyboard navigation; history is a named group containing the Status history list or empty-state region. Uses token styling in dark (default), light and high_contrast. Examples: runtime-with-transition maps state and lastTransitionAt to one actor-free event, runtime-without-transition maps null to [], and realtime-configuration and postgres-table-detail omit history. Optional labels?: Partial overrides overview, activity, settings and sections (all strings). Unset keys use I18nProvider (en/es); no provider keeps English. Only the tabs and tablist are localized by this template; fixtures and other section text are unchanged. readOnly keeps only the localized overview tab. See the Catalog labels live preview. ConsoleShell retains sample frame defaults when shell is omitted or an object. shell={null} renders only Page and PageHeader, with breadcrumbs in the header; pass breadcrumbs or [] to avoid the editable sample trail. The host owns main and provides ChartLocaleProvider for custom chart locales. Optional tabs?: { value: string; label: string; content: ReactNode }[] replaces the built-in tabs in caller order, even with readOnly; [] hides the tab set. With custom tabs, defaultTab and onTabChange use string values; an omitted or unknown defaultTab selects the first tab. Without tabs they retain ResourceDetailTab values. Custom tabs use the same Base UI keyboard navigation and panel mounting. See the embedded-custom-tabs live preview for shell-less composition inside a host main.
- Package: `@gntik-ai/templates` · export `ResourceDetailPage` · group Resources · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/resource-detail/template.meta.ts
- Layout: SidebarLayout + Page
- Blocks: app-sidebar, app-topbar, page-header, description-list-card, status-timeline, activity-feed, form-section, danger-zone
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=resource-detail
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add resource-detail`
Usage:
```tsx
import { ResourceDetailPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Resource form (`resource-form`)
Long create/edit form: two-column form sections, an error summary that links to each invalid field after submit, and a sticky save bar that tracks unsaved changes.
- Package: `@gntik-ai/templates` · export `ResourceFormPage` · group Resources · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/resource-form/template.meta.ts
- Layout: SidebarLayout + Page
- Blocks: app-sidebar, app-topbar, page-header, form-section, validation-summary, sticky-action-bar
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=resource-form
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add resource-form`
Usage:
```tsx
import { ResourceFormPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Resource gallery (`resource-gallery`)
Card gallery of resources with a toolbar (search, grid/table view toggle, create action), per-card actions and a no-results state.
- Package: `@gntik-ai/templates` · export `ResourceGalleryPage` · group Resources · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/resource-gallery/template.meta.ts
- Layout: SidebarLayout + Page
- Blocks: app-sidebar, app-topbar, page-header, page-toolbar, resource-card-grid, data-table, empty-states
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=resource-gallery
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add resource-gallery`
Usage:
```tsx
import { ResourceGalleryPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Resource index (`resource-index`)
List page for any resource: header with the create action, filter bar with saved views, a selectable sortable table with pagination, a sticky bulk action bar and a no-results state.
- Package: `@gntik-ai/templates` · export `ResourceIndexPage` · group Resources · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/resource-index/template.meta.ts
- Layout: SidebarLayout + Page
- Blocks: app-sidebar, app-topbar, page-header, filter-bar, data-table, pagination-footer, bulk-action-bar, empty-states
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=resource-index
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add resource-index`
Usage:
```tsx
import { ResourceIndexPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Resource tree (`resource-tree`)
Resource index with a team → project TreeList (counts, single selection) beside grouped DataTables (group by kind, region or team; one SectionHeader + table per group), StatusTag status and OverflowList tags.
- Package: `@gntik-ai/templates` · export `ResourceTreePage` · group Resources · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/resource-tree/template.meta.ts
- Layout: SidebarLayout · Page
- Blocks: app-sidebar, app-topbar, page-header, section-header, data-table
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=resource-tree
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add resource-tree`
Usage:
```tsx
import { ResourceTreePage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### API keys settings (`settings-api-keys`)
API key list with scopes and last use, a create-key dialog that shows the secret once, and typed revoke confirmation.
- Package: `@gntik-ai/templates` · export `SettingsApiKeysPage` · group Settings · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/settings-api-keys/template.meta.ts
- Layout: SettingsLayout
- Blocks: app-sidebar, app-topbar, page-header, data-table, create-api-key-dialog, confirm-destructive
- Console shell: no
- Registry dependencies: settings-profile
- Preview: /preview.html?kind=template&id=settings-api-keys
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add settings-api-keys`
Usage:
```tsx
import { SettingsApiKeysPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Appearance settings (`settings-appearance`)
Theme switcher wired to the ThemeProvider, an in-app Reduce motion switch, table density and interface language.
- Package: `@gntik-ai/templates` · export `SettingsAppearancePage` · group Settings · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/settings-appearance/template.meta.ts
- Layout: SettingsLayout
- Blocks: app-sidebar, app-topbar, page-header, settings-row
- Console shell: no
- Registry dependencies: settings-profile
- Preview: /preview.html?kind=template&id=settings-appearance
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add settings-appearance`
Usage:
```tsx
import { SettingsAppearancePage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Billing settings (`settings-billing`)
Current plan with billing cycle, quota meters, the default payment method and invoice history.
- Package: `@gntik-ai/templates` · export `SettingsBillingPage` · group Settings · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/settings-billing/template.meta.ts
- Layout: SettingsLayout
- Blocks: app-sidebar, app-topbar, page-header, plan-card, quota-meters, payment-method-card, invoice-table
- Console shell: no
- Registry dependencies: settings-profile
- Preview: /preview.html?kind=template&id=settings-billing
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add settings-billing`
Usage:
```tsx
import { SettingsBillingPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Integrations settings (`settings-integrations`)
Connector cards with on/off switches, webhook endpoints and a filterable delivery log.
- Package: `@gntik-ai/templates` · export `SettingsIntegrationsPage` · group Settings · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/settings-integrations/template.meta.ts
- Layout: SettingsLayout
- Blocks: app-sidebar, app-topbar, page-header, settings-row
- Console shell: no
- Registry dependencies: settings-profile
- Preview: /preview.html?kind=template&id=settings-integrations
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add settings-integrations`
Usage:
```tsx
import { SettingsIntegrationsPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Members settings (`settings-members`)
Workspace members with inline roles, pending invitations, an invite dialog and a role permission matrix.
- Package: `@gntik-ai/templates` · export `SettingsMembersPage` · group Settings · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/settings-members/template.meta.ts
- Layout: SettingsLayout
- Blocks: app-sidebar, app-topbar, page-header, members-table, pending-invitations, invite-members-dialog
- Console shell: no
- Registry dependencies: settings-profile
- Preview: /preview.html?kind=template&id=settings-members
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add settings-members`
Usage:
```tsx
import { SettingsMembersPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Notification settings (`settings-notifications`)
Event × channel notification matrix with bulk row and column toggles and a sticky save bar.
- Package: `@gntik-ai/templates` · export `SettingsNotificationsPage` · group Settings · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/settings-notifications/template.meta.ts
- Layout: SettingsLayout
- Blocks: app-sidebar, app-topbar, page-header, notification-matrix, sticky-action-bar
- Console shell: no
- Registry dependencies: settings-profile
- Preview: /preview.html?kind=template&id=settings-notifications
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add settings-notifications`
Usage:
```tsx
import { SettingsNotificationsPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Profile settings (`settings-profile`)
Personal profile defaults to mode='editable': avatar header, name and email, locale and time zone, with a sticky save bar. Opt into mode='read-only' for an initials avatar without upload or Edit actions, identity and optional context description lists, and optional ordered roles badges. identity/context accept { id, label, value, mono } rows; value may be a link. contextFallback supplies missing-value text; emptyRolesMessage supplies the empty roles state. Omitted roles/context hide their sections. Read-only mode passes saveBar='none' through SettingsFrame and has no profile form controls, copy buttons or save actions; the responsive section navigation remains available.
- Package: `@gntik-ai/templates` · export `SettingsProfilePage` · group Settings · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/settings-profile/template.meta.ts
- Layout: SettingsLayout
- Blocks: app-sidebar, app-topbar, page-header, profile-header, form-section, sticky-action-bar, description-list-card, section-header
- Console shell: no
- Preview: /preview.html?kind=template&id=settings-profile
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add settings-profile`
Usage:
```tsx
import { SettingsProfilePage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Security settings (`settings-security`)
Password change, two-factor setup with a one-time code dialog, and signed-in sessions and devices.
- Package: `@gntik-ai/templates` · export `SettingsSecurityPage` · group Settings · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/settings-security/template.meta.ts
- Layout: SettingsLayout
- Blocks: app-sidebar, app-topbar, page-header, form-section, settings-row, sessions-devices
- Console shell: no
- Registry dependencies: settings-profile
- Preview: /preview.html?kind=template&id=settings-security
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add settings-security`
Usage:
```tsx
import { SettingsSecurityPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Workspace settings (`settings-workspace`)
Workspace name, URL slug, logo upload and data region, with a danger zone for transfer and deletion.
- Package: `@gntik-ai/templates` · export `SettingsWorkspacePage` · group Settings · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/settings-workspace/template.meta.ts
- Layout: SettingsLayout
- Blocks: app-sidebar, app-topbar, page-header, form-section, file-upload-panel, sticky-action-bar, danger-zone
- Console shell: no
- Registry dependencies: settings-profile
- Preview: /preview.html?kind=template&id=settings-workspace
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add settings-workspace`
Usage:
```tsx
import { SettingsWorkspacePage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Sidebar shell (`shell-sidebar`)
Console starter: app sidebar, topbar and an empty Page with a title, description and a first-run empty state.
- Package: `@gntik-ai/templates` · export `ShellSidebarPage` · group Shells · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/shell-sidebar/template.meta.ts
- Layout: SidebarLayout
- Blocks: app-sidebar, app-topbar, page-header, empty-states
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=shell-sidebar
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add shell-sidebar`
Usage:
```tsx
import { ShellSidebarPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Top-nav shell (`shell-topnav`)
Top-navigation starter: brand, primary links (drawer on small screens), search, notifications, theme and user menu, a page header and an empty body.
- Package: `@gntik-ai/templates` · export `ShellTopnavPage` · group Shells · status stable
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/shell-topnav/template.meta.ts
- Layout: StackedLayout
- Blocks: app-topbar, page-header, empty-states
- Console shell: no
- Preview: /preview.html?kind=template&id=shell-topnav
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add shell-topnav`
Usage:
```tsx
import { ShellTopnavPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### 403 Forbidden (`status-403`)
Full-page permission state: explains who can grant access, with a Request access action (idle · pending · requested, like NoAccessEmpty) and a way home.
- Package: `@gntik-ai/templates` · export `Status403Page` · group System · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/status-403/template.meta.ts
- Layout: StatusLayout
- Console shell: no
- Preview: /preview.html?kind=template&id=status-403
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add status-403`
Usage:
```tsx
import { Status403Page } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### 404 Not found (`status-404`)
Full-page not-found state: code, message, a primary way home, Go back and a support link. The optional heading slot replaces the built-in title so PageHeader supplies the only h1; code stays above it and actions remain unchanged. In the PageHeader example set as="div", breadcrumbs={null}, meta={[]}, tabs={null}, actions={[]}, status="" and description={null}; use StatusLayout’s description once. PageHeader titleRef adds tabIndex=-1 for ref.current.focus() on route load without a Tab stop; leave slot links and actions empty to keep the primary action first in main.
- Package: `@gntik-ai/templates` · export `Status404Page` · group System · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/status-404/template.meta.ts
- Layout: StatusLayout
- Blocks: page-header
- Console shell: no
- Preview: /preview.html?kind=template&id=status-404
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add status-404`
Usage:
```tsx
import { Status404Page } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### 500 Server error (`status-500`)
Full-page server error: StatusLayout message with an ErrorPanel (status code, copyable request id, retry, technical details) and links home and to the status page.
- Package: `@gntik-ai/templates` · export `Status500Page` · group System · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/status-500/template.meta.ts
- Layout: StatusLayout
- Blocks: error-panel
- Console shell: no
- Preview: /preview.html?kind=template&id=status-500
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add status-500`
Usage:
```tsx
import { Status500Page } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Maintenance (`status-maintenance`)
Full-page maintenance state: message, Check again and a status page link, with a maintenance SystemBanner carrying the window and a subscribe link.
- Package: `@gntik-ai/templates` · export `StatusMaintenancePage` · group System · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/status-maintenance/template.meta.ts
- Layout: StatusLayout
- Blocks: system-banner
- Console shell: no
- Preview: /preview.html?kind=template&id=status-maintenance
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add status-maintenance`
Usage:
```tsx
import { StatusMaintenancePage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Upgrade required (`upgrade-required`)
Plan gate inside the console: what the locked feature does, the current plan, and a PricingTable comparison whose CTA starts the upgrade.
- Package: `@gntik-ai/templates` · export `UpgradeRequiredPage` · group System · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/upgrade-required/template.meta.ts
- Layout: SidebarLayout · Page
- Blocks: app-sidebar, app-topbar, page-header, inline-callout, pricing-table
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=upgrade-required
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add upgrade-required`
Usage:
```tsx
import { UpgradeRequiredPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Audit log (`audit-log`)
Workspace audit trail: FilterBar (search, actor / resource / result filters) and DateRangeFilter over a DataTable of events with actor, action, target and result; a drawer shows the change diff (CodeBlock); export to CSV. CSV export neutralises leading spreadsheet formula triggers.
- Package: `@gntik-ai/templates` · export `AuditLogPage` · group Work · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/audit-log/template.meta.ts
- Layout: SidebarLayout · Page
- Blocks: app-sidebar, app-topbar, page-header, filter-bar, date-range-filter, data-table, empty-states
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=audit-log
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add audit-log`
Usage:
```tsx
import { AuditLogPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Messaging (`messaging`)
Team messaging in the console: SplitLayout conversation list (unread counts, last message) → thread of ChatMessages with Thumbnail attachments, participants AvatarGroup, and a ChatComposer with file attachments; Back on small screens.
- Package: `@gntik-ai/templates` · export `MessagingPage` · group Work · status experimental
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/messaging/template.meta.ts
- Layout: SidebarLayout · Page · SplitLayout
- Blocks: app-sidebar, app-topbar, page-header
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=messaging
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add messaging`
Usage:
```tsx
import { MessagingPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Notification inbox (`notification-inbox`)
Master–detail inbox in the console: All / Unread / Mentions filter (ToggleGroup), read and unread rows, a detail pane with Open, Mark as unread and Archive, and Back on small screens.
- Package: `@gntik-ai/templates` · export `NotificationInboxPage` · group Work · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/notification-inbox/template.meta.ts
- Layout: SidebarLayout · Page · SplitLayout
- Blocks: app-sidebar, app-topbar, page-header
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=notification-inbox
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add notification-inbox`
Usage:
```tsx
import { NotificationInboxPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```
### Search results (`search-results`)
Workspace search: query field, facets as Checkbox groups with counts, results grouped by type under SectionHeaders, arrow-key navigation through the results, and NoResultsEmpty.
- Package: `@gntik-ai/templates` · export `SearchResultsPage` · group Work · status beta
- Docs: https://github.com/gntik-ai/gntik-ui/blob/main/packages/templates/src/search-results/template.meta.ts
- Layout: SidebarLayout · Page
- Blocks: app-sidebar, app-topbar, page-header, section-header, empty-states
- Console shell: yes (pass the product frame through the `shell` prop)
- Preview: /preview.html?kind=template&id=search-results
- Install: import from `@gntik-ai/templates`, or copy the source with `gntik-ui add search-results`
Usage:
```tsx
import { SearchResultsPage } from '@gntik-ai/templates';
// Every prop is optional: unset ones fall back to the template fixtures.
export default function Route() {
return ;
}
```