# 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 (
); } ``` ### 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 (
{command}
); } ``` ### 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'))} /> {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 && ( )}
); } ``` ### 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 ( ); } `; 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 ); } ``` ### 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 Tabnext 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 (
Deployments, compact view with selection Select Deployment Region Requests {DEPLOYMENTS.map((d) => ( toggle(d.id)} className={checkboxClass} /> {d.name} {d.region} {count(d.requests)} ))}
); } ``` ### 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 (
); } ``` ### 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