@ggui-ai/ui-gen 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/generation-dispatch.d.ts +1 -1
- package/dist/adapters/generation-dispatch.js +449 -194
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +4 -6
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +2 -2
- package/dist/advanced/index.js +391 -134
- package/dist/advanced/index.js.map +1 -1
- package/dist/boilerplate.js +29 -5
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +70 -0
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-C8Ki9Wz1.d.ts → create-ui-generator-CxPvudbs.d.ts} +1 -1
- package/dist/{dispatch-D2htzi9h.d.ts → dispatch-N8wdNOX-.d.ts} +4 -2
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js +147 -3
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/axis-checks/registry.js +142 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +3 -3
- package/dist/evaluation/index.js +217 -84
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +91 -64
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.js +4 -0
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +1 -1
- package/dist/harness/runtime.d.ts +1 -1
- package/dist/harness/runtime.js +31 -7
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +8 -1
- package/dist/harness/types-public.js +91 -64
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +391 -134
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +1 -1
- package/dist/{types-public-DVSssdc-.d.ts → types-public-DKR_h70H.d.ts} +9 -0
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +12 -12
- package/src/adapters/google/sdk.ts +11 -7
- package/src/boilerplate/free-design-prompt.ts +2 -0
- package/src/boilerplate/generate.ts +8 -1
- package/src/boilerplate/hard-sections.ts +32 -3
- package/src/boilerplate/system-prompt.ts +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +5 -3
- package/src/boilerplate/templates/layouts/fullscreen-mobile.tsx.tmpl +3 -3
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +5 -3
- package/src/check/run-tier0.ts +152 -0
- package/src/coding-agent/tools.ts +58 -6
- package/src/contract-context.ts +13 -0
- package/src/create-harness.ts +1 -0
- package/src/evaluation/axis-checks/checks/universal.ts +179 -1
- package/src/evaluation/axis-checks/dispatch.ts +8 -1
- package/src/evaluation/types-public.ts +9 -0
- package/src/harness/coding/generate-task-runner.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +14 -0
- package/src/harness/types-public.ts +7 -0
- package/src/run-check.ts +2 -0
- package/src/tools/get-wire.ts +1 -1
- package/src/validation/primitives.ts +1 -1
package/dist/advanced/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { getCssTokens, fillFitRule, FORBIDDEN_IMPORT_SPECIFIERS } from '@ggui-ai
|
|
|
6
6
|
import { classifyUi, MODEL_REGISTRY, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, listContractGadgets, HOOK_NAME_RE, modelRefOfRoute, isGeneratorId, APP_GENERATION_PROFILE_BOUNDS, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
|
|
7
7
|
import fs, { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync, rm, createReadStream, createWriteStream, readdirSync, statSync } from 'fs';
|
|
8
8
|
import { contentHash } from '@ggui-ai/protocol/content-hash';
|
|
9
|
-
import
|
|
9
|
+
import ts4 from 'typescript';
|
|
10
10
|
import path, { dirname, resolve, join, basename } from 'path';
|
|
11
11
|
import { createRequire } from 'module';
|
|
12
12
|
import { Linter } from 'eslint';
|
|
@@ -21,7 +21,7 @@ import { pipeline } from 'stream/promises';
|
|
|
21
21
|
import { createBrotliDecompress, createUnzip } from 'zlib';
|
|
22
22
|
import { PlaywrightNotAvailableError, validateContractBehavior } from '@ggui-ai/ui-visual-tester';
|
|
23
23
|
import { parseGeneratorSlug, formatGeneratorSlug, isValidGeneratorSlug } from '@ggui-ai/mcp-server-core';
|
|
24
|
-
import { LUCIDE_ICON_NAMES } from '@ggui-ai/design';
|
|
24
|
+
import { LUCIDE_ICON_NAMES, maxWidth } from '@ggui-ai/design';
|
|
25
25
|
import git from 'isomorphic-git';
|
|
26
26
|
import { Volume, createFsFromVolume } from 'memfs';
|
|
27
27
|
import { createPatch, applyPatch } from 'diff';
|
|
@@ -1037,11 +1037,8 @@ __export(sdk_exports3, {
|
|
|
1037
1037
|
});
|
|
1038
1038
|
async function runAdkLoop(params) {
|
|
1039
1039
|
const startTime = Date.now();
|
|
1040
|
-
const { FunctionTool, LlmAgent, InMemoryRunner, isFinalResponse } = await import('@google/adk');
|
|
1040
|
+
const { FunctionTool, Gemini, LlmAgent, InMemoryRunner, isFinalResponse } = await import('@google/adk');
|
|
1041
1041
|
const capture = createCapture();
|
|
1042
|
-
if (params.apiKey && !process.env.GOOGLE_GENAI_API_KEY) {
|
|
1043
|
-
process.env.GOOGLE_GENAI_API_KEY = params.apiKey;
|
|
1044
|
-
}
|
|
1045
1042
|
const adkTools = params.tools.map(
|
|
1046
1043
|
(def) => new FunctionTool({
|
|
1047
1044
|
name: def.name,
|
|
@@ -1057,9 +1054,10 @@ async function runAdkLoop(params) {
|
|
|
1057
1054
|
}
|
|
1058
1055
|
})
|
|
1059
1056
|
);
|
|
1057
|
+
const model = params.apiKey ? new Gemini({ model: params.model, apiKey: params.apiKey }) : params.model;
|
|
1060
1058
|
const agent = new LlmAgent({
|
|
1061
1059
|
name: "ui_generator",
|
|
1062
|
-
model
|
|
1060
|
+
model,
|
|
1063
1061
|
instruction: params.systemPrompt,
|
|
1064
1062
|
tools: adkTools
|
|
1065
1063
|
});
|
|
@@ -1404,7 +1402,7 @@ var init_raw4 = __esm({
|
|
|
1404
1402
|
var PRIMITIVES_DOCUMENTATION, VALID_PRIMITIVES;
|
|
1405
1403
|
var init_primitives = __esm({
|
|
1406
1404
|
"src/validation/primitives.ts"() {
|
|
1407
|
-
PRIMITIVES_DOCUMENTATION = "# ggui Primitives & Design System Reference\n\n> You are a world-class UI engineer working with ggui's component library for the first time.\n> This reference documents every available component, prop, and convention.\n> Components handle theming automatically via built-in variants \u2014 pick the right variant and the theme does the rest.\n> For custom styling beyond variants, use CSS variables: var(--ggui-*, fallback).\n\n## Primitives\n\nImport: `import { Component } from '@ggui-ai/design'`\n\n### Container\n\nContainer -- Width-constrained wrapper that centers content horizontally.\n\nRenders a `<div>` with `width: 100%` and a `maxWidth` constraint.\nWhen `center` is true (the default), applies `margin: 0 auto`.\nNo background, border, or shadow -- use Card for visual containment.\n\nCSS variables used: none (pure layout primitive).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| maxWidth | `'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| '3xl' \\| 'full' \\| string` | `'lg'` | Maximum width constraint. Accepts a preset token or any CSS width string. - `'xs'` -- 320px - `'sm'` -- 480px - `'md'` -- 640px - `'lg'` -- 768px - `'xl'` -- 1024px - `'2xl'` -- 1280px - `'3xl'` -- 1536px - `'full'` -- 100% Custom strings (e.g., `'900px'`, `'60ch'`) are passed through as-is. |\n| center | `boolean` | `true` | Whether to center the container horizontally via `margin: 0 auto`. |\n| padding | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `undefined (no padding)` | Padding applied to all sides. Prefer a spacing-scale name (`'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl'`) \u2014 each resolves to the matching `--ggui-spacing-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. |\n\n**Example:**\n```tsx\n<Container maxWidth=\"xl\" padding=\"var(--ggui-spacing-6)\">\n <Stack gap=\"var(--ggui-spacing-4)\">\n <Heading level={1}>Dashboard</Heading>\n <Card shadow=\"md\" padding=\"var(--ggui-spacing-5)\">\n <Text>Welcome back!</Text>\n </Card>\n </Stack>\n</Container>\n```\n\n### Card\n\nCard -- Container with background, shadow, and optional border.\n\nRenders a `<div>` with:\n- Background: `var(--ggui-color-container)`\n- Border (when enabled): `1px solid var(--ggui-color-outlineVariant)`\n- Shadow and radius controlled by design tokens via CSS variables.\n- No built-in transitions.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| padding | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `'lg'` | Padding applied to all sides. Prefer a spacing-scale name (`'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl'`) \u2014 each resolves to the matching `--ggui-spacing-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. |\n| shadow | `'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'` | `'sm'` | Shadow elevation level. Maps to design tokens: - `'none'` -- no shadow - `'sm'` -- var(--ggui-shape-shadow-sm) -- subtle, default - `'md'` -- var(--ggui-shape-shadow-md) -- dialogs, emphasized sections - `'lg'` -- var(--ggui-shape-shadow-lg) -- floating panels - `'xl'` -- var(--ggui-shape-shadow-xl) -- popovers, modals |\n| border | `boolean` | `true` | Whether to render a 1px border using `var(--ggui-color-outlineVariant)`. |\n| radius | `'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| number \\| string` | `'lg'` | Corner radius. Prefer a radius-scale name (`'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'`) \u2014 each resolves to the matching `--ggui-shape-radius-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. |\n| surface | `'default' \\| 'elevated' \\| 'sunken' \\| 'accent' \\| 'inverted' \\| 'hero' \\| 'transparent'` | `'default'` | Semantic surface slot. Same vocabulary as ; see that prop's docs for the full slot table. Default Card surface is `'default'` (the active theme's `--ggui-color-container`); pair with `shadow=\"md\"\\|\"lg\"` for elevated cards, `'hero'` for a greeting or hero card (the theme's hero ground, right on a light or a dark host), or `'inverted'` for a dark testimonial-style card on a light theme. |\n\n**Example:**\n```tsx\n<Card shadow=\"md\" padding=\"lg\" radius=\"lg\">\n <Stack gap=\"md\">\n <Text size=\"sm\" weight=\"medium\">Settings</Text>\n <Input label=\"Name\" value={name} onChange={setName} />\n <Button variant=\"primary\">Save</Button>\n </Stack>\n</Card>\n```\n\n### Stack\n\nStack -- Flexbox layout primitive for arranging children along a single axis.\n\nRenders a `<div>` with `display: flex`. Default layout is vertical (column).\nAll flex shorthand values (`align`, `justify`, `wrap`) are abstracted into\nsemantic prop names.\n\nCSS variables used: none (pure layout primitive).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| direction | `'vertical' \\| 'horizontal'` | `'vertical'` | Main axis direction. - `'vertical'` -- `flex-direction: column` - `'horizontal'` -- `flex-direction: row` |\n| gap | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `'sm'` | Gap between children. Prefer a spacing-scale name (`'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl'`) \u2014 each resolves to the matching `--ggui-spacing-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. |\n| align | `'start' \\| 'center' \\| 'end' \\| 'stretch'` | `'stretch'` | Cross-axis alignment (maps to `align-items`). - `'start'` -- flex-start - `'center'` -- center - `'end'` -- flex-end - `'stretch'` -- stretch (children fill cross-axis) |\n| justify | `'start' \\| 'center' \\| 'end' \\| 'between' \\| 'around' \\| 'evenly'` | `'start'` | Main-axis content distribution (maps to `justify-content`). - `'start'` -- flex-start - `'center'` -- center - `'end'` -- flex-end - `'between'` -- space-between - `'around'` -- space-around - `'evenly'` -- space-evenly |\n| wrap | `boolean` | `false` | Whether children wrap to the next line when they overflow. Maps to `flex-wrap: wrap` when true. |\n\n**Example:**\n```tsx\n<Stack gap=\"lg\" align=\"center\">\n <Heading level={2}>Profile</Heading>\n <Text>Edit your account details below.</Text>\n <Stack direction=\"horizontal\" gap=\"sm\" justify=\"end\">\n <Button variant=\"ghost\">Cancel</Button>\n <Button variant=\"primary\">Save</Button>\n </Stack>\n</Stack>\n```\n\n### Grid\n\nGrid -- 2-D layout primitive. Arranges children into rows AND\ncolumns; reach for it when Stack/Row's single-axis flow isn't\nenough (card galleries, dashboards, stat grids).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| columns | `number \\| ResponsiveColumns` | `2` | Column count. Three forms: - a number \u2014 that many equal columns at every width (`columns={3}`); - a map \u2014 explicit counts per breakpoint (`columns={{ base: 1, md: 3 }}` = 1 column on mobile, 3 from `md`). Use this when the request names exact per-breakpoint counts (\"3 per row on desktop, 1 on mobile\"). Ignored entirely when `minColumnWidth` is set. |\n| gap | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `'md'` | Gap between cells. Prefer a spacing-scale name (`'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl'`); a number is pixels. |\n| minColumnWidth | `number \\| string` | `undefined (use `columns`)` | When set, the grid becomes responsive \u2014 it fits as many equal columns as possible, each at least this wide, and `columns` is ignored. A number is treated as pixels. |\n\n**Example:**\n```tsx\n<Grid columns={3} gap=\"md\">\n {items.map((it) => <Card key={it.id}>{it.name}</Card>)}\n</Grid>\n```\n\n### Skeleton\n\nSkeleton -- a pulsing placeholder for content that has not loaded\nyet. ggui UIs are agent-driven (props arrive late, streams start\nempty), so a loading frame is the rule \u2014 render `Skeleton` instead\nof a blank screen or a hand-rolled pulsing `<div>`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| variant | `'rect' \\| 'text' \\| 'circle'` | `'rect'` | Shape preset. - `'rect'` -- a block (default); pair with `width` / `height`. - `'text'` -- a single text line (height ~1em). - `'circle'` -- equal width/height, fully rounded (avatar slot). |\n| width | `number \\| string` | - | Width. A number is pixels. Defaults to `100%` (`2.5rem` for circle). |\n| height | `number \\| string` | - | Height. A number is pixels. Defaults by variant when unset. |\n| radius | `'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| number \\| string` | `'sm'` | Corner radius. Prefer a radius-scale name. Ignored for `variant=\"circle\"` (always fully round). |\n\n**Example:**\n```tsx\n{user === undefined\n ? <Skeleton variant=\"text\" width=\"40%\" />\n : <Text>{user.name}</Text>}\n```\n\n### Box\n\nBox -- Generic container with padding, margin, background, and border-radius.\n\nRenders a plain `<div>`. Unlike Card, Box has no default background, shadow,\nor border -- it is a blank canvas for custom styling. Use it for layout\nspacing, colored sections, or wrapping arbitrary content.\n\nWhen both `paddingX`/`paddingY` and `padding` are provided, the axis-specific\nprops take precedence and `padding` is ignored.\n\nCSS variables used: none (all values are passed through directly).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| padding | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `undefined (no padding)` | Padding applied to all four sides. Prefer a spacing-scale name (`'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl'`) \u2014 each resolves to the matching `--ggui-spacing-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. Ignored when `paddingX` or `paddingY` is set. |\n| paddingX | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `undefined` | Horizontal (left + right) padding. Accepts a spacing-scale name, a pixel number, or a raw CSS string \u2014 see . When set alongside `paddingY`, they combine into a shorthand `padding: {Y} {X}`. When set without `paddingY`, vertical padding defaults to 0. |\n| paddingY | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `undefined` | Vertical (top + bottom) padding. Accepts a spacing-scale name, a pixel number, or a raw CSS string \u2014 see . When set alongside `paddingX`, they combine into a shorthand `padding: {Y} {X}`. When set without `paddingX`, horizontal padding defaults to 0. |\n| margin | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `undefined (no margin)` | Margin applied to all four sides. Accepts a spacing-scale name, a pixel number, or a raw CSS string \u2014 see . |\n| surface | `'default' \\| 'elevated' \\| 'sunken' \\| 'accent' \\| 'inverted' \\| 'hero' \\| 'transparent'` | `undefined (transparent)` | Semantic surface slot. Picks the right `var(--ggui-color-*)` background token from the active theme. The ONLY way to set a theme-tracking background fill on Box. Available slots: - `'default'` \u2014 base container surface (most common) - `'elevated'` \u2014 same fill, intended to be paired with shadow (use Card.shadow for actual elevation) - `'sunken'` \u2014 recessed / inset region (`surfaceVariant` token) - `'accent'` \u2014 highlighted / branded fill (`primary-50` token) - `'inverted'` \u2014 dark surface in light mode, light in dark (testimonials, code-snippet cards, hero panels). The surface owns ALL its inks: body text, `muted` / `subtle` labels, chips and outlines inside it re-map to inverse-derived values by cascade \u2014 no `tone=\"inverse\"` needed, and no hand-picked greys. - `'hero'` \u2014 the hero panel's ground: brand-tinted on a light host, the ink pair on a dark one (`heroGround` / `onHeroGround`, derived per mode) \u2014 reach for it for a greeting or hero card instead of guessing the host's darkness with `'inverted'`; it owns its subtree's inks likewise. - `'transparent'` \u2014 explicit \"no fill\" For non-theme-mapped brand colors (e.g. a partner's exact brand hex like Stripe purple) use the escape \u2014 every other hex / rgba on Box is rejected by tier-0 self-check. |\n| assetColor | `string` | `undefined` | Asset color escape \u2014 the typed valve for legitimate non-theme color values (a partner's exact brand hex, a fixed product surface, etc.). Renders as the Box background. **MUST be paired with .** The semantic name is human-readable documentation of why this color bypasses the theme \u2014 e.g. `\"stripe-brand-purple\"`, `\"slack-aubergine\"`. Tier-0 self-check allows hex / rgba inside `assetColor` ONLY when `assetSemantic` is a non-empty string; one without the other fails the check. Reach for `surface` first. This escape exists for the small set of cases where the operator's theme MUST NOT override the value (brand identity rendering). |\n| assetSemantic | `string` | `undefined` | Human-readable semantic label that documents why bypasses the theme. Required when `assetColor` is set; tier-0 self-check rejects empty strings or a missing `assetSemantic` next to a hex `assetColor`. Examples: `\"stripe-brand-purple\"`, `\"slack-aubergine\"`, `\"partner-logo-orange\"`. Pure documentation \u2014 no rendering effect. |\n| radius | `'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| number \\| string` | `undefined (no rounding)` | Corner radius. Prefer a radius-scale name (`'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'`) \u2014 each resolves to the matching `--ggui-shape-radius-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. |\n\n**Example:**\n```tsx\n<Box paddingX=\"xl\" paddingY=\"lg\" surface=\"accent\" radius=\"lg\">\n <Text size=\"sm\" tone=\"emphasized\">\n Tip: You can customize your theme in Settings.\n </Text>\n</Box>\n```\n\n### Divider\n\nDivider -- A 1px line to visually separate content sections.\n\nRenders an `<hr>` (horizontal) or `<div>` (vertical) with `role=\"separator\"`.\n- Horizontal: 1px tall, full width, with vertical margin.\n- Vertical: 1px wide, stretches to parent height via `align-self: stretch`,\n with horizontal margin. Works best inside a horizontal Stack or Row.\n\nDefault color: `var(--ggui-color-outlineVariant)`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| orientation | `'horizontal' \\| 'vertical'` | `'horizontal'` | Line direction. - `'horizontal'` -- renders `<hr>`, full width, 1px height, margin top/bottom - `'vertical'` -- renders `<div>`, 1px width, `align-self: stretch`, margin left/right |\n| margin | `number \\| string` | `16` | Spacing around the divider. Numbers are treated as pixels. Applied as vertical margin for horizontal dividers, horizontal margin for vertical. |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `undefined (uses `var(--ggui-color-outlineVariant)`)` | Semantic color slot. Same vocabulary as ; the theme decides what each tone LOOKS like. Defaults to a quiet outline-variant tint when unset (independent of the tone slots). |\n\n**Example:**\n```tsx\n<Stack gap={0}>\n <Text>Section A</Text>\n <Divider margin=\"var(--ggui-spacing-3)\" />\n <Text>Section B</Text>\n</Stack>\n```\n\n### Spacer\n\nSpacer -- Invisible spacing element, either fixed-size or flexible.\n\nRenders an empty `<div>`.\n- Fixed mode (number): sets both `width` and `height` to the given pixel\n value with `flex-shrink: 0`, creating rigid spacing in any direction.\n- Flex mode (`'flex'`): sets `flex: 1`, expanding to fill remaining space\n in a flex container. Useful for pushing siblings apart.\n\nCSS variables used: none.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| size | `number \\| 'flex'` | `16` | Spacing amount. - Number: fixed square spacer (width and height in pixels, `flex-shrink: 0`). - `'flex'`: expands to fill available space (`flex: 1`). |\n\n**Example:**\n```tsx\n<Stack direction=\"horizontal\" align=\"center\">\n <Heading level={3}>Logo</Heading>\n <Spacer size=\"flex\" />\n <Button variant=\"ghost\">Login</Button>\n</Stack>\n```\n\n### Text\n\nText -- Versatile typography primitive for body copy, captions, and labels.\n\nRenders as `<p>` by default (configurable via `is`). Typography is\ncomposed from three orthogonal axes: `size` (font size, with a\nmatching line-height built in), `weight`, and `tone` (semantic\ncolor). `caps` adds the uppercase, letter-spaced treatment for tiny\nsection labels (the classic overline/eyebrow look) \u2014 the label's text\ncomes from a prop, never from the component: an invented eyebrow is copy\nnothing can change.\n\nDefault text color: `var(--ggui-color-onContainer)`.\nAll text renders with `margin: 0` (no default paragraph spacing).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| size | `'xs' \\| 'sm' \\| 'base' \\| 'lg' \\| 'xl' \\| '2xl' \\| '3xl' \\| '4xl'` | `'base'` | Font size. Maps to CSS variables with pixel fallbacks; each size carries a matching line-height (1.5 for `xs`--`base`, 1.625 for `lg`, then tightening on display sizes: 1.375 at `xl`, 1.25 from `2xl` up): - `'xs'` -- var(--ggui-font-size-xs) (12px) - `'sm'` -- var(--ggui-font-size-sm) (14px) - `'base'` -- var(--ggui-font-size-base) (16px) - `'lg'` -- var(--ggui-font-size-lg) (18px) - `'xl'` -- var(--ggui-font-size-xl) (20px) - `'2xl'` -- var(--ggui-font-size-2xl) (24px) - `'3xl'` -- var(--ggui-font-size-3xl) (30px) - `'4xl'` -- var(--ggui-font-size-4xl) (36px) |\n| weight | `'normal' \\| 'medium' \\| 'semibold' \\| 'bold'` | `'normal'` | Font weight. Maps to CSS variables with numeric fallbacks: - `'normal'` -- var(--ggui-font-weight-normal) - `'medium'` -- var(--ggui-font-weight-medium) - `'semibold'` -- var(--ggui-font-weight-semibold) - `'bold'` -- var(--ggui-font-weight-bold) |\n| caps | `boolean` | `false` | When true, uppercases the text and adds 0.05em letter-spacing -- the eyebrow/overline treatment for tiny section labels. Pair with `size=\"xs\" weight=\"semibold\" tone=\"muted\"`. |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `'default' (var(--ggui-color-onContainer))` | Semantic color slot. Picks the right `var(--ggui-color-*)` token from the active theme. The theme decides what each tone LOOKS like \u2014 `'muted'` is a quiet warm grey on Claudic, a cool slate on Indigo, dim cyan on Neon-Noir. Components that use `tone` track the operator's theme switch automatically. Available slots: `'default'` (primary body text), `'muted'` (secondary / metadata), `'subtle'` (very-low-emphasis hint), `'emphasized'` (branded accent), `'loud'` (strongest accent \u2014 the theme's readable accent ink), `'success'` / `'warning'` / `'error'` / `'info'` (status text), `'inverse'` (text on dark surface), `'inherit'` (parent's color). `tone` is the ONLY way to set a Text color. The legacy `color?: string` escape was retired \u2014 raw color strings bypass theming and silently override the operator's preset. |\n| align | `'left' \\| 'center' \\| 'right'` | `undefined (inherits from parent)` | Horizontal text alignment. Maps directly to `text-align`. |\n| truncate | `boolean` | `false` | When true, clips overflowing text with an ellipsis. Applies `overflow: hidden`, `text-overflow: ellipsis`, and `white-space: nowrap`. |\n| is | `'p' \\| 'span' \\| 'div' \\| 'label'` | `'p'` | HTML element to render. Choose based on semantic context: - `'p'` -- paragraph (default, block-level) - `'span'` -- inline text within a sentence - `'div'` -- generic block container - `'label'` -- form label (pair with `htmlFor`) |\n| id | `string` | - | `id` for the rendered element \u2014 anchor an in-page link, or pair with a form control's `aria-labelledby`. |\n| htmlFor | `string` | - | Associates an `is=\"label\"` element with a form control by the control's `id`. Only meaningful when `is=\"label\"`. |\n\n**Example:**\n```tsx\n<Stack gap=\"var(--ggui-spacing-1)\">\n <Text size=\"xs\" weight=\"semibold\" caps tone=\"muted\">{props.section}</Text>\n <Text size=\"lg\">Welcome back, Jane.</Text>\n <Text size=\"xs\" tone=\"muted\">\n Last login: 2 hours ago\n </Text>\n</Stack>\n```\n\n### Heading\n\nHeading -- Semantic heading element (h1-h6) with preset typography styles.\n\nRenders the corresponding `<h1>`-`<h6>` HTML element based on `level`.\nEach level has a preset font size, weight, line height, and letter spacing\nfrom the heading typography tokens:\n- Level 1: 36px / bold / 1.25 line-height / -0.025em tracking\n- Level 2: 30px / bold / 1.25 line-height / -0.025em tracking\n- Level 3: 24px / semibold / 1.375 line-height / 0em tracking\n- Level 4: 20px / semibold / 1.375 line-height / 0em tracking\n- Level 5: 18px / semibold / 1.5 line-height / 0em tracking\n- Level 6: 16px / semibold / 1.5 line-height / 0em tracking\n\nDefault text color: `var(--ggui-color-onContainer)`.\nAll headings render with `margin: 0` (no default heading spacing).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| level | `1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6` | `2` | Semantic heading level. Determines both the HTML element (`<h1>`-`<h6>`) and the preset typography style. |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `'default' (var(--ggui-color-onContainer))` | Semantic color slot. Same vocabulary as ; see that prop's docs for the full slot table. `tone` is the ONLY way to set a Heading color \u2014 the legacy `color?: string` escape was retired so the operator's theme always wins. |\n| align | `'left' \\| 'center' \\| 'right'` | `undefined (inherits from parent)` | Horizontal text alignment. Maps directly to `text-align`. |\n\n**Example:**\n```tsx\n<Stack gap=\"var(--ggui-spacing-2)\">\n <Heading level={1}>Page Title</Heading>\n <Heading level={3} tone=\"emphasized\">\n Subsection\n </Heading>\n <Text>Body content goes here.</Text>\n</Stack>\n```\n\n### Button\n\nButton -- A clickable button primitive with multiple visual variants and sizes.\n\nRenders a native `<button>` element styled with inline CSS derived from design-token\nCSS variables. Supports a loading spinner, left/right icon slots, and a cross-platform\n`onPress` alias for `onClick`.\n\nBase styles applied to every variant:\n- `border-radius: var(--ggui-shape-radius-md)`\n- `font-weight: var(--ggui-font-weight-medium)`\n- `box-shadow: var(--ggui-shape-shadow-sm)`\n- `gap: var(--ggui-spacing-2)` between icon and text\n- Transitions: background-color, box-shadow, opacity at 200ms ease-in-out\n\nDisabled or loading: `opacity: 0.5`, `cursor: not-allowed`, click handler suppressed.\n\nAlso extends native `ButtonHTMLAttributes` (except `style`/`className`), so props\nlike `type`, `form`, `aria-*`, and `data-*` are forwarded to the `<button>` element.\nThe `type` prop defaults to `'button'` (not `'submit'`), preventing accidental form\nsubmissions.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| variant | `'primary' \\| 'secondary' \\| 'outline' \\| 'ghost' \\| 'danger'` | `'primary'` | Visual style. Maps to CSS variables: - `'primary'` -- `var(--ggui-color-primary-600)` background, white text, no border - `'secondary'` -- `var(--ggui-color-sunken)` background, `var(--ggui-color-onSunken)` text, no border - `'outline'` -- transparent background, `1px solid var(--ggui-color-primary-600)` border, label in the surface's on-colour (`onContainer`) so it reads on any ground \u2014 the brand shows on the border, never as label ink on a dark theme - `'ghost'` -- transparent background, `var(--ggui-color-onSunken)` text, no border - `'danger'` -- `var(--ggui-color-error-600)` background, white text, no border |\n| size | `'xs' \\| 'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls padding, font size, and minimum height: - `'xs'` -- padding `4px 8px`, font `var(--ggui-font-size-xs)`, min-height 24px - `'sm'` -- padding `6px 12px`, font `var(--ggui-font-size-sm)`, min-height 32px - `'md'` -- padding `10px 16px`, font `var(--ggui-font-size-sm)`, min-height 40px - `'lg'` -- padding `12px 24px`, font `var(--ggui-font-size-base)`, min-height 48px |\n| fullWidth | `boolean` | `false` | When true, sets `width: 100%` so the button fills its container. |\n| loading | `boolean` | `false` | When true, replaces children with a 16px `Spinner` (color: `currentColor`) and disables interaction (same effect as `disabled`). |\n| leftIcon | `ReactNode` | - | ReactNode rendered before children, inside the flex layout with `var(--ggui-spacing-2)` gap. |\n| rightIcon | `ReactNode` | - | ReactNode rendered after children, inside the flex layout with `var(--ggui-spacing-2)` gap. |\n| onPress | `() => void` | - | Alias for `onClick` for cross-platform compatibility (React Native convention). If both `onClick` and `onPress` are provided, `onClick` takes precedence. |\n\n**Example:**\n```tsx\n<Button variant=\"primary\" size=\"md\" leftIcon={<Icon name=\"save\" />} onClick={handleSave}>\n Save Changes\n</Button>\n```\n\n### Input\n\nInput -- A single-line text input with label, validation, and helper text.\n\nRenders a `<div>` wrapper containing an optional `<label>`, a native `<input>`,\nand an optional message `<span>` for error or helper text.\n\nStyling:\n- Border: `1px solid var(--ggui-color-outline)` (normal),\n `var(--ggui-color-error-500)` (error)\n- Background: `var(--ggui-color-container)` (normal),\n `var(--ggui-color-container)` (disabled)\n- Text: `var(--ggui-color-onContainer)`\n- Border radius: `var(--ggui-shape-radius-md)`\n- Label: `var(--ggui-font-size-sm)`, `var(--ggui-font-weight-medium)`,\n `var(--ggui-color-onSunken)`\n- Transitions: border-color, box-shadow at 200ms ease-in-out\n\nAccessibility: auto-generated `id` links `<label>` to `<input>` via `htmlFor`.\nWhen `error` is set, `aria-invalid` is true and the message has `role=\"alert\"`.\nWhen `required` is true, a red asterisk is appended to the label.\n\nAlso extends native `InputHTMLAttributes` (except `style`, `className`, `onChange`,\n`size`), so props like `autoFocus`, `name`, `pattern`, `aria-*` are forwarded.\n\n**IMPORTANT:** `onChange` receives the string value directly, NOT a React\n`ChangeEvent`. This differs from native `<input>` behavior.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Label rendered above the input. Linked to the input via auto-generated `htmlFor`/`id`. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| placeholder | `string` | - | Placeholder text shown when the input is empty. |\n| value | `string` | - | Controlled value of the input. |\n| onChange | `(value: string) => void` | - | Change handler. Receives the new string value directly, NOT a React event. |\n| type | `'text' \\| 'email' \\| 'password' \\| 'number' \\| 'tel' \\| 'url' \\| 'search' \\| 'date' \\| 'time' \\| 'datetime-local'` | `'text'` | HTML input type. Determines browser behavior (keyboard on mobile, validation, masking). The date/time types render the browser's native, localized, keyboard-accessible picker; their `value` stays an ISO string (`'2026-07-29'`, `'14:30'`, `'2026-07-29T14:30'`) delivered through the usual value-first `onChange`. |\n| error | `string` | - | Error message displayed below the input in `var(--ggui-color-error-500)`. When set, the border turns red and the message element gets `role=\"alert\"`. Takes precedence over `helperText`. |\n| helperText | `string` | - | Helper text displayed below the input in `var(--ggui-color-onSunken)`. Only shown when `error` is not set. |\n| required | `boolean` | `false` | When true, appends a red asterisk (`*`) to the label and sets the native `required` attribute on the `<input>`. |\n| disabled | `boolean` | `false` | When true, sets the native `disabled` attribute. Background changes to `var(--ggui-color-container)`. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls padding and font size: - `'sm'` -- padding `6px 10px`, font `var(--ggui-font-size-sm)` - `'md'` -- padding `10px 12px`, font `var(--ggui-font-size-sm)` - `'lg'` -- padding `12px 14px`, font `var(--ggui-font-size-base)` |\n\n**Example:**\n```tsx\n<Input label=\"Email\" type=\"email\" value={email} onChange={setEmail} error={emailError} />\n```\n\n### TextArea\n\nTextArea -- A multiline text input with label, validation, character count, and auto-resize.\n\nRenders a `<div>` wrapper containing an optional `<label>`, a native `<textarea>`,\nand a footer row with error/helper text on the left and character count on the right.\n\nStyling:\n- Padding: `10px 12px`, font: `var(--ggui-font-size-sm)`, `font-family: inherit`\n- Border: `1px solid var(--ggui-color-outline)` (normal),\n `var(--ggui-color-error-500)` (error)\n- Background: `var(--ggui-color-container)` (normal),\n `var(--ggui-color-container)` (disabled)\n- Border radius: `var(--ggui-shape-radius-md)`\n- Resize: `vertical` by default, `none` when `autoResize` is true\n- Transitions: border-color, box-shadow at 200ms ease-in-out\n\nAccessibility: same label/error linking pattern as Input (auto-generated ids,\n`aria-invalid`, `role=\"alert\"` on error message).\n\nAlso extends native `TextareaHTMLAttributes` (except `style`, `className`, `onChange`).\n\n**IMPORTANT:** `onChange` receives the string value directly, NOT a React\n`ChangeEvent`. This differs from native `<textarea>` behavior.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Label rendered above the textarea. Linked via auto-generated `htmlFor`/`id`. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| placeholder | `string` | - | Placeholder text shown when the textarea is empty. |\n| value | `string` | - | Controlled value of the textarea. |\n| onChange | `(value: string) => void` | - | Change handler. Receives the new string value directly, NOT a React event. |\n| rows | `number` | `4` | Number of visible text rows (native `rows` attribute on `<textarea>`). |\n| error | `string` | - | Error message displayed below the textarea in `var(--ggui-color-error-500)`. When set, the border turns red and the message element gets `role=\"alert\"`. Takes precedence over `helperText`. |\n| helperText | `string` | - | Helper text displayed below the textarea in `var(--ggui-color-onSunken)`. Only shown when `error` is not set. |\n| required | `boolean` | `false` | When true, appends a red asterisk (`*`) to the label and sets the native `required` attribute on the `<textarea>`. |\n| disabled | `boolean` | `false` | When true, sets the native `disabled` attribute. Background changes to `var(--ggui-color-container)`. |\n| maxLength | `number` | - | Maximum character length (native `maxLength` attribute). Also used as the denominator in the character count display when `showCount` is true. |\n| showCount | `boolean` | `false` | When true AND `maxLength` is set, displays a `{current}/{max}` character counter in the footer row (right-aligned, `var(--ggui-font-size-xs)`). Has no effect without `maxLength`. |\n| autoResize | `boolean` | `false` | When true, sets CSS `resize: none` on the textarea. The flag disables manual resizing to signal that external logic handles sizing. The component does NOT auto-adjust height based on content in the current implementation. |\n\n**Example:**\n```tsx\n<TextArea label=\"Bio\" value={bio} onChange={setBio} rows={6} maxLength={500} showCount />\n```\n\n### Select\n\nSelect -- A native dropdown selection primitive with label and validation.\n\nRenders a `<div>` wrapper containing an optional `<label>`, a native `<select>`\nwith custom styling, and an optional message `<span>`.\n\nThe native `<select>` has `appearance: none` with a custom chevron SVG rendered\nas a `background-image` (right-aligned, 12px, onSurfaceVariant color). Extra right\npadding (36px) accommodates the chevron.\n\nStyling:\n- Border: `1px solid var(--ggui-color-outline)` (normal),\n `var(--ggui-color-error-500)` (error)\n- Background: `var(--ggui-color-container)` (normal),\n `var(--ggui-color-container)` (disabled)\n- Text: `var(--ggui-color-onContainer)` when a value is selected,\n `var(--ggui-color-onSunken)` when showing placeholder\n- Border radius: `var(--ggui-shape-radius-md)`\n- Cursor: `pointer` (normal), `not-allowed` (disabled)\n- Transitions: border-color, box-shadow at 200ms ease-in-out\n\nAccessibility: auto-generated `id` links `<label>` to `<select>`.\nWhen `error` is set, `aria-invalid` is true and the message has `role=\"alert\"`.\n\nAlso extends native `SelectHTMLAttributes` (except `style`, `className`,\n`onChange`, `size`).\n\n**IMPORTANT:** `onChange` receives the selected value string directly, NOT a\nReact `ChangeEvent`. This differs from native `<select>` behavior.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Label rendered above the select. Linked via auto-generated `htmlFor`/`id`. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| value | `string` | - | Controlled value. Should match one of the `options[].value` strings. |\n| onChange | `(value: string) => void` | - | Change handler. Receives the selected option's value string directly, NOT a React event. |\n| options | `SelectOption[]` | - | Array of selectable options. Rendered as native `<option>` elements. Must contain at least one option (or use `placeholder` for an empty-state prompt). |\n| placeholder | `string` | - | Placeholder text rendered as a disabled `<option value=\"\">` at the top of the list. Shown when no value is selected. |\n| error | `string` | - | Error message displayed below the select in `var(--ggui-color-error-500)`. When set, the border turns red and the message has `role=\"alert\"`. Takes precedence over `helperText`. |\n| helperText | `string` | - | Helper text displayed below the select in `var(--ggui-color-onSunken)`. Only shown when `error` is not set. |\n| required | `boolean` | `false` | When true, appends a red asterisk (`*`) to the label and sets the native `required` attribute. |\n| disabled | `boolean` | `false` | When true, sets the native `disabled` attribute. Background changes to `var(--ggui-color-container)` and cursor becomes `not-allowed`. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls padding and font size: - `'sm'` -- padding `6px 10px`, font `var(--ggui-font-size-sm)` - `'md'` -- padding `10px 12px`, font `var(--ggui-font-size-sm)` - `'lg'` -- padding `12px 14px`, font `var(--ggui-font-size-base)` |\n\n**Example:**\n```tsx\n<Select\n label=\"Country\"\n value={country}\n onChange={setCountry}\n options={[\n { value: 'us', label: 'United States' },\n { value: 'uk', label: 'United Kingdom' },\n ]}\n placeholder=\"Select a country\"\n/>\n```\n\n### Checkbox\n\nCheckbox -- A custom-styled checkbox with label and description.\n\nRenders a `<label>` wrapper containing a visually-hidden native `<input type=\"checkbox\">`\noverlaid by a custom 18x18px visual box. Supports checked, unchecked, and indeterminate\nstates, each with a distinct SVG icon (checkmark or horizontal dash).\n\nStyling:\n- Box border: `2px solid var(--ggui-color-primary-600)` (checked/indeterminate),\n `var(--ggui-color-outline)` (unchecked)\n- Box fill: `var(--ggui-color-primary-600)` (checked/indeterminate),\n `var(--ggui-color-container)` (unchecked)\n- Check/dash icon: white SVG, 12x12px\n- Box radius: `var(--ggui-shape-radius-sm)`\n- Transition: all 0.2s\n- Label: `var(--ggui-font-size-sm)`, `var(--ggui-font-weight-medium)`\n- Description: `var(--ggui-font-size-xs)`, `var(--ggui-color-onSunken)`\n- Disabled: `opacity: 0.5`, `cursor: not-allowed`\n- Gap between box and text: `var(--ggui-spacing-2)`\n\n**IMPORTANT:** `onChange` receives the boolean checked state directly, NOT a\nReact `ChangeEvent`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Primary label text rendered beside the checkbox box. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| checked | `boolean` | - | Controlled checked state. |\n| onChange | `(checked: boolean) => void` | - | Change handler. Receives the new boolean checked state directly, NOT a React event. |\n| disabled | `boolean` | `false` | When true, sets `opacity: 0.5` and `cursor: not-allowed`. The native input is also disabled, preventing keyboard and click interaction. |\n| description | `string` | - | Secondary description text rendered below the label in smaller, muted type (`var(--ggui-font-size-xs)`, `var(--ggui-color-onSunken)`). |\n| indeterminate | `boolean` | `false` | When true, displays a horizontal dash instead of a checkmark. Used for \"select all\" states where some (but not all) children are checked. The `indeterminate` property is set via a ref on the native `<input>`. Visually identical to `checked` in terms of border and fill color. |\n\n**Example:**\n```tsx\n<Checkbox\n label=\"Accept terms\"\n description=\"You agree to the Terms of Service and Privacy Policy\"\n checked={accepted}\n onChange={setAccepted}\n/>\n```\n\n### Toggle\n\nToggle -- A switch/toggle input rendered as a pill-shaped track with a sliding knob.\n\nRenders a `<label>` wrapper with a `<div role=\"switch\">` track and an animated\ncircular knob. Does NOT use a native `<input>` -- keyboard interaction is handled\nmanually (Space and Enter keys toggle the state). The element is focusable via\n`tabIndex={0}` and shows a focus ring on focus.\n\nStyling:\n- Track (on): `var(--ggui-color-primary-600)`\n- Track (off): `var(--ggui-color-outline)`\n- Knob: white circle with `var(--ggui-shape-shadow-sm)`\n- Focus ring: `0 0 0 3px var(--ggui-color-primary-200)`\n- Transitions: background-color, box-shadow, knob position at 200ms ease-in-out\n- Disabled: `opacity: 0.5`, `cursor: not-allowed`, `tabIndex: -1`\n- Gap between toggle and label: `var(--ggui-spacing-2)`\n\n**IMPORTANT:** `onChange` receives the new boolean state directly (inverted from\ncurrent), NOT a React event.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Label text rendered to the right of the toggle track. Also used as `aria-label` on the switch element. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| checked | `boolean` | - | Controlled checked (on/off) state. |\n| onChange | `(checked: boolean) => void` | - | Change handler. Receives the new boolean state directly (i.e., `!checked`), NOT a React event. |\n| disabled | `boolean` | `false` | When true, sets `opacity: 0.5`, `cursor: not-allowed`, and removes the element from tab order (`tabIndex: -1`). Click and keyboard handlers are suppressed. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls track and knob dimensions: - `'sm'` -- track 36x20px, knob 16px diameter - `'md'` -- track 44x24px, knob 20px diameter - `'lg'` -- track 52x28px, knob 24px diameter |\n\n**Example:**\n```tsx\n<Toggle label=\"Enable notifications\" checked={enabled} onChange={setEnabled} size=\"md\" />\n```\n\n### RadioGroup\n\nRadioGroup -- A group of mutually exclusive radio options with optional label and error.\n\nRenders a `<div role=\"radiogroup\">` containing a label span, a flex container of\nradio options, and an optional error message. Each option is a `<label>` with a\nvisually-hidden native `<input type=\"radio\">` and a custom 18px circle indicator.\n\nStyling:\n- Selected circle: `2px solid var(--ggui-color-primary-600)` border with\n an 8px `var(--ggui-color-primary-600)` filled inner dot\n- Unselected circle: `2px solid var(--ggui-color-outline)` border,\n `var(--ggui-color-container)` fill\n- Circle radius: `var(--ggui-shape-radius-full)`\n- Transition: all 0.2s\n- Vertical gap: `var(--ggui-spacing-2)`, horizontal gap: `var(--ggui-spacing-4)`\n- Error: `var(--ggui-font-size-xs)`, `var(--ggui-color-error-500)`,\n `role=\"alert\"`\n- Disabled options: `opacity: 0.5`, `cursor: not-allowed`\n\nAccessibility: the group has `role=\"radiogroup\"` with `aria-labelledby` pointing\nto the label and `aria-describedby` pointing to the error message (when present).\nAll radio inputs share a common auto-generated `name` attribute.\n\n**IMPORTANT:** `onChange` receives the selected option's value string directly,\nNOT a React `ChangeEvent`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Group label rendered above the options. Used as `aria-labelledby` target on the `role=\"radiogroup\"` container. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| value | `string` | - | Controlled value. Should match one of `options[].value`. |\n| onChange | `(value: string) => void` | - | Change handler. Receives the newly selected option's value string directly, NOT a React event. |\n| options | `RadioOption[]` | - | Array of radio options. Must contain at least two options for meaningful selection. |\n| direction | `'vertical' \\| 'horizontal'` | `'vertical'` | Layout direction for the options container: - `'vertical'` -- column layout, `var(--ggui-spacing-2)` gap - `'horizontal'` -- row layout with `flex-wrap: wrap`, `var(--ggui-spacing-4)` gap |\n| disabled | `boolean` | `false` | When true, disables ALL options (individual `RadioOption.disabled` is additive). Each option gets `opacity: 0.5` and `cursor: not-allowed`. |\n| error | `string` | - | Error message displayed below all options in `var(--ggui-color-error-500)` with `role=\"alert\"`. Linked to the radiogroup via `aria-describedby`. |\n\n**Example:**\n```tsx\n<RadioGroup\n label=\"Plan\"\n value={plan}\n onChange={setPlan}\n options={[\n { value: 'free', label: 'Free', description: 'Up to 5 projects' },\n { value: 'pro', label: 'Pro', description: 'Unlimited projects' },\n ]}\n/>\n```\n\n### Slider\n\nSlider -- A range input with a custom-styled track, fill, and thumb.\n\nRenders a `<div>` wrapper containing an optional label/value header, and a\ntrack area with three layers: background track, colored fill, and a circular\nthumb. A native `<input type=\"range\">` is overlaid with `opacity: 0` to\nprovide accessible keyboard and pointer interaction.\n\nStyling:\n- Track: 6px tall, `var(--ggui-color-outlineVariant)` background, `border-radius: 3px`\n- Fill: `var(--ggui-color-primary-600)` (normal),\n `var(--ggui-color-outline)` (disabled)\n- Thumb: 20px white circle with `2px solid var(--ggui-color-primary-600)`,\n `var(--ggui-shape-shadow-sm)`; disabled border uses outline\n- Value display (when `showValue`): `var(--ggui-color-primary-600)`,\n `var(--ggui-font-size-sm)`, right-aligned in the header row\n- Fill and thumb transitions: 0.1s for smooth dragging\n\nAccessibility: the native `<input type=\"range\">` carries `aria-valuenow`,\n`aria-valuemin`, `aria-valuemax`, and is linked to the label via `aria-labelledby`.\nFalls back to `aria-label=\"Slider\"` when no label is provided.\n\n**IMPORTANT:** `onChange` receives the numeric value directly, NOT a React\n`ChangeEvent`. The value is coerced via `Number(e.target.value)`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Label rendered above the slider track (left-aligned). Used as `aria-labelledby` target on the native range input. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| value | `number` | `0` | Controlled numeric value. Must be between `min` and `max`. |\n| onChange | `(value: number) => void` | - | Change handler. Receives the new numeric value directly, NOT a React event. |\n| min | `number` | `0` | Minimum allowed value. |\n| max | `number` | `100` | Maximum allowed value. |\n| step | `number` | `1` | Step increment for the slider. Determines the granularity of selectable values. |\n| disabled | `boolean` | `false` | When true, sets `cursor: not-allowed` on the native input. The fill color changes to `var(--ggui-color-outline)` and the thumb border also uses outline. |\n| showValue | `boolean` | `false` | When true, displays the current numeric value right-aligned in the header row (beside the label) in `var(--ggui-color-primary-600)`. |\n\n**Example:**\n```tsx\n<Slider label=\"Volume\" value={volume} onChange={setVolume} min={0} max={100} step={5} showValue />\n```\n\n### Badge\n\nBadge -- Inline label for status indicators, counts, or categories.\n\nRenders a `<span>` with `display: inline-flex`, centered content, and\n`white-space: nowrap`. Semantic variant colors use background/text pairings\nfrom the 100/700 color scale. Pill shape uses `border-radius: 9999px`;\nnon-pill uses `var(--ggui-shape-radius-sm)`.\n\nFont weight: `var(--ggui-font-weight-medium)` across all variants.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| variant | `'default' \\| 'primary' \\| 'secondary' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'tertiary'` | `'default'` | Visual style. Maps to background/text color pairings: - `'default'` -- bg `var(--ggui-color-sunken)`, text `var(--ggui-color-onSunken)` - `'primary'` -- bg `var(--ggui-color-primary-100)`, text `var(--ggui-color-primary-700)` - `'secondary'` -- bg `var(--ggui-color-outlineVariant)`, text `var(--ggui-color-onContainer)` - `'success'` -- bg `var(--ggui-color-success-100)`, text `var(--ggui-color-success-700)` - `'warning'` -- bg `var(--ggui-color-warning-100)`, text `var(--ggui-color-warning-700)` - `'error'` -- bg `var(--ggui-color-error-100)`, text `var(--ggui-color-error-700)` - `'info'` -- bg `var(--ggui-color-info-100)`, text `var(--ggui-color-info-700)` |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls padding and font size: - `'sm'` -- padding `2px 6px`, font `var(--ggui-font-size-xs)` - `'md'` -- padding `2px 8px`, font `var(--ggui-font-size-xs)` - `'lg'` -- padding `4px 10px`, font `var(--ggui-font-size-sm)` |\n| pill | `boolean` | `true` | When true, uses fully rounded corners (`border-radius: 9999px`). When false, uses `var(--ggui-shape-radius-sm)`. |\n\n**Example:**\n```tsx\n<Badge variant=\"success\" size=\"sm\">Active</Badge>\n```\n\n### Spinner\n\nSpinner -- Animated SVG loading indicator.\n\nRenders an `<svg>` with `role=\"status\"` and `aria-label=\"Loading\"`.\nThe SVG contains a full outlineVariant background circle and a quarter-arc\nforeground stroke in the spinner color.\n\nAnimation: `ggui-spin 1s linear infinite` (360-degree rotation).\nThe `@keyframes ggui-spin` definition is injected inline via a `<style>` tag.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| size | `number` | `24` | Width and height of the SVG element in pixels. The internal viewBox is always `0 0 24 24`, so this controls rendered size only. |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `undefined (uses `var(--ggui-color-primary-600)`)` | Semantic color slot for the animated foreground arc. Same vocabulary as ; the theme decides the resolved value. The background circle always uses `var(--ggui-color-outlineVariant)`. Use `'inherit'` when the spinner sits inside a colored container (e.g. inside a Button) \u2014 the stroke picks up `currentColor` from the parent so it tracks the container's foreground. |\n\n**Example:**\n```tsx\n<Spinner size={32} tone=\"success\" />\n```\n\n### Avatar\n\nAvatar -- User or entity representation with image or auto-generated initials.\n\nRenders a `<div role=\"img\">` with `overflow: hidden` and `flex-shrink: 0`.\nWhen `src` is provided and loads successfully, renders an `<img>` with\n`object-fit: cover`. On image error (or when no `src`), falls back to\ninitials derived from `name` (up to 2 characters, uppercase).\n\nInitials background: deterministic color from a 5-color palette based on\nname hash (primary-500, success-500, warning-500, error-500, info-500).\nFalls back to `var(--ggui-color-outline)` when no name is given.\nInitials text: white, `font-weight: var(--ggui-font-weight-semibold)`,\n`font-size: resolvedSize * 0.4`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| src | `string` | - | Image URL. When provided and the image loads, it is rendered with `object-fit: cover`. On load error, falls back to initials. |\n| name | `string` | - | Name used for two purposes: 1. Generating initials (splits on spaces, takes first letter of each word, max 2). 2. Deterministic background color selection via character code hash. Also used as `aria-label` on the container. Falls back to `'Avatar'` if omitted. |\n| size | `number \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'` | `'md'` | Avatar dimensions. Named sizes map to pixel values: - `'xs'` -- 24px - `'sm'` -- 32px - `'md'` -- 40px - `'lg'` -- 48px - `'xl'` -- 64px Numeric values are used directly as pixel dimensions. |\n| shape | `'circle' \\| 'square'` | `'circle'` | Container shape. - `'circle'` -- `border-radius: 50%` - `'square'` -- `border-radius: var(--ggui-shape-radius-md)` |\n\n**Example:**\n```tsx\n<Avatar src=\"/photos/jane.jpg\" name=\"Jane Doe\" size=\"lg\" shape=\"circle\" />\n```\n\n### Alert\n\nAlert -- Contextual message box for important information with icon and optional dismiss.\n\nRenders a `<div role=\"alert\">` with flex layout (12px gap), variant-specific\nbackground, border, text color, and a leading icon. Each variant provides a\ndefault SVG icon (info circle, checkmark, warning triangle, or X circle) that\ncan be overridden via the `icon` prop.\n\nLayout: icon (flex-shrink: 0) | content column (title + body) | close button.\nBorder radius: `var(--ggui-shape-radius-lg)`.\nPadding: `12px 16px`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| variant | `'info' \\| 'success' \\| 'warning' \\| 'error'` | `'info'` | Visual style. Maps to background/border/text/icon color sets: - `'info'` -- bg `var(--ggui-color-info-50)`, border `var(--ggui-color-info-200)`, text `var(--ggui-color-info-800)`, icon `var(--ggui-color-info-500)` - `'success'` -- bg `var(--ggui-color-success-50)`, border `var(--ggui-color-success-200)`, text `var(--ggui-color-success-800)`, icon `var(--ggui-color-success-500)` - `'warning'` -- bg `var(--ggui-color-warning-50)`, border `var(--ggui-color-warning-200)`, text `var(--ggui-color-warning-800)`, icon `var(--ggui-color-warning-500)` - `'error'` -- bg `var(--ggui-color-error-50)`, border `var(--ggui-color-error-200)`, text `var(--ggui-color-error-800)`, icon `var(--ggui-color-error-500)` |\n| title | `string` | - | Optional title rendered above the body in semibold (`var(--ggui-font-weight-semibold)`), `var(--ggui-font-size-sm)`. Title and body are separated by `var(--ggui-spacing-1)` gap. |\n| closable | `boolean` | `false` | When true, renders a close button (X icon) in the top-right area. The button has `min-width: 28px`, `min-height: 28px`, and `opacity: 0.7`. Requires `onClose` to be functional. |\n| onClose | `() => void` | - | Callback fired when the close button is clicked. Only relevant when `closable` is true. |\n| icon | `ReactNode` | - | Custom icon ReactNode to replace the default variant icon. Rendered at the leading position with the variant's icon color applied via `color` CSS property. |\n\n**Example:**\n```tsx\n<Alert variant=\"warning\" title=\"Rate limit\" closable onClose={() => setShow(false)}>\n You have 3 requests remaining this minute.\n</Alert>\n```\n\n### Progress\n\nProgress -- Horizontal progress bar with determinate and indeterminate modes.\n\nRenders a track `<div role=\"progressbar\">` with a colored fill child.\nThe track background is `var(--ggui-color-outlineVariant)` with\npill-shaped corners (border-radius = height / 2).\n\nDeterminate mode: fill width transitions smoothly (`width 0.3s ease`).\nIndeterminate mode: fill is 30% width, animated with\n`ggui-progress-indeterminate 1.5s ease-in-out infinite`\n(translateX from -100% to 400%). The `@keyframes` are injected inline.\n\nAccessibility: `aria-valuenow` is set in determinate mode, omitted in\nindeterminate. `aria-valuemin` is always 0, `aria-valuemax` matches `max`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| value | `number` | - | Current progress value. Clamped to `[0, max]` and converted to a percentage for the fill width: `Math.min(100, Math.max(0, (value / max) * 100))`. |\n| max | `number` | `100` | Maximum value representing 100% progress. |\n| variant | `'default' \\| 'success' \\| 'warning' \\| 'error'` | `'default'` | Fill bar color. Maps to CSS variables: - `'default'` -- `var(--ggui-color-primary-600)` - `'success'` -- `var(--ggui-color-success-500)` - `'warning'` -- `var(--ggui-color-warning-500)` - `'error'` -- `var(--ggui-color-error-500)` |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls track height in pixels: - `'sm'` -- 4px - `'md'` -- 8px - `'lg'` -- 12px |\n| label | `string` | - | Accessible name describing what this bar measures, e.g. `\"Survey progress\"` or `\"Upload\"`. Becomes the progressbar's `aria-label` and \u2014 when `showLabel` is set \u2014 the visible header text in place of the generic word \"Progress\". Always pass this when the surrounding context does not already make the meaning obvious. |\n| showLabel | `boolean` | `false` | When true, displays a header row above the track with the `label` text (or \"Progress\" if `label` is unset) on the left, and the rounded percentage value on the right. |\n| indeterminate | `boolean` | `false` | When true, ignores `value` for visual width and plays a looping animation instead. The fill bar is 30% width and slides across the track. Animation: `ggui-progress-indeterminate 1.5s ease-in-out infinite`. `aria-valuenow` is omitted from the progressbar element. |\n\n**Example:**\n```tsx\n<Progress value={65} variant=\"success\" size=\"md\" showLabel />\n```\n\n### Image\n\nImage -- An `<img>` element with built-in error handling and fallback support.\n\nRenders a native `<img>` with `display: block`. On load error, either renders\nthe `fallback` ReactNode (if provided) or a default placeholder `<div>` with a\nsurfaceVariant background and a centered image SVG icon in outline.\n\nSize values: numbers are treated as pixels, strings are passed through as-is.\nWhen no `width` is set, defaults to `100%`. When no `height` is set, defaults\nto `auto`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| src | `string` | - | Image source URL. Load failure triggers the fallback state. |\n| alt | `string` | - | Alt text for the image. Used as `aria-label` in the error placeholder too. |\n| width | `number \\| string` | `'100%' (applied at render time, not on the type)` | Image width. Numbers are pixels, strings are CSS values (e.g., `'100%'`, `'50vw'`). |\n| height | `number \\| string` | `'auto' (applied at render time, not on the type)` | Image height. Numbers are pixels, strings are CSS values. |\n| objectFit | `'cover' \\| 'contain' \\| 'fill' \\| 'none' \\| 'scale-down'` | `'cover'` | CSS `object-fit` value controlling how the image fills its box. |\n| radius | `'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| number \\| string` | - | Corner radius applied to both the image and the error placeholder. Prefer a radius-scale name (`'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'`) \u2014 each resolves to the matching `--ggui-shape-radius-*` token. A number is treated as pixels; any other string is passed through. |\n| fallback | `ReactNode` | - | Custom ReactNode rendered when the image fails to load. When provided, completely replaces the default error placeholder (no wrapper div). When omitted, a surfaceVariant background div with an image icon is shown. |\n\n**Example:**\n```tsx\n<Image src=\"/hero.jpg\" alt=\"Hero banner\" width=\"100%\" height={400} objectFit=\"cover\" radius=\"md\" />\n```\n\n### Icon\n\nIcon -- a curated Lucide subset (~200 names) + emoji passthrough.\n\nThree resolution layers:\n1. **Lucide icon:** pass a name from the curated subset (e.g. `sun`, `cloud-rain`, `heart`, `shopping-cart`, `sparkles`, `arrow-up-right`).\n Accepts kebab-case, camelCase, or PascalCase. Renders as stroke SVG. ONLY the subset renders: an\n unknown name renders an empty box (never a placeholder glyph) \u2014 the `get_available_icons` tool lists the exact names.\n2. **Emoji:** pass emoji/unicode directly (e.g. `\u2600\uFE0F`, `\u{1F327}\uFE0F`). Rendered as text.\n3. **Custom SVG:** pass children (`<svg>` element) for full control.\n\nContainer: `<span>` with `display: inline-flex`, centered content.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| name | `string` | - | Lucide icon name from the curated subset (kebab-case, camelCase, or PascalCase all work); a name outside the subset renders an empty box. Also accepts emoji/unicode characters directly. |\n| size | `number` | `24` | Icon dimensions in pixels (applied to both width and height of the wrapper and the inner SVG element). |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `undefined (icon uses `currentColor`)` | Semantic color slot. Same vocabulary as ; the theme decides what each tone LOOKS like. Resolves to a CSS `color` on the wrapper which the inner SVG inherits via `currentColor`. Use `'inherit'` (the default behavior when unset) for icons that should pick up the parent's foreground color. |\n| children | `ReactNode` | - | Custom SVG children. When provided, `name` is ignored and children are rendered inside a sized `<span>` wrapper. |\n| 'aria-label' | `string` | - | Accessible name for a standalone, meaning-bearing icon. When set, the icon exposes `role=\"img\"` + this label. When omitted (the default) the icon is decorative and hidden from screen readers (`aria-hidden`) \u2014 the right choice for an icon next to a text label. |\n\n**Example:**\n```tsx\n<Icon name=\"search\" size={20} tone=\"muted\" />\n<Icon name=\"cloud-rain\" size={32} />\n<Icon name=\"\u2600\uFE0F\" size={24} />\n```\n\n### Link\n\nLink -- Styled anchor element with external link support.\n\nRenders a native `<a>` element. When `external` is true, sets\n`target=\"_blank\"` and `rel=\"noopener noreferrer\"`, and appends a small\n(12px) external-link SVG icon after the children.\n\nTransition: `color 0.2s`.\nUnderline behavior is controlled via mouseEnter/mouseLeave event handlers\n(for the `'hover'` mode).\n\nAlso extends native `AnchorHTMLAttributes` (except `style`/`className`),\nso props like `aria-*`, `data-*`, `title`, etc. are forwarded to the `<a>`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| href | `string` | - | Destination URL. Passed directly to the `<a href>` attribute. |\n| external | `boolean` | `false` | When true, opens link in a new tab (`target=\"_blank\"`, `rel=\"noopener noreferrer\"`) and appends a 12px external-link icon after children. |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `undefined (uses `var(--ggui-color-primary-600)`)` | Semantic color slot for the link text. Same vocabulary as ; the theme decides what each tone LOOKS like. Defaults to a primary-tinted accent (`'loud'`-ish) when unset. |\n| underline | `'always' \\| 'hover' \\| 'none'` | `'hover'` | Underline behavior: - `'always'` -- `text-decoration: underline` at all times - `'hover'` -- underline appears on mouse enter, removed on mouse leave - `'none'` -- no underline ever |\n\n**Example:**\n```tsx\n<Link href=\"https://docs.ggui.ai\" external>Documentation</Link>\n```\n\n### Tooltip\n\nTooltip -- Hoverable information popup positioned relative to a trigger element.\n\nWraps `children` in a `<div>` trigger (display: inline-block) and renders\na fixed-position tooltip `<div role=\"tooltip\">` when visible.\n\nTooltip appearance:\n- Background: `var(--ggui-color-onContainer)`\n- Text: white, `var(--ggui-font-size-xs)`\n- Padding: `6px 10px`, border-radius: `var(--ggui-shape-radius-md)`\n- Max width: 200px, `white-space: nowrap`, `pointer-events: none`\n- Z-index: `zIndex.tooltip` (1800)\n\nShow/hide: triggered by mouseEnter/mouseLeave AND focus/blur on the\ntrigger element. Uses `position: fixed` with coordinates calculated from\n`getBoundingClientRect()` and an 8px offset from the trigger edge.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | Trigger element. Wrapped in a `<div>` with mouseEnter/mouseLeave and focus/blur handlers. |\n| content | `ReactNode` | - | Tooltip content. Can be text or any ReactNode. |\n| position | `'top' \\| 'bottom' \\| 'left' \\| 'right'` | `'top'` | Tooltip placement relative to the trigger element: - `'top'` -- above, centered horizontally, transformed `translateX(-50%) translateY(-100%)` - `'bottom'` -- below, centered horizontally, transformed `translateX(-50%)` - `'left'` -- to the left, centered vertically, transformed `translateX(-100%) translateY(-50%)` - `'right'` -- to the right, centered vertically, transformed `translateY(-50%)` |\n| delay | `number` | `200` | Delay in milliseconds before the tooltip becomes visible after hover/focus. Hiding is immediate (no delay). |\n\n**Example:**\n```tsx\n<Tooltip content=\"Copy to clipboard\" position=\"top\">\n <Button variant=\"ghost\"><Icon name=\"copy\" /></Button>\n</Tooltip>\n```\n\n### Table\n\nTable -- Data table with sortable columns, striped rows, and hover highlights.\n\nRenders a scrollable wrapper `<div>` containing a native `<table>` with\n`border-collapse: collapse` and `width: 100%`. The wrapper has\n`overflow-x: auto` for horizontal scrolling on narrow viewports.\n\nHeader row: 2px bottom border (`var(--ggui-color-outlineVariant)`).\nData rows: 1px bottom border (`var(--ggui-color-sunken)`).\nHover: `var(--ggui-color-container)` background with 150ms ease transition.\nStriped: alternating rows (odd index) get `var(--ggui-color-container)`.\n\nSort behavior: clicking a sortable column header calls `onSort(key, direction)`.\nIf the same column is clicked again while ascending, it toggles to descending.\nThe component does NOT sort data internally -- the parent must sort `data` and\npass updated `sortKey`/`sortDirection`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| columns | `TableColumn<T>[]` | - | Array of column definitions controlling header labels, data keys, and rendering. |\n| data | `T[]` | - | Array of row data objects. Each object's keys should match the column `key` values. |\n| sortKey | `string` | - | The `key` of the currently sorted column. Used to highlight the active sort indicator and determine toggle direction on next click. |\n| sortDirection | `SortDirection` | `'asc'` | Current sort direction for the column identified by `sortKey`. Controls which triangle indicator is highlighted in the header. |\n| onSort | `(key: string, direction: SortDirection) => void` | - | Sort change handler. Called when a sortable column header is clicked. Receives the column `key` and the new `SortDirection`. The component does NOT sort data internally -- you must sort `data` in your state and pass updated `sortKey`/`sortDirection`. |\n| striped | `boolean` | `false` | When true, alternating rows (odd index) get a `var(--ggui-color-container)` background. |\n| hoverable | `boolean` | `true` | When true, rows highlight with `var(--ggui-color-container)` on mouse enter, with a 150ms ease background-color transition. |\n| compact | `boolean` | `false` | When true, reduces cell padding: - Compact: `var(--ggui-spacing-1) var(--ggui-spacing-2)` - Normal: `var(--ggui-spacing-2) var(--ggui-spacing-4)` |\n| bordered | `boolean` | `false` | When true, adds a 1px border around the table wrapper and between cells. Wrapper border: `1px solid var(--ggui-color-outlineVariant)`. Cell borders: `1px solid var(--ggui-color-sunken)`. Wrapper border-radius: `var(--ggui-shape-radius-lg)`. |\n| caption | `string` | - | Accessible table caption. Rendered as a `<caption>` element with `caption-side: top`, `var(--ggui-font-size-sm)`, `var(--ggui-color-onSunken)`. |\n\n**Example:**\n```tsx\n<Table\n columns={[\n { key: 'name', header: 'Name', sortable: true },\n { key: 'role', header: 'Role' },\n { key: 'status', header: 'Status', render: (v) => <Badge variant={v as string}>{v as string}</Badge> },\n ]}\n data={users}\n sortKey={sortKey}\n sortDirection={sortDir}\n onSort={(key, dir) => { setSortKey(key); setSortDir(dir); }}\n striped\n/>\n```\n\n### Tabs\n\nTabs -- Accessible tab navigation with panels and keyboard support.\n\nRenders a `<div role=\"tablist\">` with `<button role=\"tab\">` elements and a\n`<div role=\"tabpanel\">` for the active tab's content. Supports controlled\n(`activeKey` + `onChange`) and uncontrolled (internal state) modes.\n\nKeyboard navigation: ArrowLeft/ArrowRight (and ArrowUp/ArrowDown) cycle\nthrough enabled tabs. Home/End jump to first/last. Focus follows selection.\nDisabled tabs are skipped during keyboard navigation.\n\n**IMPORTANT:** `onChange` receives the tab's `key` string directly, NOT a\nReact event.\n\nTab panel padding: `var(--ggui-spacing-4) 0` (top/bottom only).\nTransitions: color, background-color, border-color at 200ms ease-in-out.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| items | `TabItem[]` | - | Array of tab definitions. Must contain at least one item. |\n| activeKey | `string` | - | Controlled active tab key. When provided, the component is controlled and will not manage its own state. Must match one of `items[].key`. When omitted, defaults to the first item's key (uncontrolled mode). |\n| onChange | `(key: string) => void` | - | Tab change handler. Receives the selected tab's `key` string directly, NOT a React event. In controlled mode, you must update `activeKey` in response to this callback. |\n| variant | `'line' \\| 'pills' \\| 'enclosed'` | `'line'` | Visual style of the tab bar: - `'line'` -- underline indicator (2px solid primary-600 on active), border-bottom on tab list - `'pills'` -- filled pill buttons (primary-600 bg, white text on active), surfaceVariant container with radius-lg - `'enclosed'` -- bordered tab buttons with open bottom (card-style), border-bottom on tab list |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls tab button padding and font size: - `'sm'` -- padding `var(--ggui-spacing-1) var(--ggui-spacing-2)`, font `var(--ggui-font-size-xs)` - `'md'` -- padding `var(--ggui-spacing-2) var(--ggui-spacing-4)`, font `var(--ggui-font-size-sm)` - `'lg'` -- padding `var(--ggui-spacing-4) var(--ggui-spacing-6)`, font `var(--ggui-font-size-base)` |\n| fullWidth | `boolean` | `false` | When true, tab buttons expand equally to fill the container width (`flex: 1`, `justify-content: center` on each button). |\n\n**Example:**\n```tsx\n<Tabs\n variant=\"pills\"\n items={[\n { key: 'overview', label: 'Overview', content: <Overview /> },\n { key: 'settings', label: 'Settings', content: <Settings /> },\n ]}\n activeKey={tab}\n onChange={setTab}\n/>\n```\n\n### Toast\n\nToast -- Notification banner with auto-dismiss and slide-in animation.\n\nRenders a `<div role=\"alert\" aria-live=\"assertive\">` with a variant-specific\nicon, optional title, message body, and optional close button.\n\nAnimation: `ggui-slideInUp 200ms ease-out both` on mount (from the motion\ntoken system). The keyframes are provided by the MotionKeyframes provider.\n\nAuto-dismiss: when `onClose` is provided and `duration > 0`, a timer calls\n`onClose` after `duration` ms. Setting `duration` to `0` disables auto-dismiss.\nThe timer resets if `visible`, `duration`, or `onClose` changes.\n\nDimensions: `min-width: 280px`, `max-width: 420px`.\nShadow: `var(--ggui-shape-shadow-lg)`.\nBorder radius: `var(--ggui-shape-radius-lg)`.\n\nWhen `visible` is false, renders nothing (returns `null`).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| message | `ReactNode` | - | Message body content. Can be text or any ReactNode. |\n| variant | `'info' \\| 'success' \\| 'warning' \\| 'error'` | `'info'` | Visual style. Maps to background/border/text/icon color sets (same palette as Alert): - `'info'` -- bg `var(--ggui-color-info-50)`, border `var(--ggui-color-info-200)`, text `var(--ggui-color-info-800)` - `'success'` -- bg `var(--ggui-color-success-50)`, border `var(--ggui-color-success-200)`, text `var(--ggui-color-success-800)` - `'warning'` -- bg `var(--ggui-color-warning-50)`, border `var(--ggui-color-warning-200)`, text `var(--ggui-color-warning-800)` - `'error'` -- bg `var(--ggui-color-error-50)`, border `var(--ggui-color-error-200)`, text `var(--ggui-color-error-800)` |\n| title | `string` | - | Optional title rendered above the message in semibold (`var(--ggui-font-weight-semibold)`, `var(--ggui-font-size-sm)`). |\n| duration | `number` | `5000` | Auto-dismiss delay in milliseconds. After this duration, `onClose` is called automatically. Set to `0` to disable auto-dismiss (toast stays until manually closed). The timer is only active when both `visible` is true and `onClose` is provided. |\n| onClose | `() => void` | - | Callback fired on auto-dismiss timeout or when the close button is clicked. When provided, a close button (X icon, 16px) is rendered in the top-right area. When omitted, no close button is shown and auto-dismiss is disabled. |\n| visible | `boolean` | `true` | Controls rendering. When false, the component returns `null`. Toggling from false to true triggers the slide-in animation. |\n| position | `'top-right' \\| 'top-left' \\| 'bottom-right' \\| 'bottom-left' \\| 'top-center' \\| 'bottom-center'` | - | Intended screen position. This prop is defined on the interface but is NOT implemented by the Toast component itself -- positioning must be handled by a parent container or toast manager. |\n\n**Example:**\n```tsx\n<Toast variant=\"success\" title=\"Saved\" message=\"Your changes have been saved.\" onClose={() => setShow(false)} />\n```\n\n### Accordion\n\nAccordion -- Collapsible content sections with chevron rotation animation.\n\nRenders a vertical list of items, each with a `<button>` header (inside `<h3>`)\nand a `<div role=\"region\">` panel. Supports controlled (`expandedKeys` + `onChange`)\nand uncontrolled (internal state) modes.\n\nChevron animation: the trailing chevron icon rotates from 0deg (collapsed) to\n180deg (expanded) with `transition: transform 200ms ease-in-out`.\n\nHeader button: full-width flex layout (`justify-content: space-between`),\n`var(--ggui-font-size-sm)`, `var(--ggui-font-weight-medium)`,\n`var(--ggui-color-onContainer)`.\nHeader padding: `var(--ggui-spacing-2) var(--ggui-spacing-4)`.\nBackground transition: `background-color 100ms ease-in-out`.\n\n**IMPORTANT:** `onChange` receives the full array of currently expanded keys,\nNOT a single key or a React event. In single mode (`multiple: false`), this\narray will have at most one element.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| items | `AccordionItem[]` | - | Array of collapsible section definitions. |\n| expandedKeys | `string[]` | - | Controlled expanded state. Array of item `key` values that should be open. When provided, the component is controlled and will not manage its own expansion state. When omitted, defaults to `[]` (all collapsed, uncontrolled mode). |\n| onChange | `(expandedKeys: string[]) => void` | - | Expand/collapse handler. Receives the complete array of expanded keys after a toggle. In controlled mode, you must update `expandedKeys` in response to this callback. |\n| multiple | `boolean` | `false` | When true, multiple items can be open simultaneously. When false, opening one item closes any other open item (single-expand mode). |\n| variant | `'default' \\| 'bordered' \\| 'separated'` | `'default'` | Visual style controlling borders and spacing: - `'default'` -- top border on first item, bottom border on all items (`var(--ggui-color-outlineVariant)`), no gap between items - `'bordered'` -- connected card style with left/right/bottom borders on all items, top border on first, shared rounded corners (radius-lg on first/last) - `'separated'` -- each item is an independent card with full border (`1px solid var(--ggui-color-outlineVariant)`), `var(--ggui-shape-radius-lg)` radius, `var(--ggui-spacing-2)` gap between items |\n\n**Example:**\n```tsx\n<Accordion\n variant=\"separated\"\n items={[\n { key: 'faq1', title: 'How do I get started?', content: 'Sign up and...' },\n { key: 'faq2', title: 'What is the pricing?', content: 'We offer...' },\n ]}\n expandedKeys={expanded}\n onChange={setExpanded}\n multiple\n/>\n```\n\n### Support Types\n\n**ResponsiveColumns:**\n\nExplicit column count per viewport breakpoint, mobile-first. `base`\napplies from 0 up; each named key overrides at and above its\nbreakpoint width (`sm` 640px, `md` 768px, `lg` 1024px, `xl` 1280px).\nOmit `base` to default to a single column on the narrowest screens.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| base | `number` | Columns below the `sm` breakpoint. |\n| sm | `number` | Columns from 640px up. |\n| md | `number` | Columns from 768px up. |\n| lg | `number` | Columns from 1024px up. |\n| xl | `number` | Columns from 1280px up. |\n\n**SelectOption:**\n\nAn individual option within a `Select` dropdown.\n\nRendered as a native `<option>` element. When `disabled` is true, the option\nis visible but not selectable.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| value | `string` | The value submitted when this option is selected. Must be unique within the options array. |\n| label | `string` | The display text shown in the dropdown. |\n| disabled | `boolean` | When true, the option is visible but cannot be selected (grayed out by the browser). |\n\n**RadioOption:**\n\nAn individual option within a `RadioGroup`.\n\nRendered as a `<label>` containing a visually-hidden `<input type=\"radio\">`\nand a custom 18px circle indicator. Supports an optional description line\nbelow the label text.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| value | `string` | The value emitted via `RadioGroupProps.onChange` when this option is selected. Must be unique. |\n| label | `string` | Display text for this option. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| description | `string` | Optional secondary description rendered below the label in smaller, muted type (`var(--ggui-font-size-xs)`, `var(--ggui-color-onSunken)`). |\n| disabled | `boolean` | When true, this individual option is grayed out (`opacity: 0.5`) and cannot be selected, regardless of the group-level `disabled` prop. |\n\n**TableColumn:**\n\nColumn definition for the Table component.\n\nEach column maps a `key` in the row data object to a table column with\na header label, optional sorting, alignment, width, and custom rendering.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| key | `string` | Property key in the row data object. Used to extract cell values via `row[key]`. Must be unique across all columns in a Table. |\n| header | `string` | Column header text. Rendered in uppercase, `var(--ggui-font-size-xs)`, `var(--ggui-font-weight-semibold)`, `var(--ggui-color-onSunken)`, with `letter-spacing: 0.05em`. |\n| width | `number \\| string` | Fixed column width. Numbers are pixels, strings are CSS values (e.g., `'200px'`, `'30%'`). When omitted, the column auto-sizes based on content. |\n| align | `'left' \\| 'center' \\| 'right'` | Horizontal text alignment for both the header and data cells. |\n| sortable | `boolean` | When true, the header cell becomes clickable and shows sort direction indicators (ascending/descending triangles). Clicking toggles between `'asc'` and `'desc'`. The header gets `cursor: pointer`, `tabIndex: 0`, and keyboard support (Enter/Space to toggle). |\n| render | `(value: unknown, row: T, index: number) => ReactNode` | Custom cell renderer. When provided, called instead of rendering `row[key]` directly. Receives the cell value, the full row object, and the row index. |\n\n**TabItem:**\n\nDefinition of a single tab within a Tabs component.\n\nEach item provides a unique `key` for identification, a `label` for the\ntab button, and `content` for the associated panel.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| key | `string` | Unique identifier for this tab. Used to match `activeKey` and as the value passed to `onChange`. |\n| label | `ReactNode` | Tab button label. Can be text or any ReactNode. |\n| content | `ReactNode` | Panel content rendered below the tab bar when this tab is active. |\n| disabled | `boolean` | When true, the tab button shows `opacity: 0.5`, `cursor: not-allowed`, and cannot be selected via click or keyboard navigation. |\n| icon | `ReactNode` | Optional icon rendered before the label inside the tab button, with `var(--ggui-spacing-1)` gap between icon and label. |\n\n**AccordionItem:**\n\nDefinition of a single collapsible section within an Accordion.\n\nEach item provides a unique `key`, a clickable `title` for the header button,\nand `content` revealed when expanded.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| key | `string` | Unique identifier for this item. Used in `expandedKeys` and passed to `onChange`. |\n| title | `ReactNode` | Header label rendered inside the toggle button. Can be text or any ReactNode. |\n| content | `ReactNode` | Panel content rendered below the header when expanded. Styled with `var(--ggui-font-size-sm)`, `var(--ggui-color-onSunken)`, `line-height: var(--ggui-font-lineHeight-normal)`. Padding: `0 var(--ggui-spacing-4) var(--ggui-spacing-4)`. |\n| disabled | `boolean` | When true, the header button shows `opacity: 0.5`, `cursor: not-allowed`, and cannot be toggled. |\n\n\n## Components\n\nImport: `import { Component } from '@ggui-ai/design'`\n\n### SearchField\n\nA text input with a leading search icon and optional submit button.\n\nComposes: `Input` (native `<input type=\"search\">`), `Button`, `Spinner`, `Icon`.\n\nSupports controlled and uncontrolled usage. When `value` is `undefined` the\ncomponent tracks its own state internally. Pressing **Enter** triggers\n`onSearch` with the current value. When `loading` is `true` the search icon\nis replaced by a `Spinner` and the input is disabled.\n\nTokens used: `colors.gray[300]` border, `colors.gray[50]` disabled bg,\n`colors.gray[400]` icon color, `colors.gray[900]` text color.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| value | `string` | - | Current search value. When provided, the component is **controlled** and the caller must update this via `onChange`. When omitted, the component manages its own internal state. |\n| onChange | `(value: string) => void` | - | Called on every keystroke with the new input string (value directly, not a React `ChangeEvent`). |\n| onSearch | `(value: string) => void` | - | Called when the user presses **Enter** or clicks the search button (if `showButton` is `true`). Receives the current value directly. |\n| placeholder | `string` | `'Search...'` | Placeholder text shown when the input is empty. |\n| showButton | `boolean` | `false` | When `true`, renders a `Button` primitive to the right of the input. The button's label is set by `buttonText`. |\n| buttonText | `string` | `'Search'` | Label rendered inside the submit button. Only visible when `showButton` is `true`. |\n| loading | `boolean` | - | When `true`, replaces the search icon with a `Spinner`, disables the input, and disables the submit button. |\n| disabled | `boolean` | - | When `true`, the input and button are visually disabled and do not respond to interaction. The input background changes to `colors.gray[50]`. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls input height and font size. - `'sm'` -- 6px vertical padding, 14px font - `'md'` -- 10px vertical padding, 14px font - `'lg'` -- 12px vertical padding, 16px font The Button size maps `sm`->`sm`, `md`->`md`, `lg`->`md`. |\n\n**Example:**\n```tsx\n```tsx\n<SearchField\n value={query}\n onChange={setQuery}\n onSearch={(q) => fetchResults(q)}\n placeholder=\"Search products...\"\n showButton\n buttonText=\"Go\"\n size=\"md\"\n/>\n```\n```\n\n### FormField\n\nA wrapper that adds a label, optional description, error message,\nand helper text around any form input passed as `children`.\n\nComposes: no other primitives -- pure layout with semantic `<label>` and\n`<span>` elements.\n\nVisual hierarchy (top to bottom):\n1. **Label** (required) -- `fontSize.sm`, `fontWeight.medium`, `colors.gray[700]`\n2. **Required indicator** -- red asterisk (`colors.error[500]`) appended to label\n3. **Description** -- `fontSize.xs`, `colors.gray[500]`, 4px bottom margin\n4. **Children** -- the form control itself\n5. **Error / Helper text** -- `fontSize.xs`; error in `colors.error[500]`,\n helper in `colors.gray[500]`. Error takes priority when both are provided.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Text rendered inside the `<label>` element above the input. |\n| children | `ReactNode` | - | The form control (typically an `Input`, `Select`, or `Textarea` primitive) rendered between the label/description and the error/helper row. |\n| error | `string` | - | Error message displayed below `children` in `colors.error[500]`. When present, it takes precedence over `helperText`. |\n| helperText | `string` | - | Neutral guidance text displayed below `children` in `colors.gray[500]`. Hidden when `error` is present. |\n| required | `boolean` | - | When `true`, appends a red asterisk (`*`) after the label text. Does **not** add any HTML validation attributes -- handle that on the child input. |\n| description | `string` | - | Secondary description rendered between the label and the child control in `fontSize.xs` / `colors.gray[500]`. Use for longer guidance that does not belong in `helperText`. |\n\n**Example:**\n```tsx\n```tsx\n<FormField\n label=\"Email address\"\n required\n description=\"We will never share your email.\"\n error={errors.email}\n>\n <Input value={email} onChange={setEmail} placeholder=\"you@example.com\" />\n</FormField>\n```\n```\n\n### MenuItem\n\nA full-width clickable row for menus, sidebars, and action lists.\n\nComposes: none -- renders a native `<button>` element.\n\nBuilt-in transition: `background-color 0.15s` on hover.\n\nColor logic:\n- **Normal**: text `colors.gray[700]`, hover bg `colors.gray[100]`\n- **Active**: bg `colors.primary[50]`, text `colors.gray[700]`, `fontWeight.medium`\n- **Danger**: text `colors.error[600]`, hover bg `colors.error[50]`, active bg `colors.error[100]`\n- **Disabled**: text `colors.gray[400]`, `cursor: not-allowed`\n\nLayout: flexbox row with `8px` gap, `8px 12px` padding, `radius.md` border-radius.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Primary text content of the menu item. |\n| icon | `ReactNode` | - | Icon or element rendered to the left of the label. Flex-shrink 0. |\n| rightElement | `ReactNode` | - | Element rendered to the right of the label (e.g., a keyboard shortcut badge or a count). Colored `colors.gray[400]`, flex-shrink 0. |\n| onClick | `() => void` | - | Called when the item is clicked. Suppressed when `disabled` is `true`. |\n| disabled | `boolean` | - | When `true`, the item is non-interactive: `colors.gray[400]` text, `cursor: not-allowed`, click handler suppressed. |\n| active | `boolean` | - | Marks this item as the current selection. Applies a tinted background (`colors.primary[50]`, or `colors.error[100]` when `danger` is also set) and `fontWeight.medium`. |\n| danger | `boolean` | - | Switches the item to destructive styling: `colors.error[600]` text, `colors.error[50]` hover background. |\n\n**Example:**\n```tsx\n```tsx\n<MenuItem\n label=\"Delete project\"\n icon={<Icon name=\"trash\" size={16} />}\n danger\n onClick={() => confirmDelete(projectId)}\n/>\n```\n```\n\n### Tag\n\nAn inline label for categories, filters, statuses, or selections.\nOptionally dismissable via a close button.\n\nComposes: none -- pure `<span>` with an optional close `<button>`.\n\nEach `variant` maps to a background / text / border color triple from the\ndesign tokens:\n- `'default'` -- `gray[100]` / `gray[700]` / `gray[200]`\n- `'primary'` -- `primary[50]` / `primary[700]` / `primary[200]`\n- `'success'` -- `success[50]` / `success[700]` / `success[200]`\n- `'warning'` -- `warning[50]` / `warning[700]` / `warning[200]`\n- `'error'` -- `error[50]` / `error[700]` / `error[200]`\n- `'info'` -- `info[50]` / `info[700]` / `info[200]`\n\nThe close button renders an inline SVG \"x\" icon (12x12) with `opacity: 0.7`\nand an `aria-label=\"Remove\"`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | Tag content -- typically a short text string. |\n| variant | `'default' \\| 'primary' \\| 'success' \\| 'warning' \\| 'error' \\| 'info'` | `'default'` | Semantic color variant applied to background, text, and border. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls padding, font size, and internal gap. - `'sm'` -- 2px/6px padding, `fontSize.xs`, 4px gap - `'md'` -- 4px/8px padding, `fontSize.xs`, 6px gap - `'lg'` -- 6px/10px padding, `fontSize.sm`, 6px gap |\n| closable | `boolean` | - | When `true`, renders a small close (\"x\") button after the content. Clicking it fires `onClose`. |\n| onClose | `() => void` | - | Called when the close button is clicked. Only relevant when `closable` is `true`. |\n| icon | `ReactNode` | - | Icon or element rendered before the text content. |\n\n**Example:**\n```tsx\n```tsx\n<Tag variant=\"success\" size=\"md\" closable onClose={() => removeFilter(id)}>\n Active\n</Tag>\n```\n```\n\n### Dropdown\n\nA click-triggered menu anchored to a trigger element. Manages its own\nopen/close state internally.\n\nComposes: `MenuItem` for each option.\n\nBehavior:\n- Clicking the trigger toggles the menu open/closed.\n- Selecting an option calls `onChange(option.value)` and closes the menu.\n- Clicking outside the container or pressing **Escape** closes the menu.\n- The currently selected option (matching `value`) is rendered with\n `MenuItem`'s `active` state.\n\nMenu panel: `colors.white` bg, `colors.gray[200]` border, `radius.lg`\nborder-radius, `shadow.lg`, `zIndex.dropdown`, 160px min-width, 4px padding.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| trigger | `ReactNode` | - | The element the user clicks to open the menu. Receives a wrapping `<div>` with `cursor: pointer` (or `not-allowed` when disabled). |\n| options | `DropdownOption[]` | - | Array of selectable options rendered as `MenuItem` rows. |\n| value | `string` | - | The `value` of the currently selected option. The matching `MenuItem` is rendered with `active` styling. |\n| onChange | `(value: string) => void` | - | Called with the `value` string of the selected option (not the full `DropdownOption` object). The menu closes immediately after. |\n| placement | `'bottom-start' \\| 'bottom-end' \\| 'top-start' \\| 'top-end'` | `'bottom-start'` | Where to anchor the menu panel relative to the trigger. - `'bottom-start'` -- below, aligned to left edge - `'bottom-end'` -- below, aligned to right edge - `'top-start'` -- above, aligned to left edge - `'top-end'` -- above, aligned to right edge |\n| disabled | `boolean` | - | When `true`, the trigger shows `cursor: not-allowed` and clicking it does not open the menu. |\n\n**Example:**\n```tsx\n```tsx\n<Dropdown\n trigger={<Button variant=\"outline\">Sort by</Button>}\n options={[\n { value: 'name', label: 'Name' },\n { value: 'date', label: 'Date created' },\n { value: 'delete', label: 'Delete', danger: true },\n ]}\n value={sortBy}\n onChange={setSortBy}\n placement=\"bottom-end\"\n/>\n```\n```\n\n### Autocomplete\n\nA text input with a filterable suggestion dropdown, keyboard navigation,\nand loading/empty states.\n\nComposes: `Input` primitive (with `label`, `error`, `placeholder` forwarded),\n`Spinner` (loading state).\n\nFiltering: options are filtered client-side by case-insensitive substring\nmatch against both `option.label` and `option.value`.\n\nKeyboard support:\n- **ArrowDown / ArrowUp** -- move highlight through filtered options\n (opens dropdown if closed)\n- **Enter** -- selects the highlighted option\n- **Escape** -- closes the dropdown\n\nOn selection, `onChange` is called with `option.label` (the display text)\nand `onSelect` is called with the full `AutocompleteOption` object.\n\nDropdown panel: `colors.white` bg, `colors.gray[200]` border, `radius.lg`\nborder-radius, `shadow.lg`, `zIndex.dropdown`, max-height 240px with\noverflow scroll.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| value | `string` | `''` | Current text in the input field. On selection, this is set to the selected option's `label`. |\n| onChange | `(value: string) => void` | - | Called on every keystroke with the new input string (value directly, not a React event). Also called on selection with `option.label`. |\n| onSelect | `(option: AutocompleteOption) => void` | - | Called when the user selects an option (click or Enter on highlighted item). Receives the full `AutocompleteOption` object, not just the value string. |\n| options | `AutocompleteOption[]` | - | The full list of available options. Filtering is handled internally via case-insensitive substring match on `label` and `value`. |\n| placeholder | `string` | - | Placeholder text forwarded to the inner `Input` primitive. |\n| label | `string` | - | Label text forwarded to the inner `Input` primitive. |\n| loading | `boolean` | - | When `true`, the dropdown shows a centered `Spinner` instead of the option list. The input remains interactive. |\n| disabled | `boolean` | - | When `true`, the inner `Input` is disabled and the dropdown does not open. |\n| error | `string` | - | Error message forwarded to the inner `Input` primitive. |\n| noResultsText | `string` | `'No results found'` | Text shown in the dropdown when filtering produces zero matches. Rendered centered in `colors.gray[500]` / `fontSize.sm`. |\n\n**Example:**\n```tsx\n```tsx\n<Autocomplete\n label=\"Country\"\n value={country}\n onChange={setCountry}\n onSelect={(opt) => setCountryCode(opt.value)}\n options={countries}\n placeholder=\"Type to search...\"\n noResultsText=\"No countries found\"\n/>\n```\n```\n\n### Breadcrumb\n\nA horizontal navigation trail showing the user's location within a\nhierarchy. Renders a `<nav aria-label=\"Breadcrumb\">`.\n\nComposes: `Link` primitive (for items with `href`).\n\nRendering rules per item:\n- **Last item**: static `<span>` with `colors.gray[900]`, `fontWeight: 500`,\n and `aria-current=\"page\"`.\n- **Non-last with `href`**: `Link` in `colors.gray[500]` with\n `underline=\"hover\"`. If `onItemClick` is provided, `e.preventDefault()`\n is called and the handler fires instead of navigating.\n- **Non-last without `href`**: unstyled `<button>` in `colors.gray[500]`.\n\nLayout: flexbox row, `8px` gap, `fontSize.sm`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| items | `BreadcrumbItem[]` | - | Ordered array of breadcrumb segments from root to current page. |\n| separator | `ReactNode` | `'/'` | Separator rendered between each pair of items. Can be a string (e.g., `\"/\"`, `\">\"`) or a ReactNode (e.g., an `Icon`). Rendered in `colors.gray[400]`. |\n| onItemClick | `(item: BreadcrumbItem, index: number) => void` | - | Called when a non-last item is clicked. Receives the `BreadcrumbItem` and its zero-based `index`. When provided on items that have `href`, the default navigation is prevented via `e.preventDefault()`. |\n\n**Example:**\n```tsx\n```tsx\n<Breadcrumb\n items={[\n { label: 'Home', href: '/' },\n { label: 'Projects', href: '/projects' },\n { label: 'ggui' },\n ]}\n separator=\"/\"\n onItemClick={(item) => router.push(item.href!)}\n/>\n```\n```\n\n### Pagination\n\nPage navigation controls with previous/next arrows, numbered page buttons,\nand optional first/last jumps. Renders a `<nav aria-label=\"Pagination\">`.\n\nComposes: `Button` (ghost variant for prev/next/first/last arrows),\n`Icon` (`chevron-left`, `chevron-right`).\n\nBuilt-in transition: `all 0.15s` on page number buttons.\n\nPage windowing: when `totalPages > maxVisible`, the component shows the\nfirst page, last page, a window of pages around `currentPage`, and\nellipsis (\"...\") markers for gaps. The window adjusts when near the\nstart or end of the range.\n\nActive page button: `colors.primary[600]` bg, `colors.white` text,\n`fontWeight.medium`. Inactive: transparent bg, `colors.gray[700]` text.\n\nArrow buttons are automatically disabled at boundary pages (first/last).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| currentPage | `number` | - | Current active page. **1-indexed** (first page is `1`). |\n| totalPages | `number` | - | Total number of pages. Determines when last-page / next-page buttons disable. |\n| onPageChange | `(page: number) => void` | - | Called when the user clicks a page number, arrow, or first/last button. Receives the target page number (1-indexed) directly. |\n| showFirstLast | `boolean` | `true` | When `true`, renders double-chevron buttons for jumping to the first and last page. These buttons are disabled when already on the respective boundary. |\n| maxVisible | `number` | `5` | Maximum number of page buttons visible at once (including the first and last page, but excluding ellipsis markers). When `totalPages` exceeds this value, ellipsis gaps appear. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls button dimensions and icon sizes. - `'sm'` -- 28px buttons, 14px icons, Button size `xs` - `'md'` -- 32px buttons, 16px icons, Button size `sm` - `'lg'` -- 40px buttons, 20px icons, Button size `md` |\n| disabled | `boolean` | - | When `true`, all page buttons and arrows are visually dimmed (`opacity: 0.5`) and clicks are suppressed. |\n\n**Example:**\n```tsx\n```tsx\n<Pagination\n currentPage={page}\n totalPages={20}\n onPageChange={setPage}\n maxVisible={7}\n size=\"md\"\n/>\n```\n```\n\n### EmptyState\n\nEmptyState -- placeholder for a region with no data: empty lists,\nzero search results, an error fallback. Render it instead of\nnothing whenever a data array could be empty \u2014 a list that shows\nnothing when empty looks broken.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| icon | `string \\| ReactNode` | - | A Lucide icon name (kebab-case), rendered large and subtle above the title, or a custom node. Omit for a text-only empty state. |\n| title | `string` | - | The headline, e.g. \"No results found\". |\n| description | `string` | - | Optional supporting line below the title. |\n| action | `ReactNode` | - | Optional call-to-action, typically a `<Button>`. |\n\n**Example:**\n```tsx\n{results.length === 0\n ? <EmptyState icon=\"search-x\" title=\"No matches\" description=\"Try a broader query.\" />\n : results.map((r) => <Row key={r.id}>\u2026</Row>)}\n```\n\n### Stat\n\nStat -- a single KPI / metric: a label, a large value, an optional\ntrend-coloured delta and icon. Reach for it whenever the UI is\n\"show a number\" \u2014 dashboards, weather and price cards, analytics\ntiles. Drop several into a `<Grid>` for a stat grid.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | The metric name, e.g. \"Revenue\". Rendered small and uppercase above the value. |\n| value | `string \\| number` | - | The headline value \u2014 the big number. A number, or a pre-formatted string (`\"$48.2k\"`, `\"18\xB0C\"`). |\n| delta | `string` | - | Optional change indicator, pre-formatted, e.g. `\"+12.5%\"` or `\"-3\"`. |\n| trend | `'up' \\| 'down' \\| 'neutral'` | `'neutral'` | Direction of `delta` \u2014 colours it: `'up'` success, `'down'` error, `'neutral'` muted. |\n| icon | `string \\| ReactNode` | - | Optional Lucide icon name (kebab-case) or custom node, shown next to the label. |\n\n**Example:**\n```tsx\n<Stat label=\"Revenue\" value=\"$48.2k\" delta=\"+12.5%\" trend=\"up\" icon=\"trending-up\" />\n```\n\n### Stepper\n\nStepper -- a display-only step indicator for multi-step flows\n(wizards, checkouts, onboarding): numbered markers joined by\nconnector lines, with completed (check), current (filled), and\nupcoming (outlined) states styled distinctly.\n\nStepper renders step state; it never owns it. Declare the step\nlabels as a top-level `const`, track the current index in\n`useState`, and advance it from your own Next/Back buttons (or\n`onStepClick`).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| steps | `string[]` | - | Ordered step labels, one per step. Declare as a top-level `const` array \u2014 never as state. |\n| current | `number` | - | Zero-based index of the active step. Steps before it render as completed (check mark), steps after it as upcoming. |\n| orientation | `'horizontal' \\| 'vertical'` | `'horizontal'` | Flow direction. `'horizontal'` joins the markers with connector lines in a row; `'vertical'` stacks the steps with short connector columns \u2014 better for sidebars or long step labels. |\n| onStepClick | `(index: number) => void` | - | When provided, each step becomes a real button and clicks receive the step's zero-based index. Typical use: jumping back to a completed step; guard forward jumps in the handler if the flow validates per step. |\n\n**Example:**\n```tsx\nconst STEPS = ['Account', 'Profile', 'Review'];\n// In the component: const [step, setStep] = useState(0);\n<Stack gap=\"lg\">\n <Stepper steps={STEPS} current={step} />\n {step === 0 && <AccountFields />}\n <Row gap=\"sm\" justify=\"end\">\n <Button variant=\"ghost\" disabled={step === 0} onClick={() => setStep(step - 1)}>Back</Button>\n <Button onClick={() => setStep(step + 1)}>Next</Button>\n </Row>\n</Stack>\n```\n\n### Markdown\n\nMarkdown -- renders markdown-formatted text as styled content:\nheadings, paragraphs, **bold** / *italic*, `inline code`, fenced\ncode blocks, bulleted and numbered lists, pipe tables, and links.\n\nUse it whenever a prop or context slot carries prose the agent\nauthors in markdown -- article bodies, AI-generated explanations,\ndescriptions, notes, help text. Pass the raw markdown string\nstraight through; never pre-parse it, never split it into per-line\n`Text` elements, and never rebuild formatting with nested\n`<strong>`/`<em>` markup yourself.\n\nSafe by construction: raw HTML in the source displays as literal\ntext (never markup), and links render inert unless their scheme is\nhttp(s)/mailto. Streaming-tolerant: a mid-stream prefix parses\ncleanly and completed constructs never change shape, so binding a\ngrowing string re-renders correctly.\n\nFor a single line of formatted text INSIDE another element (a table\ncell, a list row, a caption), use `MarkdownInline` instead -- same\n`markdown` prop, inline-level constructs only (no headings, lists,\nor code fences), renders inside the parent's typography.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| markdown | `string` | - | Markdown source text, passed through verbatim. Supports the conversational subset: `#`-`###` headings, paragraphs, bold, italic, inline code, fenced code blocks, `-`/`1.` lists, `\\|` pipe tables, and `[label](url)` links. Bind stateful or streamed prose directly. |\n\n**Example:**\n```tsx\n<Stack gap=\"md\">\n <Markdown markdown={recipe.instructions} />\n</Stack>\n```\n\n### MarkdownInline\n\nMarkdownInline -- renders ONE line of inline-level markdown (bold,\nitalic, inline code, links) inside the parent's typography -- a table\ncell, a list row, a caption, a summary line. Same `markdown` prop as\n`Markdown`; block constructs (headings, lists, code fences) are not\nrendered -- reach for `Markdown` for multi-paragraph bodies.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| markdown | `string` | - | Inline markdown source -- bold, italic, inline code, and `[label](url)` links. Block-level syntax renders as plain text. |\n\n**Example:**\n```tsx\n<Row gap=\"sm\">\n <Badge variant=\"success\">stable</Badge>\n <MarkdownInline markdown={release.summary} />\n</Row>\n```\n\n### Support Types\n\n**DropdownOption:**\n\nA single option inside a `Dropdown`. Each option maps to one `MenuItem`\ninternally.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| value | `string` | Unique identifier returned to `onChange` when this option is selected. |\n| label | `string` | Human-readable text shown in the menu row. |\n| icon | `ReactNode` | Optional icon rendered to the left of the label via `MenuItem.icon`. |\n| disabled | `boolean` | When `true`, the option is visible but non-interactive. |\n| danger | `boolean` | When `true`, the option uses destructive (red) styling via `MenuItem.danger`. |\n\n**AutocompleteOption:**\n\nA single option in the `Autocomplete` suggestion list.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| value | `string` | Unique identifier for this option. Also used for case-insensitive filtering against the input value. |\n| label | `string` | Primary display text. Also used for case-insensitive filtering and is written into the input on selection. |\n| description | `string` | Secondary description rendered below the label in `fontSize.xs` / `colors.gray[500]`. |\n| icon | `ReactNode` | Icon rendered to the left of the label/description block. Flex-shrink 0. |\n| disabled | `boolean` | When `true`, the option is visible but non-interactive (`cursor: not-allowed`, `colors.gray[400]` text). |\n\n**BreadcrumbItem:**\n\nA single segment in a `Breadcrumb` trail. Items with `href` render as\n`Link` primitives; items without render as plain `<button>` elements.\nThe last item in the array is always rendered as static text with\n`aria-current=\"page\"`.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| label | `string` | Display text for this breadcrumb segment. |\n| href | `string` | URL for this segment. When provided (and this is not the last item), the segment renders as a `Link` primitive with `underline=\"hover\"`. When omitted, it renders as a `<button>`. |\n| icon | `ReactNode` | Icon rendered immediately before the label. Its color follows the segment's text color: `colors.gray[500]` for navigable items, `colors.gray[900]` for the current (last) item. |\n\n\n## Compositions\n\nImport: `import { Component } from '@ggui-ai/design'`\n\n### Header\n\nProps for the `Header` composition.\n\nA horizontal page header that arranges a logo, navigation, and action slots\nin a flex row (`justify-content: space-between`). Internally renders a `<header>` element;\ndoes not compose other ggui primitives.\n\nWhen `sticky` is true the header gets `position: sticky; top: 0` with `zIndex.sticky`\nand a `shadow.sm` box-shadow.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| logo | `ReactNode` | - | Logo or brand element rendered at the start (flex-shrink: 0). |\n| navigation | `ReactNode` | - | Navigation content rendered in a `<nav>` element with `flex: 1` and a 32 px left margin. |\n| actions | `ReactNode` | - | Right-side action elements (buttons, avatar, etc.) rendered with a 12 px gap. |\n| sticky | `boolean` | `false` | When true, the header becomes `position: sticky` at the top of its scroll container with `zIndex.sticky` and `shadow.sm`. |\n| background | `string` | `colors.white` | Background color of the header. |\n| bordered | `boolean` | `true` | When true, renders a 1 px bottom border in `colors.gray[200]`. |\n\n**Example:**\n```tsx\n```tsx\n<Header\n logo={<img src=\"/logo.svg\" alt=\"Acme\" />}\n navigation={<a href=\"/docs\">Docs</a>}\n actions={<Button size=\"sm\">Sign In</Button>}\n sticky\n bordered\n/>\n```\n```\n\n### Sidebar\n\nProps for the `Sidebar` composition.\n\nA vertical navigation panel that composes the `Icon` primitive for chevron indicators.\nItems are rendered as `<button>` elements inside a scrollable `<nav>`. Nested items\nare indented 16 px per depth level. The sidebar animates width changes with a 200 ms\nCSS transition. Active items are highlighted with `colors.primary[50]` background\nand `colors.primary[700]` text.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| items | `SidebarItem[]` | - | Array of navigation items to render. |\n| activeId | `string` | - | ID of the currently active item. Matched items get a highlighted background and bold text. |\n| onItemClick | `(item: SidebarItem) => void` | - | Called when any item (including parent items with children) is clicked. |\n| collapsed | `boolean` | `false` | When true, hides labels and badges; only icons remain visible, centered in the collapsed width. Nested children are hidden entirely. |\n| header | `ReactNode` | - | Content rendered above the item list, separated by a bottom border. |\n| footer | `ReactNode` | - | Content rendered below the item list, separated by a top border. |\n| width | `number` | `256` | Width in pixels when expanded. |\n| collapsedWidth | `number` | `64` | Width in pixels when collapsed. |\n\n**Example:**\n```tsx\n```tsx\n<Sidebar\n items={[\n { id: 'home', label: 'Home', icon: <Icon name=\"home\" /> },\n { id: 'settings', label: 'Settings', icon: <Icon name=\"settings\" />,\n children: [\n { id: 'profile', label: 'Profile' },\n { id: 'billing', label: 'Billing' },\n ]},\n ]}\n activeId=\"home\"\n onItemClick={(item) => navigate(item.href)}\n collapsed={false}\n width={256}\n/>\n```\n```\n\n### CardGrid\n\nProps for the `CardGrid` composition.\n\nA CSS Grid wrapper that arranges children in equal-width columns. When `columns`\nis a number, it produces `repeat(N, 1fr)`. When it is a responsive object,\nit falls back to `repeat(auto-fit, minmax(280px, 1fr))` for fluid responsive behavior.\n\nDoes not compose any ggui primitives internally \u2014 it is a pure layout container.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | Card elements to arrange in the grid. |\n| columns | `number \\| { sm?: number; md?: number; lg?: number }` | `3` | Number of columns, or a responsive breakpoint map. - `number` \u2014 fixed column count via `repeat(N, 1fr)`. - `{ sm?, md?, lg? }` \u2014 triggers `repeat(auto-fit, minmax(280px, 1fr))` for fluid layout. |\n| gap | `number` | `16` | Gap between grid items in pixels. |\n\n**Example:**\n```tsx\n```tsx\n<CardGrid columns={3} gap={24}>\n <Card>A</Card>\n <Card>B</Card>\n <Card>C</Card>\n</CardGrid>\n```\n```\n\n### CommentThread\n\nProps for the `CommentThread` composition.\n\nA threaded comment section that composes `Avatar`, `Button`, `TextArea`, and `Spinner`\nprimitives. Comments are rendered recursively with 40 px indentation per nesting level.\nEach comment shows author avatar, name, timestamp, content, reactions, and a \"Reply\" toggle.\nWhen `currentUser` is provided, a new-comment input with avatar and submit button is shown\nabove the thread.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| comments | `Comment[]` | - | Array of top-level comments to render. Replies are nested within each comment. |\n| currentUser | `{ /** Display name for the current user's avatar. */ name: string; /** Avatar image URL for the current user. */ avatar?: string; }` | - | Current user info. When provided, a new-comment input area is rendered above the thread. |\n| onAddComment | `(content: string, parentId?: string) => void` | - | Called when the user submits a new top-level comment. |\n| onReply | `(commentId: string, content: string) => void` | - | Called when the user submits a reply to an existing comment. |\n| onReaction | `(commentId: string, emoji: string) => void` | - | Called when the user clicks an emoji reaction on a comment. |\n| loading | `boolean` | - | When true, shows a centered `Spinner` instead of the comment list. |\n\n**Example:**\n```tsx\n```tsx\n<CommentThread\n comments={[\n { id: '1', author: { name: 'Alice' }, content: 'Great work!', timestamp: new Date(),\n replies: [{ id: '2', author: { name: 'Bob' }, content: 'Thanks!', timestamp: new Date() }] }\n ]}\n currentUser={{ name: 'Alice', avatar: '/alice.jpg' }}\n onAddComment={(content) => post(content)}\n onReply={(commentId, content) => reply(commentId, content)}\n onReaction={(commentId, emoji) => react(commentId, emoji)}\n/>\n```\n```\n\n### DataTable\n\nProps for the `DataTable` composition.\n\nA sortable, selectable data table that composes `Checkbox`, `Spinner`, and `Icon`\nprimitives. Renders a `<table>` inside a bordered container with 8 px border-radius.\nThe header row has a `colors.gray[50]` background. Sortable columns show a chevron\nicon on click (toggles asc/desc). Selected rows are highlighted with `colors.primary[50]`.\nRow background transitions use a 150 ms ease. The \"select all\" checkbox supports an\nindeterminate state when a subset of rows is selected.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| columns | `DataTableColumn<T>[]` | - | Column definitions that control header, alignment, sorting, and rendering. |\n| data | `T[]` | - | Row data array. Each entry corresponds to one table row. |\n| rowKey | `string \\| ((row: T) => string)` | `'id'` | Property name or function used to derive a unique key for each row. |\n| loading | `boolean` | - | When true, shows a centered `Spinner` instead of table body rows. |\n| emptyText | `string` | `'No data'` | Text shown when `data` is empty and not loading. |\n| onSort | `(key: string, direction: 'asc' \\| 'desc') => void` | - | Called when a sortable column header is clicked. Toggles direction automatically (asc -> desc) if the same column is clicked again. |\n| sortKey | `string` | - | The column key currently being sorted. |\n| sortDirection | `'asc' \\| 'desc'` | - | The current sort direction. |\n| onRowClick | `(row: T, index: number) => void` | - | Called when a row is clicked. Rows get `cursor: pointer` when this handler is provided. |\n| selectable | `boolean` | - | When true, adds a checkbox column at the start of each row. |\n| selectedKeys | `string[]` | `[]` | Array of currently selected row keys. |\n| onSelectionChange | `(keys: string[]) => void` | - | Called when the set of selected row keys changes (via row checkbox or select-all). |\n\n**Example:**\n```tsx\n```tsx\n<DataTable\n columns={[\n { key: 'name', header: 'Name', sortable: true },\n { key: 'email', header: 'Email' },\n { key: 'role', header: 'Role', render: (v) => <Badge>{v}</Badge> },\n ]}\n data={users}\n rowKey=\"id\"\n selectable\n selectedKeys={selected}\n onSelectionChange={setSelected}\n onSort={(key, dir) => sort(key, dir)}\n sortKey=\"name\"\n sortDirection=\"asc\"\n/>\n```\n```\n\n### ChatWindow\n\nProps for the `ChatWindow` composition.\n\nA messaging interface that composes `Avatar`, `Button`, `Spinner`, and `Icon` primitives.\nLayout is a flex column filling 100% height with a bordered `radius.lg` container.\nMessages from the current user align right with `colors.primary[600]` bubbles and white text.\nOther users' messages align left with `colors.gray[100]` bubbles. The message area\nauto-scrolls to the bottom on new messages via `scrollIntoView({ behavior: 'smooth' })`.\nThe text input sends on Enter (Shift+Enter for newline).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| messages | `ChatMessage[]` | - | Array of chat messages to display in chronological order. |\n| currentUserId | `string` | - | ID of the current user. Messages from this user render right-aligned with a primary color bubble. |\n| onSendMessage | `(content: string) => void` | - | Called when the user submits a message (Enter key or send button). |\n| loading | `boolean` | - | When true, shows a centered `Spinner` instead of the message list. |\n| typing | `{ name: string } \\| null` | - | When non-null, displays a typing indicator below the last message (e.g., \"Alice is typing...\"). |\n| placeholder | `string` | `'Type a message...'` | Placeholder text for the message input field. |\n| header | `ReactNode` | - | Optional header content rendered above the message area, separated by a bottom border. |\n\n**Example:**\n```tsx\n```tsx\n<ChatWindow\n messages={messages}\n currentUserId=\"user-1\"\n onSendMessage={(content) => send(content)}\n typing={{ name: 'Alice' }}\n placeholder=\"Type a message...\"\n header={<h3>Chat with Alice</h3>}\n/>\n```\n```\n\n### NavigationBar\n\nProps for the `NavigationBar` composition.\n\nA horizontal or vertical navigation menu. Does not compose other ggui primitives\n(uses plain `<button>` and `<a>` elements). Active items are styled per variant:\n\n- `'default'` \u2014 active item gets `colors.primary[600]` text and medium font weight.\n- `'pills'` \u2014 active item gets a `radius.full` pill with `colors.primary[100]` background.\n- `'underline'` \u2014 active item gets a 2 px bottom border in `colors.primary[600]`.\n When horizontal, the entire nav also has a 1 px bottom border.\n\nAll items have a 150 ms transition on all properties.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| items | `NavItem[]` | - | Array of navigation items to render. |\n| activeId | `string` | - | ID of the currently active item. Controls visual highlighting per variant style. |\n| onItemClick | `(item: NavItem) => void` | - | Called when a non-disabled item is clicked. For `<a>` elements, `preventDefault` is called first. |\n| orientation | `'horizontal' \\| 'vertical'` | `'horizontal'` | Layout direction. - `'horizontal'` \u2014 flex-row with 4 px gap. - `'vertical'` \u2014 flex-column with 2 px gap. |\n| variant | `'default' \\| 'pills' \\| 'underline'` | `'default'` | Visual style for active/inactive items. - `'default'` \u2014 text color change only. - `'pills'` \u2014 rounded pill background on active items. - `'underline'` \u2014 bottom border on active items. |\n\n**Example:**\n```tsx\n```tsx\n<NavigationBar\n items={[\n { id: 'home', label: 'Home', icon: <Icon name=\"home\" /> },\n { id: 'about', label: 'About' },\n { id: 'contact', label: 'Contact' },\n ]}\n activeId=\"home\"\n onItemClick={(item) => navigate(item.id)}\n orientation=\"horizontal\"\n variant=\"pills\"\n/>\n```\n```\n\n### FileUploader\n\nProps for the `FileUploader` composition.\n\nA drag-and-drop file upload area that composes `Button`, `Progress`, and `Icon` primitives.\nThe drop zone is a dashed-border container that highlights in `colors.primary[400]`/`colors.primary[50]`\non drag-over. Below the drop zone, each file in `files` is listed with its name, size,\noptional progress bar (for uploading status), and a remove button.\n\nFile validation (max size, max count) is applied client-side before calling `onFilesSelected`.\nFiles exceeding `maxSize` are silently filtered out; files exceeding `maxFiles` are truncated.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| files | `UploadedFile[]` | `[]` | Array of files currently in the upload queue, shown below the drop zone. |\n| onFilesSelected | `(files: File[]) => void` | - | Called when the user selects files via click or drag-and-drop. Receives native `File` objects after client-side filtering (maxSize, maxFiles). |\n| onFileRemove | `(fileId: string) => void` | - | Called when the user clicks the remove button on a file entry. |\n| accept | `string` | - | Accepted file types passed to the hidden `<input type=\"file\" accept=\"...\">`. E.g., `'image/*,.pdf'`. |\n| multiple | `boolean` | `true` | When true, allows selecting multiple files at once. |\n| maxSize | `number` | - | Maximum file size in bytes. Files exceeding this are silently excluded from the selection. |\n| maxFiles | `number` | - | Maximum number of files. Excess files beyond `maxFiles - files.length` are truncated. |\n| disabled | `boolean` | - | When true, the drop zone is visually dimmed (opacity 0.5) and non-interactive. |\n| dragDrop | `boolean` | `true` | When true, enables drag-and-drop on the drop zone. When false, the prompt text changes to \"Click to browse files\". |\n\n**Example:**\n```tsx\n```tsx\n<FileUploader\n files={uploadedFiles}\n onFilesSelected={(files) => startUpload(files)}\n onFileRemove={(id) => removeFile(id)}\n accept=\"image/*,.pdf\"\n multiple\n maxSize={5 * 1024 * 1024}\n maxFiles={10}\n dragDrop\n/>\n```\n```\n\n### NotificationCenter\n\nProps for the `NotificationCenter` composition.\n\nA notification list with a header bar that composes `Button`, `Spinner`, and `Icon`\nprimitives. The header shows a \"Notifications\" title, an unread count badge\n(pill in `colors.primary[100]`), and \"Mark all read\" / \"Clear all\" buttons.\nEach notification item shows a colored status dot, title, message, timestamp,\n\"Mark as read\" link, and optional action. The list is scrollable (`overflowY: auto`).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| notifications | `Notification[]` | - | Array of notifications to display, in the order provided. |\n| onMarkAsRead | `(id: string) => void` | - | Called when the user clicks \"Mark as read\" on an individual notification. |\n| onMarkAllAsRead | `() => void` | - | Called when the user clicks the \"Mark all read\" header button. Only shown when there are unread items. |\n| onDismiss | `(id: string) => void` | - | Called when the user clicks the dismiss (X) button on an individual notification. |\n| onClearAll | `() => void` | - | Called when the user clicks the \"Clear all\" header button. Only shown when there are any notifications. |\n| emptyText | `string` | `'No notifications'` | Text shown when `notifications` is empty and not loading. |\n| loading | `boolean` | - | When true, shows a centered `Spinner` instead of the notification list. |\n\n**Example:**\n```tsx\n```tsx\n<NotificationCenter\n notifications={[\n { id: '1', title: 'Deployment complete', type: 'success', timestamp: new Date(), read: false },\n { id: '2', title: 'Build failed', type: 'error', message: 'Lint errors', timestamp: new Date() },\n ]}\n onMarkAsRead={(id) => markRead(id)}\n onMarkAllAsRead={() => markAllRead()}\n onDismiss={(id) => dismiss(id)}\n onClearAll={() => clearAll()}\n/>\n```\n```\n\n### Modal\n\nProps for the `Modal` composition.\n\nA dialog overlay that composes `Button`, `Icon`, and `Heading` primitives.\nThe overlay uses `animation.fadeIn` (`ggui-fadeIn`) and the dialog panel uses\n`animation.scaleIn` (`ggui-scaleIn`) \u2014 both GPU-composited (opacity + transform).\nWhen open, `document.body.style.overflow` is set to `'hidden'` to prevent background\nscrolling, and restored on close. The dialog has `role=\"dialog\"` and `aria-modal=\"true\"`.\n\nSize widths: `sm` = 400 px, `md` = 500 px, `lg` = 640 px, `xl` = 800 px,\n`full` = 100vw (no border-radius, no padding).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| open | `boolean` | - | Controls modal visibility. When false, the component renders nothing. |\n| onClose | `() => void` | - | Called to close the modal (overlay click, escape key, or close button). |\n| title | `string` | - | Optional title rendered in the modal header via the `Heading` primitive (level 4). |\n| children | `ReactNode` | - | Modal body content rendered in a scrollable area (`overflowY: auto`). |\n| footer | `ReactNode` | - | Footer content rendered below the body, right-aligned with an 8 px gap, separated by a top border. |\n| size | `'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| 'full'` | `'md'` | Controls the width of the modal panel. - `'sm'` \u2014 400 px - `'md'` \u2014 500 px - `'lg'` \u2014 640 px - `'xl'` \u2014 800 px - `'full'` \u2014 100vw, no border-radius, stretches to fill viewport |\n| closeOnOverlayClick | `boolean` | `true` | When true, clicking the semi-transparent overlay behind the modal calls `onClose`. |\n| closeOnEscape | `boolean` | `true` | When true, pressing the Escape key calls `onClose`. |\n| showCloseButton | `boolean` | `true` | When true, renders a ghost close button (X icon) in the modal header. |\n\n**Example:**\n```tsx\n```tsx\n<Modal\n open={isOpen}\n onClose={() => setOpen(false)}\n title=\"Confirm Action\"\n size=\"md\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={() => setOpen(false)}>Cancel</Button>\n <Button onClick={handleConfirm}>Confirm</Button>\n </>\n }\n>\n <p>Are you sure you want to proceed?</p>\n</Modal>\n```\n```\n\n### CommandPalette\n\nProps for the `CommandPalette` composition.\n\nA searchable command menu (Cmd+K / Ctrl+K pattern) that composes `Spinner` and `Icon`\nprimitives. Appears as a centered overlay at 15vh from the top. Commands are filtered\nby label and description (case-insensitive substring match). Results are grouped under\nuppercase section headers. Keyboard navigation is fully supported:\nArrow Up/Down to navigate, Enter to select, Escape to close.\n\nWhen `recentIds` are provided and the search query is empty, matching commands appear\nin a \"Recent\" section at the top (deduplicated from their original groups).\n\nThe footer shows navigation hints: \"Up/Down Navigate\", \"Enter Select\", \"Esc Close\".\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| open | `boolean` | - | Controls palette visibility. When false, the component renders nothing. |\n| onClose | `() => void` | - | Called to close the palette (overlay click, Escape key, or after command selection). |\n| commands | `Command[]` | - | Full array of available commands. Filtered client-side by the search query. |\n| onSelect | `(command: Command) => void` | - | Called when a non-disabled command is selected (Enter key or click). The palette auto-closes after selection. |\n| placeholder | `string` | `'Search commands...'` | Placeholder text for the search input. |\n| recentIds | `string[]` | `[]` | IDs of recently used commands. When the query is empty, these appear in a \"Recent\" section at the top of the results. |\n| loading | `boolean` | - | When true, shows a centered `Spinner` instead of the command list. |\n\n**Example:**\n```tsx\n```tsx\n<CommandPalette\n open={isOpen}\n onClose={() => setOpen(false)}\n commands={[\n { id: 'new', label: 'New File', shortcut: 'Ctrl+N', group: 'File' },\n { id: 'save', label: 'Save', shortcut: 'Ctrl+S', group: 'File' },\n { id: 'theme', label: 'Toggle Theme', group: 'Preferences' },\n ]}\n onSelect={(cmd) => executeCommand(cmd.id)}\n recentIds={['save']}\n placeholder=\"Search commands...\"\n/>\n```\n```\n\n### Footer\n\nProps for the `Footer` composition.\n\nA site footer with `role=\"contentinfo\"` that lays out a brand slot, link columns,\nsocial icons, and a bottom bar. Does not compose other ggui primitives (uses plain\nHTML elements). Content is constrained to `max-width: 1280px` with auto margins.\nLink columns use a responsive flex layout (`flex: 0 1 180px`). The bottom bar\nincludes copyright text, social links, and bottom-bar links separated by a top border.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| brand | `ReactNode` | - | Brand element (logo, tagline) rendered in a flexible column (`flex: 1 1 280px`). |\n| columns | `FooterColumn[]` | - | Array of link columns rendered in a flex-wrap layout with 48 px gap. |\n| socialLinks | `FooterSocialLink[]` | - | Social media icon links rendered in the bottom bar. |\n| bottomText | `string` | - | Text displayed at the start of the bottom bar (e.g., copyright notice). |\n| bottomLinks | `FooterLink[]` | - | Links displayed in the bottom bar after social icons (e.g., Privacy, Terms). |\n| background | `string` | `colors.gray[50]` | Background color of the footer. |\n| bordered | `boolean` | `true` | When true, renders a 1 px top border in `colors.gray[200]`. |\n\n**Example:**\n```tsx\n```tsx\n<Footer\n brand={<img src=\"/logo.svg\" alt=\"Acme\" />}\n columns={[\n { title: 'Product', links: [{ label: 'Features', href: '/features' }] },\n { title: 'Company', links: [{ label: 'About', href: '/about' }] },\n ]}\n socialLinks={[\n { label: 'Twitter', href: 'https://twitter.com/acme', icon: <TwitterIcon /> },\n ]}\n bottomText=\"© 2026 Acme Inc.\"\n bottomLinks={[{ label: 'Privacy', href: '/privacy' }]}\n bordered\n/>\n```\n```\n\n### Hero\n\nProps for the `Hero` composition.\n\nA prominent landing-page hero section that renders heading, description, CTA buttons,\nand an optional media slot. Does not compose other ggui primitives (uses plain HTML\nelements styled with design tokens). Content is constrained to `max-width: 1280px`.\n\nLayout modes:\n- `align='center'` \u2014 single-column centered layout with `max-width: 800px` text area.\n- `align='left'` \u2014 two-column side-by-side layout (50/50 split with media slot).\n\nSize controls vertical padding and font sizes:\n- `'sm'` \u2014 48 px vertical padding, 3xl heading, lg description.\n- `'md'` \u2014 80 px vertical padding, 4xl heading, xl description.\n- `'lg'` \u2014 120 px vertical padding, 5xl heading, xl description.\n\nThe primary action button uses `colors.primary[600]` fill; the secondary action uses\nan outlined style. When `overlay` is true with a `backgroundImage`, text switches to white\nand borders become semi-transparent.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| heading | `string` | - | Main heading text rendered as an `<h1>` with bold weight and tight line-height. |\n| description | `string` | - | Description paragraph rendered below the heading with relaxed line-height. |\n| primaryAction | `HeroAction` | - | Primary CTA button rendered with `colors.primary[600]` background and white text. |\n| secondaryAction | `HeroAction` | - | Secondary CTA button rendered with a transparent background and a 1 px border. |\n| media | `ReactNode` | - | Media element (image, video, illustration) rendered beside or below the text content. |\n| align | `'center' \\| 'left'` | `'center'` | Text and layout alignment. - `'center'` \u2014 centered single-column layout. - `'left'` \u2014 left-aligned text with media in a right column (50/50 split). |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls vertical padding and heading/description font sizes. - `'sm'` \u2014 compact (48 px padding, 3xl/lg fonts). - `'md'` \u2014 standard (80 px padding, 4xl/xl fonts). - `'lg'` \u2014 spacious (120 px padding, 5xl/xl fonts). |\n| background | `string` | `colors.white (when no backgroundImage is set)` | Background color of the hero section. |\n| backgroundImage | `string` | - | Background image URL applied as `background-size: cover; background-position: center`. |\n| overlay | `boolean` | `false` | When true and `backgroundImage` is set, renders a semi-transparent black overlay (`rgba(0,0,0,0.5)`) and switches text to white/semi-transparent white for contrast. |\n\n**Example:**\n```tsx\n```tsx\n<Hero\n heading=\"Build Better UIs, Faster\"\n description=\"The universal interface layer between AI agents and humans.\"\n primaryAction={{ label: 'Get Started', onClick: () => navigate('/signup') }}\n secondaryAction={{ label: 'Learn More', href: '/docs' }}\n media={<img src=\"/hero.png\" alt=\"Hero\" />}\n align=\"left\"\n size=\"lg\"\n/>\n```\n```\n\n### Support Types\n\n**SidebarItem:**\n\nA single navigation entry in a `Sidebar`. Supports nested children for\ncollapsible sub-menus and an optional badge slot.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier used for active-state matching and React keys. |\n| label | `string` | Display label for the item. Hidden when the sidebar is collapsed. |\n| icon | `ReactNode` | Leading icon rendered before the label. Remains visible when collapsed. |\n| href | `string` | Optional URL associated with this item (not rendered as a link by default). |\n| badge | `ReactNode` | Trailing badge element (e.g., unread count). Hidden when collapsed. |\n| children | `SidebarItem[]` | Nested child items. When present, the item acts as a collapsible section (chevron indicator shown). |\n| disabled | `boolean` | When true, the item is visually dimmed and non-interactive (`cursor: not-allowed`). |\n\n**Comment:**\n\nA single comment entry in a `CommentThread`. Supports nested replies\nand emoji reactions with counts.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier for this comment. |\n| author | `{ /** Display name of the author. */ name: string; /** URL for the author's avatar image. Passed to the `Avatar` primitive. */ avatar?: string; }` | Comment author metadata. |\n| content | `string` | The comment body text. |\n| timestamp | `string \\| Date` | Timestamp of the comment. Rendered via `toLocaleString()` when a `Date` object. |\n| replies | `Comment[]` | Nested reply comments. Each reply is rendered indented 40 px deeper. |\n| reactions | `{ emoji: string; count: number }[]` | Emoji reactions with their aggregated counts (e.g., `{ emoji: \"\u{1F44D}\", count: 3 }`). |\n\n**DataTableColumn:**\n\nColumn definition for a `DataTable`. Controls header text, width, alignment,\nsorting, and custom cell rendering.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| key | `string` | Property key on the row object used to extract cell values. Also serves as the sort key. |\n| header | `string` | Column header text displayed in the `<thead>`. |\n| width | `number \\| string` | Column width as a CSS value (number for pixels, string for any CSS unit). |\n| sortable | `boolean` | When true, the column header is clickable and triggers `onSort`. An arrow icon indicates direction. |\n| render | `(value: unknown, row: T, index: number) => ReactNode` | Custom cell renderer. When omitted, the raw value is stringified via `String()`. |\n| align | `'left' \\| 'center' \\| 'right'` | Text alignment for both header and body cells. |\n\n**ChatMessage:**\n\nA single message in a `ChatWindow`. Includes sender metadata, delivery status,\nand a timestamp.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier for this message. |\n| content | `string` | The message body text. |\n| sender | `{ /** Unique ID of the sender. Compared to `currentUserId` to determine alignment. */ id: string; /** Display name of the sender. */ name: string; /** Avatar image URL. Only shown for non-current-user messages (xs size). */ avatar?: string; }` | Sender metadata used for avatar rendering and alignment. |\n| timestamp | `string \\| Date` | Message timestamp. Rendered as `HH:MM` via `toLocaleTimeString` when a `Date` object. |\n| status | `'sending' \\| 'sent' \\| 'delivered' \\| 'read' \\| 'error'` | Delivery status indicator shown on the current user's messages. - `'sending'` \u2014 shows a dot bullet - `'sent'` \u2014 shows a single checkmark - `'delivered'` \u2014 shows double checkmarks - `'read'` \u2014 shows double checkmarks (same visual as delivered) - `'error'` \u2014 shows an exclamation mark |\n\n**NavItem:**\n\nA single navigation entry in a `NavigationBar`. Supports nested children\nfor sub-menus (rendered by the parent via dropdown, not built-in).\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier used for active-state matching and React keys. |\n| label | `string` | Display label for the navigation link. |\n| href | `string` | URL for the item. When provided, renders an `<a>` element instead of `<button>`. |\n| icon | `ReactNode` | Optional icon rendered before the label. |\n| children | `NavItem[]` | Nested child items (for sub-menu structures; rendering is consumer-defined). |\n| disabled | `boolean` | When true, the item is visually dimmed (opacity 0.5) and non-interactive. |\n\n**UploadedFile:**\n\nA file entry in the `FileUploader` composition. Tracks upload progress,\nstatus, and optional error messages.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier for this file entry. Used as a key and for removal callbacks. |\n| name | `string` | Original file name. Rendered with text-overflow ellipsis when too long. |\n| size | `number` | File size in bytes. Formatted as B/KB/MB/GB for display. |\n| type | `string` | MIME type of the file (e.g., `'image/png'`). |\n| progress | `number` | Upload progress as a percentage (0-100). Shown via the `Progress` primitive when status is `'uploading'`. |\n| status | `'pending' \\| 'uploading' \\| 'success' \\| 'error'` | Current upload lifecycle status. - `'pending'` \u2014 file selected but upload not started. - `'uploading'` \u2014 upload in progress; `progress` bar is shown. - `'success'` \u2014 upload completed. - `'error'` \u2014 upload failed; `error` message is shown in `colors.error[500]`. |\n| error | `string` | Error message displayed when status is `'error'`. |\n| url | `string` | The remote URL of the uploaded file after successful upload. |\n\n**Notification:**\n\nA single notification entry in the `NotificationCenter`. Supports semantic type coloring,\nread state, and an optional inline action button.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier for this notification. |\n| title | `string` | Notification title rendered in medium weight. |\n| message | `string` | Optional body text rendered below the title in a smaller font. |\n| timestamp | `string \\| Date` | Timestamp of the notification. Rendered via `toLocaleString()` when a `Date` object. |\n| read | `boolean` | Read state. Unread notifications get a `colors.primary[50]` background and a colored status dot matching the notification type. |\n| type | `'info' \\| 'success' \\| 'warning' \\| 'error'` | Semantic type that controls the status dot color on unread notifications. - `'info'` \u2014 `colors.info[500]` (blue) - `'success'` \u2014 `colors.success[500]` (green) - `'warning'` \u2014 `colors.warning[500]` (amber) - `'error'` \u2014 `colors.error[500]` (red) |\n| icon | `ReactNode` | Optional icon rendered alongside the notification (not used by the default implementation). |\n| action | `{ /** Button label text. */ label: string; /** Click handler for the action. */ onClick: () => void; }` | Optional inline action button rendered next to the timestamp. |\n\n**Command:**\n\nA single command entry in a `CommandPalette`. Commands can be grouped,\nhave keyboard shortcuts, and support a disabled state.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier for this command. |\n| label | `string` | Display label for the command. Searchable by the palette's query filter. |\n| description | `string` | Optional description text shown below the label. Also searchable. |\n| icon | `ReactNode` | Icon rendered at the start of the command row. |\n| shortcut | `string` | Keyboard shortcut hint displayed at the end of the row in a `<kbd>` element. |\n| group | `string` | Group name for visual sectioning. Commands with the same group are rendered under a shared header. Defaults to `'Commands'` if omitted. |\n| disabled | `boolean` | When true, the command is visually dimmed and cannot be selected. |\n\n**FooterLink:**\n\nA single link entry in a footer column or the bottom bar. Supports both\n`href` navigation and `onClick` handlers (onClick takes precedence via `preventDefault`).\n\n| Property | Type | Description |\n|----------|------|-------------|\n| label | `string` | Display text for the link. |\n| href | `string` | URL for the link. |\n| onClick | `() => void` | Click handler. When provided, `preventDefault` is called on the anchor click. |\n\n**FooterColumn:**\n\nA named column of links in the `Footer` layout. Each column has an optional title\nand a list of links rendered as a vertical stack with 10 px gap.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| title | `string` | Column heading rendered as an `<h4>` with semibold weight. |\n| links | `FooterLink[]` | Links displayed in this column. |\n\n**FooterSocialLink:**\n\nA social media link in the `Footer` bottom bar. Rendered as an icon-only anchor\nwith `aria-label` for accessibility.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| label | `string` | Accessible label for the social link (used as `aria-label`). |\n| href | `string` | URL for the social media profile or page. |\n| icon | `ReactNode` | Icon element rendered inside the anchor (typically an SVG or `Icon` primitive). |\n\n**HeroAction:**\n\nA call-to-action button definition for the `Hero` composition.\nUsed for both the primary (filled) and secondary (outlined) action buttons.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| label | `string` | Button label text. |\n| onClick | `() => void` | Click handler for the button. |\n| href | `string` | Optional URL (not used by the default implementation; available for consumer routing). |\n\n\n## System Conventions\n\n### onChange Behavior (CRITICAL)\n\nAll form control onChange handlers receive the VALUE DIRECTLY, not a React event object.\n\n```tsx\n// CORRECT \u2014 onChange receives value directly\n<Input value={name} onChange={setName} />\n<Input value={email} onChange={(value) => setEmail(value)} />\n<Select value={country} onChange={setCountry} options={countries} />\n<Checkbox checked={agreed} onChange={setAgreed} />\n\n// WRONG \u2014 DO NOT use e.target.value!\n<Input value={name} onChange={(e) => setName(e.target.value)} /> // WILL BREAK\n```\n\nApplies to: Input, TextArea, Select, Checkbox, RadioGroup, Slider, Tabs, Accordion.\n\n### Available Motion & Animation\n\nRender `<MotionKeyframes />` once (anywhere in tree) to enable all keyframes.\n\n**Entrance/exit:** fadeIn, fadeOut, slideInUp, slideInDown, scaleIn, scaleOut\n**State feedback:** flash (background-color highlight), pulse (opacity breathing), bounce (scale overshoot)\n**Easing:** linear, easeIn, easeOut, easeInOut, spring (bouncy)\n**Durations:** instant(0ms), fast(100ms), normal(200ms), slow(300ms), slower(500ms)\n\n```tsx\n// Entrance animation on mount\n<div style={{ animation: 'ggui-fadeIn 200ms ease-out' }}>Content</div>\n\n// Stagger list items\n{items.map((item, i) => (\n <div key={item.id} style={{ animation: \\`ggui-slideInUp 300ms ease-out \\${i * 50}ms both\\` }}>\n {item.name}\n </div>\n))}\n\n// Flash highlight when data changes (e.g., stock price update)\n// useAnimationKey returns a key that increments when dep changes \u2192 remounts element \u2192 replays animation\nconst priceKey = useAnimationKey(stock.price);\n<div key={priceKey} style={{\n animation: animation.flash,\n '--ggui-flash-color': stock.change > 0 ? 'var(--ggui-color-success-100)' : 'var(--ggui-color-error-100)',\n} as React.CSSProperties}>\n {stock.price}\n</div>\n\n// Respect reduced-motion preference\nconst { motionEnabled } = useMotion();\n<div style={motionEnabled ? { animation: 'ggui-scaleIn 200ms ease-out' } : undefined}>\n Content\n</div>\n```\n\n### Elevation System\n\n6 levels mapping shadow intensity to z-index for layering:\n- Level 0: flat (no shadow, z: auto) \u2014 inline content\n- Level 1: sm shadow (z: auto) \u2014 cards, sections\n- Level 2: md shadow (z: 1000) \u2014 dropdowns, popovers\n- Level 3: lg shadow (z: 1200) \u2014 sticky banners\n- Level 4: xl shadow (z: 1400) \u2014 modals, dialogs\n- Level 5: 2xl shadow (z: 1800) \u2014 tooltips, toasts\n\n### Import Constraints\n\nOnly these imports are allowed:\n- `react`\n- `@ggui-ai/design` \u2014 the whole design system (primitives, components, compositions, and the `Clickable` / `Hoverable` / `Pressable` traits) is one import; there are no subpaths\n- `@ggui-ai/wire` (wire hooks)\n\nNo external libraries (lodash, date-fns, etc.). No fetch(). No eval().";
|
|
1405
|
+
PRIMITIVES_DOCUMENTATION = "# ggui Primitives & Design System Reference\n\n> You are a world-class UI engineer working with ggui's component library for the first time.\n> This reference documents every available component, prop, and convention.\n> Components handle theming automatically via built-in variants \u2014 pick the right variant and the theme does the rest.\n> For custom styling beyond variants, use CSS variables: var(--ggui-*, fallback).\n\n## Primitives\n\nImport: `import { Component } from '@ggui-ai/design'`\n\n### Container\n\nContainer -- Width-constrained wrapper that centers content horizontally.\n\nRenders a `<div>` with `width: 100%` and a `maxWidth` constraint.\nWhen `center` is true (the default), applies `margin: 0 auto`.\nNo background, border, or shadow -- use Card for visual containment.\n\nCSS variables used: none (pure layout primitive).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| maxWidth | `'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| '3xl' \\| 'full' \\| string` | `'lg'` | Maximum width constraint. Accepts a preset token or any CSS width string. - `'xs'` -- 320px - `'sm'` -- 480px - `'md'` -- 640px - `'lg'` -- 768px - `'xl'` -- 1024px - `'2xl'` -- 1280px - `'3xl'` -- 1536px - `'full'` -- 100% Custom strings (e.g., `'900px'`, `'60ch'`) are passed through as-is. |\n| center | `boolean` | `true` | Whether to center the container horizontally via `margin: 0 auto`. |\n| padding | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `undefined (no padding)` | Padding applied to all sides. Prefer a spacing-scale name (`'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl'`) \u2014 each resolves to the matching `--ggui-spacing-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. |\n\n**Example:**\n```tsx\n<Container maxWidth=\"xl\" padding=\"var(--ggui-spacing-6)\">\n <Stack gap=\"var(--ggui-spacing-4)\">\n <Heading level={1}>Dashboard</Heading>\n <Card shadow=\"md\" padding=\"var(--ggui-spacing-5)\">\n <Text>Welcome back!</Text>\n </Card>\n </Stack>\n</Container>\n```\n\n### Card\n\nCard -- Container with background, shadow, and optional border.\n\nRenders a `<div>` with:\n- Background: `var(--ggui-color-container)`\n- Border (when enabled): `1px solid var(--ggui-color-outlineVariant)`\n- Shadow and radius controlled by design tokens via CSS variables.\n- No built-in transitions.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| padding | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `'lg'` | Padding applied to all sides. Prefer a spacing-scale name (`'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl'`) \u2014 each resolves to the matching `--ggui-spacing-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. |\n| shadow | `'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'` | `'sm'` | Shadow elevation level. Maps to design tokens: - `'none'` -- no shadow - `'sm'` -- var(--ggui-shape-shadow-sm) -- subtle, default - `'md'` -- var(--ggui-shape-shadow-md) -- dialogs, emphasized sections - `'lg'` -- var(--ggui-shape-shadow-lg) -- floating panels - `'xl'` -- var(--ggui-shape-shadow-xl) -- popovers, modals |\n| border | `boolean` | `true` | Whether to render a 1px border using `var(--ggui-color-outlineVariant)`. |\n| radius | `'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| number \\| string` | `'lg'` | Corner radius. Prefer a radius-scale name (`'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'`) \u2014 each resolves to the matching `--ggui-shape-radius-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. |\n| surface | `'default' \\| 'elevated' \\| 'sunken' \\| 'accent' \\| 'inverted' \\| 'hero' \\| 'transparent'` | `'default'` | Semantic surface slot. Same vocabulary as ; see that prop's docs for the full slot table. Default Card surface is `'default'` (the active theme's `--ggui-color-container`); pair with `shadow=\"md\"\\|\"lg\"` for elevated cards, `'hero'` for a greeting or hero card (the theme's hero ground, right on a light or a dark host), or `'inverted'` for a dark testimonial-style card on a light theme. |\n\n**Example:**\n```tsx\n<Card shadow=\"md\" padding=\"lg\" radius=\"lg\">\n <Stack gap=\"md\">\n <Text size=\"sm\" weight=\"medium\">Settings</Text>\n <Input label=\"Name\" value={name} onChange={setName} />\n <Button variant=\"primary\">Save</Button>\n </Stack>\n</Card>\n```\n\n### Stack\n\nStack -- Flexbox layout primitive for arranging children along a single axis.\n\nRenders a `<div>` with `display: flex`. Default layout is vertical (column).\nAll flex shorthand values (`align`, `justify`, `wrap`) are abstracted into\nsemantic prop names.\n\nCSS variables used: none (pure layout primitive).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| direction | `'vertical' \\| 'horizontal'` | `'vertical'` | Main axis direction. - `'vertical'` -- `flex-direction: column` - `'horizontal'` -- `flex-direction: row` |\n| gap | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `'sm'` | Gap between children. Prefer a spacing-scale name (`'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl'`) \u2014 each resolves to the matching `--ggui-spacing-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. |\n| align | `'start' \\| 'center' \\| 'end' \\| 'stretch'` | `'stretch'` | Cross-axis alignment (maps to `align-items`). - `'start'` -- flex-start - `'center'` -- center - `'end'` -- flex-end - `'stretch'` -- stretch (children fill cross-axis) |\n| justify | `'start' \\| 'center' \\| 'end' \\| 'between' \\| 'around' \\| 'evenly'` | `'start'` | Main-axis content distribution (maps to `justify-content`). - `'start'` -- flex-start - `'center'` -- center - `'end'` -- flex-end - `'between'` -- space-between - `'around'` -- space-around - `'evenly'` -- space-evenly |\n| wrap | `boolean` | `false` | Whether children wrap to the next line when they overflow. Maps to `flex-wrap: wrap` when true. |\n\n**Example:**\n```tsx\n<Stack gap=\"lg\" align=\"center\">\n <Heading level={2}>Profile</Heading>\n <Text>Edit your account details below.</Text>\n <Stack direction=\"horizontal\" gap=\"sm\" justify=\"end\">\n <Button variant=\"ghost\">Cancel</Button>\n <Button variant=\"primary\">Save</Button>\n </Stack>\n</Stack>\n```\n\n### Grid\n\nGrid -- 2-D layout primitive. Arranges children into rows AND\ncolumns; reach for it when Stack/Row's single-axis flow isn't\nenough (card galleries, dashboards, stat grids).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| columns | `number \\| ResponsiveColumns` | `2` | Column count. Three forms: - a number \u2014 that many equal columns at every width (`columns={3}`); - a map \u2014 explicit counts per breakpoint (`columns={{ base: 1, md: 3 }}` = 1 column on mobile, 3 from `md`). Use this when the request names exact per-breakpoint counts (\"3 per row on desktop, 1 on mobile\"). Ignored entirely when `minColumnWidth` is set. |\n| gap | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `'md'` | Gap between cells. Prefer a spacing-scale name (`'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl'`); a number is pixels. |\n| minColumnWidth | `number \\| string` | `undefined (use `columns`)` | When set, the grid becomes responsive \u2014 it fits as many equal columns as possible, each at least this wide, and `columns` is ignored. A number is treated as pixels. |\n\n**Example:**\n```tsx\n<Grid columns={3} gap=\"md\">\n {items.map((it) => <Card key={it.id}>{it.name}</Card>)}\n</Grid>\n```\n\n### Skeleton\n\nSkeleton -- a pulsing placeholder for content that has not loaded\nyet. ggui UIs are agent-driven (props arrive late, streams start\nempty), so a loading frame is the rule \u2014 render `Skeleton` instead\nof a blank screen or a hand-rolled pulsing `<div>`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| variant | `'rect' \\| 'text' \\| 'circle'` | `'rect'` | Shape preset. - `'rect'` -- a block (default); pair with `width` / `height`. - `'text'` -- a single text line (height ~1em). - `'circle'` -- equal width/height, fully rounded (avatar slot). |\n| width | `number \\| string` | - | Width. A number is pixels. Defaults to `100%` (`2.5rem` for circle). |\n| height | `number \\| string` | - | Height. A number is pixels. Defaults by variant when unset. |\n| radius | `'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| number \\| string` | `'sm'` | Corner radius. Prefer a radius-scale name. Ignored for `variant=\"circle\"` (always fully round). |\n\n**Example:**\n```tsx\n{user === undefined\n ? <Skeleton variant=\"text\" width=\"40%\" />\n : <Text>{user.name}</Text>}\n```\n\n### Box\n\nBox -- Generic container with padding, margin, background, and border-radius.\n\nRenders a plain `<div>`. Unlike Card, Box has no default background, shadow,\nor border -- it is a blank canvas for custom styling. Use it for layout\nspacing, colored sections, or wrapping arbitrary content.\n\nWhen both `paddingX`/`paddingY` and `padding` are provided, the axis-specific\nprops take precedence and `padding` is ignored.\n\nCSS variables used: none (all values are passed through directly).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| padding | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `undefined (no padding)` | Padding applied to all four sides. Prefer a spacing-scale name (`'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl'`) \u2014 each resolves to the matching `--ggui-spacing-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. Ignored when `paddingX` or `paddingY` is set. |\n| paddingX | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `undefined` | Horizontal (left + right) padding. Accepts a spacing-scale name, a pixel number, or a raw CSS string \u2014 see . When set alongside `paddingY`, they combine into a shorthand `padding: {Y} {X}`. When set without `paddingY`, vertical padding defaults to 0. |\n| paddingY | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `undefined` | Vertical (top + bottom) padding. Accepts a spacing-scale name, a pixel number, or a raw CSS string \u2014 see . When set alongside `paddingX`, they combine into a shorthand `padding: {Y} {X}`. When set without `paddingX`, horizontal padding defaults to 0. |\n| margin | `'none' \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| number \\| string` | `undefined (no margin)` | Margin applied to all four sides. Accepts a spacing-scale name, a pixel number, or a raw CSS string \u2014 see . |\n| surface | `'default' \\| 'elevated' \\| 'sunken' \\| 'accent' \\| 'inverted' \\| 'hero' \\| 'transparent'` | `undefined (transparent)` | Semantic surface slot. Picks the right `var(--ggui-color-*)` background token from the active theme. The ONLY way to set a theme-tracking background fill on Box. Available slots: - `'default'` \u2014 base container surface (most common) - `'elevated'` \u2014 same fill, intended to be paired with shadow (use Card.shadow for actual elevation) - `'sunken'` \u2014 recessed / inset region (`surfaceVariant` token) - `'accent'` \u2014 highlighted / branded fill (`primary-50` token) - `'inverted'` \u2014 dark surface in light mode, light in dark (testimonials, code-snippet cards, hero panels). The surface owns ALL its inks: body text, `muted` / `subtle` labels, chips and outlines inside it re-map to inverse-derived values by cascade \u2014 no `tone=\"inverse\"` needed, and no hand-picked greys. - `'hero'` \u2014 the hero panel's ground: brand-tinted on a light host, the ink pair on a dark one (`heroGround` / `onHeroGround`, derived per mode) \u2014 reach for it for a greeting or hero card instead of guessing the host's darkness with `'inverted'`; it owns its subtree's inks likewise. - `'transparent'` \u2014 explicit \"no fill\" For non-theme-mapped brand colors (e.g. a partner's exact brand hex like Stripe purple) use the escape \u2014 every other hex / rgba on Box is rejected by tier-0 self-check. |\n| assetColor | `string` | `undefined` | Asset color escape \u2014 the typed valve for legitimate non-theme color values (a partner's exact brand hex, a fixed product surface, etc.). Renders as the Box background. **MUST be paired with .** The semantic name is human-readable documentation of why this color bypasses the theme \u2014 e.g. `\"stripe-brand-purple\"`, `\"slack-aubergine\"`. Tier-0 self-check allows hex / rgba inside `assetColor` ONLY when `assetSemantic` is a non-empty string; one without the other fails the check. Reach for `surface` first. This escape exists for the small set of cases where the operator's theme MUST NOT override the value (brand identity rendering). |\n| assetSemantic | `string` | `undefined` | Human-readable semantic label that documents why bypasses the theme. Required when `assetColor` is set; tier-0 self-check rejects empty strings or a missing `assetSemantic` next to a hex `assetColor`. Examples: `\"stripe-brand-purple\"`, `\"slack-aubergine\"`, `\"partner-logo-orange\"`. Pure documentation \u2014 no rendering effect. |\n| radius | `'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| number \\| string` | `undefined (no rounding)` | Corner radius. Prefer a radius-scale name (`'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'`) \u2014 each resolves to the matching `--ggui-shape-radius-*` token. A number is treated as pixels; any other string is passed through as a raw CSS value. |\n\n**Example:**\n```tsx\n<Box paddingX=\"xl\" paddingY=\"lg\" surface=\"accent\" radius=\"lg\">\n <Text size=\"sm\" tone=\"emphasized\">\n Tip: You can customize your theme in Settings.\n </Text>\n</Box>\n```\n\n### Divider\n\nDivider -- A 1px line to visually separate content sections.\n\nRenders an `<hr>` (horizontal) or `<div>` (vertical) with `role=\"separator\"`.\n- Horizontal: 1px tall, full width, with vertical margin.\n- Vertical: 1px wide, stretches to parent height via `align-self: stretch`,\n with horizontal margin. Works best inside a horizontal Stack or Row.\n\nDefault color: `var(--ggui-color-outlineVariant)`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| orientation | `'horizontal' \\| 'vertical'` | `'horizontal'` | Line direction. - `'horizontal'` -- renders `<hr>`, full width, 1px height, margin top/bottom - `'vertical'` -- renders `<div>`, 1px width, `align-self: stretch`, margin left/right |\n| margin | `number \\| string` | `16` | Spacing around the divider. Numbers are treated as pixels. Applied as vertical margin for horizontal dividers, horizontal margin for vertical. |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `undefined (uses `var(--ggui-color-outlineVariant)`)` | Semantic color slot. Same vocabulary as ; the theme decides what each tone LOOKS like. Defaults to a quiet outline-variant tint when unset (independent of the tone slots). |\n\n**Example:**\n```tsx\n<Stack gap={0}>\n <Text>Section A</Text>\n <Divider margin=\"var(--ggui-spacing-3)\" />\n <Text>Section B</Text>\n</Stack>\n```\n\n### Spacer\n\nSpacer -- Invisible spacing element, either fixed-size or flexible.\n\nRenders an empty `<div>`.\n- Fixed mode (number): sets both `width` and `height` to the given pixel\n value with `flex-shrink: 0`, creating rigid spacing in any direction.\n- Flex mode (`'flex'`): sets `flex: 1`, expanding to fill remaining space\n in a flex container. Useful for pushing siblings apart.\n\nCSS variables used: none.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| size | `number \\| 'flex'` | `16` | Spacing amount. - Number: fixed square spacer (width and height in pixels, `flex-shrink: 0`). - `'flex'`: expands to fill available space (`flex: 1`). |\n\n**Example:**\n```tsx\n<Stack direction=\"horizontal\" align=\"center\">\n <Heading level={3}>Logo</Heading>\n <Spacer size=\"flex\" />\n <Button variant=\"ghost\">Login</Button>\n</Stack>\n```\n\n### Text\n\nText -- Versatile typography primitive for body copy, captions, and labels.\n\nRenders as `<p>` by default (configurable via `is`). Typography is\ncomposed from three orthogonal axes: `size` (font size, with a\nmatching line-height built in), `weight`, and `tone` (semantic\ncolor). `caps` adds the uppercase, letter-spaced treatment for tiny\nsection labels (the classic overline/eyebrow look) \u2014 the label's text\ncomes from a prop, never from the component: an invented eyebrow is copy\nnothing can change.\n\nDefault text color: `var(--ggui-color-onContainer)`.\nAll text renders with `margin: 0` (no default paragraph spacing).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| size | `'xs' \\| 'sm' \\| 'base' \\| 'lg' \\| 'xl' \\| '2xl' \\| '3xl' \\| '4xl'` | `'base'` | Font size. Maps to CSS variables with pixel fallbacks; each size carries a matching line-height (1.5 for `xs`--`base`, 1.625 for `lg`, then tightening on display sizes: 1.375 at `xl`, 1.25 from `2xl` up): - `'xs'` -- var(--ggui-font-size-xs) (12px) - `'sm'` -- var(--ggui-font-size-sm) (14px) - `'base'` -- var(--ggui-font-size-base) (16px) - `'lg'` -- var(--ggui-font-size-lg) (18px) - `'xl'` -- var(--ggui-font-size-xl) (20px) - `'2xl'` -- var(--ggui-font-size-2xl) (24px) - `'3xl'` -- var(--ggui-font-size-3xl) (30px) - `'4xl'` -- var(--ggui-font-size-4xl) (36px) |\n| weight | `'normal' \\| 'medium' \\| 'semibold' \\| 'bold'` | `'normal'` | Font weight. Maps to CSS variables with numeric fallbacks: - `'normal'` -- var(--ggui-font-weight-normal) - `'medium'` -- var(--ggui-font-weight-medium) - `'semibold'` -- var(--ggui-font-weight-semibold) - `'bold'` -- var(--ggui-font-weight-bold) |\n| caps | `boolean` | `false` | When true, uppercases the text and adds 0.05em letter-spacing -- the eyebrow/overline treatment for tiny section labels. Pair with `size=\"xs\" weight=\"semibold\" tone=\"muted\"`. |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `'default' (var(--ggui-color-onContainer))` | Semantic color slot. Picks the right `var(--ggui-color-*)` token from the active theme. The theme decides what each tone LOOKS like \u2014 `'muted'` is a quiet warm grey on Claudic, a cool slate on Indigo, dim cyan on Neon-Noir. Components that use `tone` track the operator's theme switch automatically. Available slots: `'default'` (primary body text), `'muted'` (secondary / metadata), `'subtle'` (very-low-emphasis hint), `'emphasized'` (branded accent), `'loud'` (strongest accent \u2014 the theme's readable accent ink), `'success'` / `'warning'` / `'error'` / `'info'` (status text), `'inverse'` (text on dark surface), `'inherit'` (parent's color). `tone` is the ONLY way to set a Text color. The legacy `color?: string` escape was retired \u2014 raw color strings bypass theming and silently override the operator's preset. |\n| align | `'left' \\| 'center' \\| 'right'` | `undefined (inherits from parent)` | Horizontal text alignment. Maps directly to `text-align`. |\n| truncate | `boolean` | `false` | When true, clips overflowing text with an ellipsis. Applies `overflow: hidden`, `text-overflow: ellipsis`, and `white-space: nowrap`. |\n| is | `'p' \\| 'span' \\| 'div' \\| 'label'` | `'p'` | HTML element to render. Choose based on semantic context: - `'p'` -- paragraph (default, block-level) - `'span'` -- inline text within a sentence - `'div'` -- generic block container - `'label'` -- form label (pair with `htmlFor`) |\n| id | `string` | - | `id` for the rendered element \u2014 anchor an in-page link, or pair with a form control's `aria-labelledby`. |\n| htmlFor | `string` | - | Associates an `is=\"label\"` element with a form control by the control's `id`. Only meaningful when `is=\"label\"`. |\n\n**Example:**\n```tsx\n<Stack gap=\"var(--ggui-spacing-1)\">\n <Text size=\"xs\" weight=\"semibold\" caps tone=\"muted\">{props.section}</Text>\n <Text size=\"lg\">Welcome back, Jane.</Text>\n <Text size=\"xs\" tone=\"muted\">\n Last login: 2 hours ago\n </Text>\n</Stack>\n```\n\n### Heading\n\nHeading -- Semantic heading element (h1-h6) with preset typography styles.\n\nRenders the corresponding `<h1>`-`<h6>` HTML element based on `level`.\nEach level has a preset font size, weight, line height, and letter spacing\nfrom the heading typography tokens:\n- Level 1: 36px / bold / 1.25 line-height / -0.025em tracking\n- Level 2: 30px / bold / 1.25 line-height / -0.025em tracking\n- Level 3: 24px / semibold / 1.375 line-height / 0em tracking\n- Level 4: 20px / semibold / 1.375 line-height / 0em tracking\n- Level 5: 18px / semibold / 1.5 line-height / 0em tracking\n- Level 6: 16px / semibold / 1.5 line-height / 0em tracking\n\nDefault text color: `var(--ggui-color-onContainer)`.\nAll headings render with `margin: 0` (no default heading spacing).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| level | `1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6` | `2` | Semantic heading level. Determines both the HTML element (`<h1>`-`<h6>`) and the preset typography style. |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `'default' (var(--ggui-color-onContainer))` | Semantic color slot. Same vocabulary as ; see that prop's docs for the full slot table. `tone` is the ONLY way to set a Heading color \u2014 the legacy `color?: string` escape was retired so the operator's theme always wins. |\n| align | `'left' \\| 'center' \\| 'right'` | `undefined (inherits from parent)` | Horizontal text alignment. Maps directly to `text-align`. |\n\n**Example:**\n```tsx\n<Stack gap=\"var(--ggui-spacing-2)\">\n <Heading level={1}>Page Title</Heading>\n <Heading level={3} tone=\"emphasized\">\n Subsection\n </Heading>\n <Text>Body content goes here.</Text>\n</Stack>\n```\n\n### Button\n\nButton -- A clickable button primitive with multiple visual variants and sizes.\n\nRenders a native `<button>` element styled with inline CSS derived from design-token\nCSS variables. Supports a loading spinner, left/right icon slots, and a cross-platform\n`onPress` alias for `onClick`.\n\nBase styles applied to every variant:\n- `border-radius: var(--ggui-shape-radius-control)` \u2014 the control ROLE (the host's button/field radius; falls to `var(--ggui-shape-radius-md)`)\n- `font-weight: var(--ggui-font-weight-medium)`\n- `box-shadow: var(--ggui-shape-shadow-sm)`\n- `gap: var(--ggui-spacing-2)` between icon and text\n- Transitions: background-color, box-shadow, opacity at 200ms ease-in-out\n\nDisabled or loading: `opacity: 0.5`, `cursor: not-allowed`, click handler suppressed.\n\nAlso extends native `ButtonHTMLAttributes` (except `style`/`className`), so props\nlike `type`, `form`, `aria-*`, and `data-*` are forwarded to the `<button>` element.\nThe `type` prop defaults to `'button'` (not `'submit'`), preventing accidental form\nsubmissions.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| variant | `'primary' \\| 'secondary' \\| 'outline' \\| 'ghost' \\| 'danger'` | `'primary'` | Visual style. Maps to CSS variables: - `'primary'` -- `var(--ggui-color-primary-600)` background, white text, no border - `'secondary'` -- `var(--ggui-color-sunken)` background, `var(--ggui-color-onSunken)` text, no border - `'outline'` -- transparent background, `1px solid var(--ggui-color-primary-600)` border, label in the surface's on-colour (`onContainer`) so it reads on any ground \u2014 the brand shows on the border, never as label ink on a dark theme - `'ghost'` -- transparent background, `var(--ggui-color-onSunken)` text, no border - `'danger'` -- `var(--ggui-color-error-600)` background, white text, no border |\n| size | `'xs' \\| 'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls padding, font size, and minimum height: - `'xs'` -- padding `4px 8px`, font `var(--ggui-font-size-xs)`, min-height 24px - `'sm'` -- padding `6px 12px`, font `var(--ggui-font-size-sm)`, min-height 32px - `'md'` -- padding `10px 16px`, font `var(--ggui-font-size-sm)`, min-height 40px - `'lg'` -- padding `12px 24px`, font `var(--ggui-font-size-base)`, min-height 48px |\n| fullWidth | `boolean` | `false` | When true, sets `width: 100%` so the button fills its container. |\n| loading | `boolean` | `false` | When true, replaces children with a 16px `Spinner` (color: `currentColor`) and disables interaction (same effect as `disabled`). |\n| leftIcon | `ReactNode` | - | ReactNode rendered before children, inside the flex layout with `var(--ggui-spacing-2)` gap. |\n| rightIcon | `ReactNode` | - | ReactNode rendered after children, inside the flex layout with `var(--ggui-spacing-2)` gap. |\n| onPress | `() => void` | - | Alias for `onClick` for cross-platform compatibility (React Native convention). If both `onClick` and `onPress` are provided, `onClick` takes precedence. |\n\n**Example:**\n```tsx\n<Button variant=\"primary\" size=\"md\" leftIcon={<Icon name=\"save\" />} onClick={handleSave}>\n Save Changes\n</Button>\n```\n\n### Input\n\nInput -- A single-line text input with label, validation, and helper text.\n\nRenders a `<div>` wrapper containing an optional `<label>`, a native `<input>`,\nand an optional message `<span>` for error or helper text.\n\nStyling:\n- Border: `1px solid var(--ggui-color-outline)` (normal),\n `var(--ggui-color-error-500)` (error)\n- Background: `var(--ggui-color-container)` (normal),\n `var(--ggui-color-container)` (disabled)\n- Text: `var(--ggui-color-onContainer)`\n- Border radius: `var(--ggui-shape-radius-control)` \u2014 the control ROLE (the host's button/field radius; falls to `var(--ggui-shape-radius-md)`)\n- Label: `var(--ggui-font-size-sm)`, `var(--ggui-font-weight-medium)`,\n `var(--ggui-color-onSunken)`\n- Transitions: border-color, box-shadow at 200ms ease-in-out\n\nAccessibility: auto-generated `id` links `<label>` to `<input>` via `htmlFor`.\nWhen `error` is set, `aria-invalid` is true and the message has `role=\"alert\"`.\nWhen `required` is true, a red asterisk is appended to the label.\n\nAlso extends native `InputHTMLAttributes` (except `style`, `className`, `onChange`,\n`size`), so props like `autoFocus`, `name`, `pattern`, `aria-*` are forwarded.\n\n**IMPORTANT:** `onChange` receives the string value directly, NOT a React\n`ChangeEvent`. This differs from native `<input>` behavior.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Label rendered above the input. Linked to the input via auto-generated `htmlFor`/`id`. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| placeholder | `string` | - | Placeholder text shown when the input is empty. |\n| value | `string` | - | Controlled value of the input. |\n| onChange | `(value: string) => void` | - | Change handler. Receives the new string value directly, NOT a React event. |\n| type | `'text' \\| 'email' \\| 'password' \\| 'number' \\| 'tel' \\| 'url' \\| 'search' \\| 'date' \\| 'time' \\| 'datetime-local'` | `'text'` | HTML input type. Determines browser behavior (keyboard on mobile, validation, masking). The date/time types render the browser's native, localized, keyboard-accessible picker; their `value` stays an ISO string (`'2026-07-29'`, `'14:30'`, `'2026-07-29T14:30'`) delivered through the usual value-first `onChange`. |\n| error | `string` | - | Error message displayed below the input in `var(--ggui-color-error-500)`. When set, the border turns red and the message element gets `role=\"alert\"`. Takes precedence over `helperText`. |\n| helperText | `string` | - | Helper text displayed below the input in `var(--ggui-color-onSunken)`. Only shown when `error` is not set. |\n| required | `boolean` | `false` | When true, appends a red asterisk (`*`) to the label and sets the native `required` attribute on the `<input>`. |\n| disabled | `boolean` | `false` | When true, sets the native `disabled` attribute. Background changes to `var(--ggui-color-container)`. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls padding and font size: - `'sm'` -- padding `6px 10px`, font `var(--ggui-font-size-sm)` - `'md'` -- padding `10px 12px`, font `var(--ggui-font-size-sm)` - `'lg'` -- padding `12px 14px`, font `var(--ggui-font-size-base)` |\n\n**Example:**\n```tsx\n<Input label=\"Email\" type=\"email\" value={email} onChange={setEmail} error={emailError} />\n```\n\n### TextArea\n\nTextArea -- A multiline text input with label, validation, character count, and auto-resize.\n\nRenders a `<div>` wrapper containing an optional `<label>`, a native `<textarea>`,\nand a footer row with error/helper text on the left and character count on the right.\n\nStyling:\n- Padding: `10px 12px`, font: `var(--ggui-font-size-sm)`, `font-family: inherit`\n- Border: `1px solid var(--ggui-color-outline)` (normal),\n `var(--ggui-color-error-500)` (error)\n- Background: `var(--ggui-color-container)` (normal),\n `var(--ggui-color-container)` (disabled)\n- Border radius: `var(--ggui-shape-radius-control)` \u2014 the control ROLE (the host's button/field radius; falls to `var(--ggui-shape-radius-md)`)\n- Resize: `vertical` by default, `none` when `autoResize` is true\n- Transitions: border-color, box-shadow at 200ms ease-in-out\n\nAccessibility: same label/error linking pattern as Input (auto-generated ids,\n`aria-invalid`, `role=\"alert\"` on error message).\n\nAlso extends native `TextareaHTMLAttributes` (except `style`, `className`, `onChange`).\n\n**IMPORTANT:** `onChange` receives the string value directly, NOT a React\n`ChangeEvent`. This differs from native `<textarea>` behavior.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Label rendered above the textarea. Linked via auto-generated `htmlFor`/`id`. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| placeholder | `string` | - | Placeholder text shown when the textarea is empty. |\n| value | `string` | - | Controlled value of the textarea. |\n| onChange | `(value: string) => void` | - | Change handler. Receives the new string value directly, NOT a React event. |\n| rows | `number` | `4` | Number of visible text rows (native `rows` attribute on `<textarea>`). |\n| error | `string` | - | Error message displayed below the textarea in `var(--ggui-color-error-500)`. When set, the border turns red and the message element gets `role=\"alert\"`. Takes precedence over `helperText`. |\n| helperText | `string` | - | Helper text displayed below the textarea in `var(--ggui-color-onSunken)`. Only shown when `error` is not set. |\n| required | `boolean` | `false` | When true, appends a red asterisk (`*`) to the label and sets the native `required` attribute on the `<textarea>`. |\n| disabled | `boolean` | `false` | When true, sets the native `disabled` attribute. Background changes to `var(--ggui-color-container)`. |\n| maxLength | `number` | - | Maximum character length (native `maxLength` attribute). Also used as the denominator in the character count display when `showCount` is true. |\n| showCount | `boolean` | `false` | When true AND `maxLength` is set, displays a `{current}/{max}` character counter in the footer row (right-aligned, `var(--ggui-font-size-xs)`). Has no effect without `maxLength`. |\n| autoResize | `boolean` | `false` | When true, sets CSS `resize: none` on the textarea. The flag disables manual resizing to signal that external logic handles sizing. The component does NOT auto-adjust height based on content in the current implementation. |\n\n**Example:**\n```tsx\n<TextArea label=\"Bio\" value={bio} onChange={setBio} rows={6} maxLength={500} showCount />\n```\n\n### Select\n\nSelect -- A native dropdown selection primitive with label and validation.\n\nRenders a `<div>` wrapper containing an optional `<label>`, a native `<select>`\nwith custom styling, and an optional message `<span>`.\n\nThe native `<select>` has `appearance: none` with a custom chevron SVG rendered\nas a `background-image` (right-aligned, 12px, onSurfaceVariant color). Extra right\npadding (36px) accommodates the chevron.\n\nStyling:\n- Border: `1px solid var(--ggui-color-outline)` (normal),\n `var(--ggui-color-error-500)` (error)\n- Background: `var(--ggui-color-container)` (normal),\n `var(--ggui-color-container)` (disabled)\n- Text: `var(--ggui-color-onContainer)` when a value is selected,\n `var(--ggui-color-onSunken)` when showing placeholder\n- Border radius: `var(--ggui-shape-radius-control)` \u2014 the control ROLE (the host's button/field radius; falls to `var(--ggui-shape-radius-md)`)\n- Cursor: `pointer` (normal), `not-allowed` (disabled)\n- Transitions: border-color, box-shadow at 200ms ease-in-out\n\nAccessibility: auto-generated `id` links `<label>` to `<select>`.\nWhen `error` is set, `aria-invalid` is true and the message has `role=\"alert\"`.\n\nAlso extends native `SelectHTMLAttributes` (except `style`, `className`,\n`onChange`, `size`).\n\n**IMPORTANT:** `onChange` receives the selected value string directly, NOT a\nReact `ChangeEvent`. This differs from native `<select>` behavior.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Label rendered above the select. Linked via auto-generated `htmlFor`/`id`. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| value | `string` | - | Controlled value. Should match one of the `options[].value` strings. |\n| onChange | `(value: string) => void` | - | Change handler. Receives the selected option's value string directly, NOT a React event. |\n| options | `SelectOption[]` | - | Array of selectable options. Rendered as native `<option>` elements. Must contain at least one option (or use `placeholder` for an empty-state prompt). |\n| placeholder | `string` | - | Placeholder text rendered as a disabled `<option value=\"\">` at the top of the list. Shown when no value is selected. |\n| error | `string` | - | Error message displayed below the select in `var(--ggui-color-error-500)`. When set, the border turns red and the message has `role=\"alert\"`. Takes precedence over `helperText`. |\n| helperText | `string` | - | Helper text displayed below the select in `var(--ggui-color-onSunken)`. Only shown when `error` is not set. |\n| required | `boolean` | `false` | When true, appends a red asterisk (`*`) to the label and sets the native `required` attribute. |\n| disabled | `boolean` | `false` | When true, sets the native `disabled` attribute. Background changes to `var(--ggui-color-container)` and cursor becomes `not-allowed`. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls padding and font size: - `'sm'` -- padding `6px 10px`, font `var(--ggui-font-size-sm)` - `'md'` -- padding `10px 12px`, font `var(--ggui-font-size-sm)` - `'lg'` -- padding `12px 14px`, font `var(--ggui-font-size-base)` |\n\n**Example:**\n```tsx\n<Select\n label=\"Country\"\n value={country}\n onChange={setCountry}\n options={[\n { value: 'us', label: 'United States' },\n { value: 'uk', label: 'United Kingdom' },\n ]}\n placeholder=\"Select a country\"\n/>\n```\n\n### Checkbox\n\nCheckbox -- A custom-styled checkbox with label and description.\n\nRenders a `<label>` wrapper containing a visually-hidden native `<input type=\"checkbox\">`\noverlaid by a custom 18x18px visual box. Supports checked, unchecked, and indeterminate\nstates, each with a distinct SVG icon (checkmark or horizontal dash).\n\nStyling:\n- Box border: `2px solid var(--ggui-color-primary-600)` (checked/indeterminate),\n `var(--ggui-color-outline)` (unchecked)\n- Box fill: `var(--ggui-color-primary-600)` (checked/indeterminate),\n `var(--ggui-color-container)` (unchecked)\n- Check/dash icon: white SVG, 12x12px\n- Box radius: `var(--ggui-shape-radius-sm)`\n- Transition: all 0.2s\n- Label: `var(--ggui-font-size-sm)`, `var(--ggui-font-weight-medium)`\n- Description: `var(--ggui-font-size-xs)`, `var(--ggui-color-onSunken)`\n- Disabled: `opacity: 0.5`, `cursor: not-allowed`\n- Gap between box and text: `var(--ggui-spacing-2)`\n\n**IMPORTANT:** `onChange` receives the boolean checked state directly, NOT a\nReact `ChangeEvent`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Primary label text rendered beside the checkbox box. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| checked | `boolean` | - | Controlled checked state. |\n| onChange | `(checked: boolean) => void` | - | Change handler. Receives the new boolean checked state directly, NOT a React event. |\n| disabled | `boolean` | `false` | When true, sets `opacity: 0.5` and `cursor: not-allowed`. The native input is also disabled, preventing keyboard and click interaction. |\n| description | `string` | - | Secondary description text rendered below the label in smaller, muted type (`var(--ggui-font-size-xs)`, `var(--ggui-color-onSunken)`). |\n| indeterminate | `boolean` | `false` | When true, displays a horizontal dash instead of a checkmark. Used for \"select all\" states where some (but not all) children are checked. The `indeterminate` property is set via a ref on the native `<input>`. Visually identical to `checked` in terms of border and fill color. |\n\n**Example:**\n```tsx\n<Checkbox\n label=\"Accept terms\"\n description=\"You agree to the Terms of Service and Privacy Policy\"\n checked={accepted}\n onChange={setAccepted}\n/>\n```\n\n### Toggle\n\nToggle -- A switch/toggle input rendered as a pill-shaped track with a sliding knob.\n\nRenders a `<label>` wrapper with a `<div role=\"switch\">` track and an animated\ncircular knob. Does NOT use a native `<input>` -- keyboard interaction is handled\nmanually (Space and Enter keys toggle the state). The element is focusable via\n`tabIndex={0}` and shows a focus ring on focus.\n\nStyling:\n- Track (on): `var(--ggui-color-primary-600)`\n- Track (off): `var(--ggui-color-outline)`\n- Knob: white circle with `var(--ggui-shape-shadow-sm)`\n- Focus ring: `0 0 0 3px var(--ggui-color-primary-200)`\n- Transitions: background-color, box-shadow, knob position at 200ms ease-in-out\n- Disabled: `opacity: 0.5`, `cursor: not-allowed`, `tabIndex: -1`\n- Gap between toggle and label: `var(--ggui-spacing-2)`\n\n**IMPORTANT:** `onChange` receives the new boolean state directly (inverted from\ncurrent), NOT a React event.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Label text rendered to the right of the toggle track. Also used as `aria-label` on the switch element. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| checked | `boolean` | - | Controlled checked (on/off) state. |\n| onChange | `(checked: boolean) => void` | - | Change handler. Receives the new boolean state directly (i.e., `!checked`), NOT a React event. |\n| disabled | `boolean` | `false` | When true, sets `opacity: 0.5`, `cursor: not-allowed`, and removes the element from tab order (`tabIndex: -1`). Click and keyboard handlers are suppressed. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls track and knob dimensions: - `'sm'` -- track 36x20px, knob 16px diameter - `'md'` -- track 44x24px, knob 20px diameter - `'lg'` -- track 52x28px, knob 24px diameter |\n\n**Example:**\n```tsx\n<Toggle label=\"Enable notifications\" checked={enabled} onChange={setEnabled} size=\"md\" />\n```\n\n### RadioGroup\n\nRadioGroup -- A group of mutually exclusive radio options with optional label and error.\n\nRenders a `<div role=\"radiogroup\">` containing a label span, a flex container of\nradio options, and an optional error message. Each option is a `<label>` with a\nvisually-hidden native `<input type=\"radio\">` and a custom 18px circle indicator.\n\nStyling:\n- Selected circle: `2px solid var(--ggui-color-primary-600)` border with\n an 8px `var(--ggui-color-primary-600)` filled inner dot\n- Unselected circle: `2px solid var(--ggui-color-outline)` border,\n `var(--ggui-color-container)` fill\n- Circle radius: `var(--ggui-shape-radius-full)`\n- Transition: all 0.2s\n- Vertical gap: `var(--ggui-spacing-2)`, horizontal gap: `var(--ggui-spacing-4)`\n- Error: `var(--ggui-font-size-xs)`, `var(--ggui-color-error-500)`,\n `role=\"alert\"`\n- Disabled options: `opacity: 0.5`, `cursor: not-allowed`\n\nAccessibility: the group has `role=\"radiogroup\"` with `aria-labelledby` pointing\nto the label and `aria-describedby` pointing to the error message (when present).\nAll radio inputs share a common auto-generated `name` attribute.\n\n**IMPORTANT:** `onChange` receives the selected option's value string directly,\nNOT a React `ChangeEvent`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Group label rendered above the options. Used as `aria-labelledby` target on the `role=\"radiogroup\"` container. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| value | `string` | - | Controlled value. Should match one of `options[].value`. |\n| onChange | `(value: string) => void` | - | Change handler. Receives the newly selected option's value string directly, NOT a React event. |\n| options | `RadioOption[]` | - | Array of radio options. Must contain at least two options for meaningful selection. |\n| direction | `'vertical' \\| 'horizontal'` | `'vertical'` | Layout direction for the options container: - `'vertical'` -- column layout, `var(--ggui-spacing-2)` gap - `'horizontal'` -- row layout with `flex-wrap: wrap`, `var(--ggui-spacing-4)` gap |\n| disabled | `boolean` | `false` | When true, disables ALL options (individual `RadioOption.disabled` is additive). Each option gets `opacity: 0.5` and `cursor: not-allowed`. |\n| error | `string` | - | Error message displayed below all options in `var(--ggui-color-error-500)` with `role=\"alert\"`. Linked to the radiogroup via `aria-describedby`. |\n\n**Example:**\n```tsx\n<RadioGroup\n label=\"Plan\"\n value={plan}\n onChange={setPlan}\n options={[\n { value: 'free', label: 'Free', description: 'Up to 5 projects' },\n { value: 'pro', label: 'Pro', description: 'Unlimited projects' },\n ]}\n/>\n```\n\n### Slider\n\nSlider -- A range input with a custom-styled track, fill, and thumb.\n\nRenders a `<div>` wrapper containing an optional label/value header, and a\ntrack area with three layers: background track, colored fill, and a circular\nthumb. A native `<input type=\"range\">` is overlaid with `opacity: 0` to\nprovide accessible keyboard and pointer interaction.\n\nStyling:\n- Track: 6px tall, `var(--ggui-color-outlineVariant)` background, `border-radius: 3px`\n- Fill: `var(--ggui-color-primary-600)` (normal),\n `var(--ggui-color-outline)` (disabled)\n- Thumb: 20px white circle with `2px solid var(--ggui-color-primary-600)`,\n `var(--ggui-shape-shadow-sm)`; disabled border uses outline\n- Value display (when `showValue`): `var(--ggui-color-primary-600)`,\n `var(--ggui-font-size-sm)`, right-aligned in the header row\n- Fill and thumb transitions: 0.1s for smooth dragging\n\nAccessibility: the native `<input type=\"range\">` carries `aria-valuenow`,\n`aria-valuemin`, `aria-valuemax`, and is linked to the label via `aria-labelledby`.\nFalls back to `aria-label=\"Slider\"` when no label is provided.\n\n**IMPORTANT:** `onChange` receives the numeric value directly, NOT a React\n`ChangeEvent`. The value is coerced via `Number(e.target.value)`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Label rendered above the slider track (left-aligned). Used as `aria-labelledby` target on the native range input. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| value | `number` | `0` | Controlled numeric value. Must be between `min` and `max`. |\n| onChange | `(value: number) => void` | - | Change handler. Receives the new numeric value directly, NOT a React event. |\n| min | `number` | `0` | Minimum allowed value. |\n| max | `number` | `100` | Maximum allowed value. |\n| step | `number` | `1` | Step increment for the slider. Determines the granularity of selectable values. |\n| disabled | `boolean` | `false` | When true, sets `cursor: not-allowed` on the native input. The fill color changes to `var(--ggui-color-outline)` and the thumb border also uses outline. |\n| showValue | `boolean` | `false` | When true, displays the current numeric value right-aligned in the header row (beside the label) in `var(--ggui-color-primary-600)`. |\n\n**Example:**\n```tsx\n<Slider label=\"Volume\" value={volume} onChange={setVolume} min={0} max={100} step={5} showValue />\n```\n\n### Badge\n\nBadge -- Inline label for status indicators, counts, or categories.\n\nRenders a `<span>` with `display: inline-flex`, centered content, and\n`white-space: nowrap`. Semantic variant colors use background/text pairings\nfrom the 100/700 color scale. Pill shape uses `border-radius: 9999px`;\nnon-pill uses `var(--ggui-shape-radius-sm)`.\n\nFont weight: `var(--ggui-font-weight-medium)` across all variants.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| variant | `'default' \\| 'primary' \\| 'secondary' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'tertiary'` | `'default'` | Visual style. Maps to background/text color pairings: - `'default'` -- bg `var(--ggui-color-sunken)`, text `var(--ggui-color-onSunken)` - `'primary'` -- bg `var(--ggui-color-primary-100)`, text `var(--ggui-color-primary-700)` - `'secondary'` -- bg `var(--ggui-color-outlineVariant)`, text `var(--ggui-color-onContainer)` - `'success'` -- bg `var(--ggui-color-success-100)`, text `var(--ggui-color-success-700)` - `'warning'` -- bg `var(--ggui-color-warning-100)`, text `var(--ggui-color-warning-700)` - `'error'` -- bg `var(--ggui-color-error-100)`, text `var(--ggui-color-error-700)` - `'info'` -- bg `var(--ggui-color-info-100)`, text `var(--ggui-color-info-700)` |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls padding and font size: - `'sm'` -- padding `2px 6px`, font `var(--ggui-font-size-xs)` - `'md'` -- padding `2px 8px`, font `var(--ggui-font-size-xs)` - `'lg'` -- padding `4px 10px`, font `var(--ggui-font-size-sm)` |\n| pill | `boolean` | `true` | When true, uses fully rounded corners (`border-radius: 9999px`). When false, uses `var(--ggui-shape-radius-sm)`. |\n\n**Example:**\n```tsx\n<Badge variant=\"success\" size=\"sm\">Active</Badge>\n```\n\n### Spinner\n\nSpinner -- Animated SVG loading indicator.\n\nRenders an `<svg>` with `role=\"status\"` and `aria-label=\"Loading\"`.\nThe SVG contains a full outlineVariant background circle and a quarter-arc\nforeground stroke in the spinner color.\n\nAnimation: `ggui-spin 1s linear infinite` (360-degree rotation).\nThe `@keyframes ggui-spin` definition is injected inline via a `<style>` tag.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| size | `number` | `24` | Width and height of the SVG element in pixels. The internal viewBox is always `0 0 24 24`, so this controls rendered size only. |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `undefined (uses `var(--ggui-color-primary-600)`)` | Semantic color slot for the animated foreground arc. Same vocabulary as ; the theme decides the resolved value. The background circle always uses `var(--ggui-color-outlineVariant)`. Use `'inherit'` when the spinner sits inside a colored container (e.g. inside a Button) \u2014 the stroke picks up `currentColor` from the parent so it tracks the container's foreground. |\n\n**Example:**\n```tsx\n<Spinner size={32} tone=\"success\" />\n```\n\n### Avatar\n\nAvatar -- User or entity representation with image or auto-generated initials.\n\nRenders a `<div role=\"img\">` with `overflow: hidden` and `flex-shrink: 0`.\nWhen `src` is provided and loads successfully, renders an `<img>` with\n`object-fit: cover`. On image error (or when no `src`), falls back to\ninitials derived from `name` (up to 2 characters, uppercase).\n\nInitials background: deterministic color from a 5-color palette based on\nname hash (primary-500, success-500, warning-500, error-500, info-500).\nFalls back to `var(--ggui-color-outline)` when no name is given.\nInitials text: white, `font-weight: var(--ggui-font-weight-semibold)`,\n`font-size: resolvedSize * 0.4`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| src | `string` | - | Image URL. When provided and the image loads, it is rendered with `object-fit: cover`. On load error, falls back to initials. |\n| name | `string` | - | Name used for two purposes: 1. Generating initials (splits on spaces, takes first letter of each word, max 2). 2. Deterministic background color selection via character code hash. Also used as `aria-label` on the container. Falls back to `'Avatar'` if omitted. |\n| size | `number \\| 'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'` | `'md'` | Avatar dimensions. Named sizes map to pixel values: - `'xs'` -- 24px - `'sm'` -- 32px - `'md'` -- 40px - `'lg'` -- 48px - `'xl'` -- 64px Numeric values are used directly as pixel dimensions. |\n| shape | `'circle' \\| 'square'` | `'circle'` | Container shape. - `'circle'` -- `border-radius: 50%` - `'square'` -- `border-radius: var(--ggui-shape-radius-md)` |\n\n**Example:**\n```tsx\n<Avatar src=\"/photos/jane.jpg\" name=\"Jane Doe\" size=\"lg\" shape=\"circle\" />\n```\n\n### Alert\n\nAlert -- Contextual message box for important information with icon and optional dismiss.\n\nRenders a `<div role=\"alert\">` with flex layout (12px gap), variant-specific\nbackground, border, text color, and a leading icon. Each variant provides a\ndefault SVG icon (info circle, checkmark, warning triangle, or X circle) that\ncan be overridden via the `icon` prop.\n\nLayout: icon (flex-shrink: 0) | content column (title + body) | close button.\nBorder radius: `var(--ggui-shape-radius-lg)`.\nPadding: `12px 16px`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| variant | `'info' \\| 'success' \\| 'warning' \\| 'error'` | `'info'` | Visual style. Maps to background/border/text/icon color sets: - `'info'` -- bg `var(--ggui-color-info-50)`, border `var(--ggui-color-info-200)`, text `var(--ggui-color-info-800)`, icon `var(--ggui-color-info-500)` - `'success'` -- bg `var(--ggui-color-success-50)`, border `var(--ggui-color-success-200)`, text `var(--ggui-color-success-800)`, icon `var(--ggui-color-success-500)` - `'warning'` -- bg `var(--ggui-color-warning-50)`, border `var(--ggui-color-warning-200)`, text `var(--ggui-color-warning-800)`, icon `var(--ggui-color-warning-500)` - `'error'` -- bg `var(--ggui-color-error-50)`, border `var(--ggui-color-error-200)`, text `var(--ggui-color-error-800)`, icon `var(--ggui-color-error-500)` |\n| title | `string` | - | Optional title rendered above the body in semibold (`var(--ggui-font-weight-semibold)`), `var(--ggui-font-size-sm)`. Title and body are separated by `var(--ggui-spacing-1)` gap. |\n| closable | `boolean` | `false` | When true, renders a close button (X icon) in the top-right area. The button has `min-width: 28px`, `min-height: 28px`, and `opacity: 0.7`. Requires `onClose` to be functional. |\n| onClose | `() => void` | - | Callback fired when the close button is clicked. Only relevant when `closable` is true. |\n| icon | `ReactNode` | - | Custom icon ReactNode to replace the default variant icon. Rendered at the leading position with the variant's icon color applied via `color` CSS property. |\n\n**Example:**\n```tsx\n<Alert variant=\"warning\" title=\"Rate limit\" closable onClose={() => setShow(false)}>\n You have 3 requests remaining this minute.\n</Alert>\n```\n\n### Progress\n\nProgress -- Horizontal progress bar with determinate and indeterminate modes.\n\nRenders a track `<div role=\"progressbar\">` with a colored fill child.\nThe track background is `var(--ggui-color-outlineVariant)` with\npill-shaped corners (border-radius = height / 2).\n\nDeterminate mode: fill width transitions smoothly (`width 0.3s ease`).\nIndeterminate mode: fill is 30% width, animated with\n`ggui-progress-indeterminate 1.5s ease-in-out infinite`\n(translateX from -100% to 400%). The `@keyframes` are injected inline.\n\nAccessibility: `aria-valuenow` is set in determinate mode, omitted in\nindeterminate. `aria-valuemin` is always 0, `aria-valuemax` matches `max`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| value | `number` | - | Current progress value. Clamped to `[0, max]` and converted to a percentage for the fill width: `Math.min(100, Math.max(0, (value / max) * 100))`. |\n| max | `number` | `100` | Maximum value representing 100% progress. |\n| variant | `'default' \\| 'success' \\| 'warning' \\| 'error'` | `'default'` | Fill bar color. Maps to CSS variables: - `'default'` -- `var(--ggui-color-primary-600)` - `'success'` -- `var(--ggui-color-success-500)` - `'warning'` -- `var(--ggui-color-warning-500)` - `'error'` -- `var(--ggui-color-error-500)` |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls track height in pixels: - `'sm'` -- 4px - `'md'` -- 8px - `'lg'` -- 12px |\n| label | `string` | - | Accessible name describing what this bar measures, e.g. `\"Survey progress\"` or `\"Upload\"`. Becomes the progressbar's `aria-label` and \u2014 when `showLabel` is set \u2014 the visible header text in place of the generic word \"Progress\". Always pass this when the surrounding context does not already make the meaning obvious. |\n| showLabel | `boolean` | `false` | When true, displays a header row above the track with the `label` text (or \"Progress\" if `label` is unset) on the left, and the rounded percentage value on the right. |\n| indeterminate | `boolean` | `false` | When true, ignores `value` for visual width and plays a looping animation instead. The fill bar is 30% width and slides across the track. Animation: `ggui-progress-indeterminate 1.5s ease-in-out infinite`. `aria-valuenow` is omitted from the progressbar element. |\n\n**Example:**\n```tsx\n<Progress value={65} variant=\"success\" size=\"md\" showLabel />\n```\n\n### Image\n\nImage -- An `<img>` element with built-in error handling and fallback support.\n\nRenders a native `<img>` with `display: block`. On load error, either renders\nthe `fallback` ReactNode (if provided) or a default placeholder `<div>` with a\nsurfaceVariant background and a centered image SVG icon in outline.\n\nSize values: numbers are treated as pixels, strings are passed through as-is.\nWhen no `width` is set, defaults to `100%`. When no `height` is set, defaults\nto `auto`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| src | `string` | - | Image source URL. Load failure triggers the fallback state. |\n| alt | `string` | - | Alt text for the image. Used as `aria-label` in the error placeholder too. |\n| width | `number \\| string` | `'100%' (applied at render time, not on the type)` | Image width. Numbers are pixels, strings are CSS values (e.g., `'100%'`, `'50vw'`). |\n| height | `number \\| string` | `'auto' (applied at render time, not on the type)` | Image height. Numbers are pixels, strings are CSS values. |\n| objectFit | `'cover' \\| 'contain' \\| 'fill' \\| 'none' \\| 'scale-down'` | `'cover'` | CSS `object-fit` value controlling how the image fills its box. |\n| radius | `'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| number \\| string` | - | Corner radius applied to both the image and the error placeholder. Prefer a radius-scale name (`'none' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'`) \u2014 each resolves to the matching `--ggui-shape-radius-*` token. A number is treated as pixels; any other string is passed through. |\n| fallback | `ReactNode` | - | Custom ReactNode rendered when the image fails to load. When provided, completely replaces the default error placeholder (no wrapper div). When omitted, a surfaceVariant background div with an image icon is shown. |\n\n**Example:**\n```tsx\n<Image src=\"/hero.jpg\" alt=\"Hero banner\" width=\"100%\" height={400} objectFit=\"cover\" radius=\"md\" />\n```\n\n### Icon\n\nIcon -- a curated Lucide subset (~200 names) + emoji passthrough.\n\nThree resolution layers:\n1. **Lucide icon:** pass a name from the curated subset (e.g. `sun`, `cloud-rain`, `heart`, `shopping-cart`, `sparkles`, `arrow-up-right`).\n Accepts kebab-case, camelCase, or PascalCase. Renders as stroke SVG. ONLY the subset renders: an\n unknown name renders an empty box (never a placeholder glyph) \u2014 the `get_available_icons` tool lists the exact names.\n2. **Emoji:** pass emoji/unicode directly (e.g. `\u2600\uFE0F`, `\u{1F327}\uFE0F`). Rendered as text.\n3. **Custom SVG:** pass children (`<svg>` element) for full control.\n\nContainer: `<span>` with `display: inline-flex`, centered content.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| name | `string` | - | Lucide icon name from the curated subset (kebab-case, camelCase, or PascalCase all work); a name outside the subset renders an empty box. Also accepts emoji/unicode characters directly. |\n| size | `number` | `24` | Icon dimensions in pixels (applied to both width and height of the wrapper and the inner SVG element). |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `undefined (icon uses `currentColor`)` | Semantic color slot. Same vocabulary as ; the theme decides what each tone LOOKS like. Resolves to a CSS `color` on the wrapper which the inner SVG inherits via `currentColor`. Use `'inherit'` (the default behavior when unset) for icons that should pick up the parent's foreground color. |\n| children | `ReactNode` | - | Custom SVG children. When provided, `name` is ignored and children are rendered inside a sized `<span>` wrapper. |\n| 'aria-label' | `string` | - | Accessible name for a standalone, meaning-bearing icon. When set, the icon exposes `role=\"img\"` + this label. When omitted (the default) the icon is decorative and hidden from screen readers (`aria-hidden`) \u2014 the right choice for an icon next to a text label. |\n\n**Example:**\n```tsx\n<Icon name=\"search\" size={20} tone=\"muted\" />\n<Icon name=\"cloud-rain\" size={32} />\n<Icon name=\"\u2600\uFE0F\" size={24} />\n```\n\n### Link\n\nLink -- Styled anchor element with external link support.\n\nRenders a native `<a>` element. When `external` is true, sets\n`target=\"_blank\"` and `rel=\"noopener noreferrer\"`, and appends a small\n(12px) external-link SVG icon after the children.\n\nTransition: `color 0.2s`.\nUnderline behavior is controlled via mouseEnter/mouseLeave event handlers\n(for the `'hover'` mode).\n\nAlso extends native `AnchorHTMLAttributes` (except `style`/`className`),\nso props like `aria-*`, `data-*`, `title`, etc. are forwarded to the `<a>`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | children |\n| href | `string` | - | Destination URL. Passed directly to the `<a href>` attribute. |\n| external | `boolean` | `false` | When true, opens link in a new tab (`target=\"_blank\"`, `rel=\"noopener noreferrer\"`) and appends a 12px external-link icon after children. |\n| tone | `\\| 'default' \\| 'muted' \\| 'subtle' \\| 'emphasized' \\| 'loud' \\| 'success' \\| 'warning' \\| 'error' \\| 'info' \\| 'inverse' \\| 'inherit'` | `undefined (uses `var(--ggui-color-primary-600)`)` | Semantic color slot for the link text. Same vocabulary as ; the theme decides what each tone LOOKS like. Defaults to a primary-tinted accent (`'loud'`-ish) when unset. |\n| underline | `'always' \\| 'hover' \\| 'none'` | `'hover'` | Underline behavior: - `'always'` -- `text-decoration: underline` at all times - `'hover'` -- underline appears on mouse enter, removed on mouse leave - `'none'` -- no underline ever |\n\n**Example:**\n```tsx\n<Link href=\"https://docs.ggui.ai\" external>Documentation</Link>\n```\n\n### Tooltip\n\nTooltip -- Hoverable information popup positioned relative to a trigger element.\n\nWraps `children` in a `<div>` trigger (display: inline-block) and renders\na fixed-position tooltip `<div role=\"tooltip\">` when visible.\n\nTooltip appearance:\n- Background: `var(--ggui-color-onContainer)`\n- Text: white, `var(--ggui-font-size-xs)`\n- Padding: `6px 10px`, border-radius: `var(--ggui-shape-radius-md)`\n- Max width: 200px, `white-space: nowrap`, `pointer-events: none`\n- Z-index: `zIndex.tooltip` (1800)\n\nShow/hide: triggered by mouseEnter/mouseLeave AND focus/blur on the\ntrigger element. Uses `position: fixed` with coordinates calculated from\n`getBoundingClientRect()` and an 8px offset from the trigger edge.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | Trigger element. Wrapped in a `<div>` with mouseEnter/mouseLeave and focus/blur handlers. |\n| content | `ReactNode` | - | Tooltip content. Can be text or any ReactNode. |\n| position | `'top' \\| 'bottom' \\| 'left' \\| 'right'` | `'top'` | Tooltip placement relative to the trigger element: - `'top'` -- above, centered horizontally, transformed `translateX(-50%) translateY(-100%)` - `'bottom'` -- below, centered horizontally, transformed `translateX(-50%)` - `'left'` -- to the left, centered vertically, transformed `translateX(-100%) translateY(-50%)` - `'right'` -- to the right, centered vertically, transformed `translateY(-50%)` |\n| delay | `number` | `200` | Delay in milliseconds before the tooltip becomes visible after hover/focus. Hiding is immediate (no delay). |\n\n**Example:**\n```tsx\n<Tooltip content=\"Copy to clipboard\" position=\"top\">\n <Button variant=\"ghost\"><Icon name=\"copy\" /></Button>\n</Tooltip>\n```\n\n### Table\n\nTable -- Data table with sortable columns, striped rows, and hover highlights.\n\nRenders a scrollable wrapper `<div>` containing a native `<table>` with\n`border-collapse: collapse` and `width: 100%`. The wrapper has\n`overflow-x: auto` for horizontal scrolling on narrow viewports.\n\nHeader row: 2px bottom border (`var(--ggui-color-outlineVariant)`).\nData rows: 1px bottom border (`var(--ggui-color-sunken)`).\nHover: `var(--ggui-color-container)` background with 150ms ease transition.\nStriped: alternating rows (odd index) get `var(--ggui-color-container)`.\n\nSort behavior: clicking a sortable column header calls `onSort(key, direction)`.\nIf the same column is clicked again while ascending, it toggles to descending.\nThe component does NOT sort data internally -- the parent must sort `data` and\npass updated `sortKey`/`sortDirection`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| columns | `TableColumn<T>[]` | - | Array of column definitions controlling header labels, data keys, and rendering. |\n| data | `T[]` | - | Array of row data objects. Each object's keys should match the column `key` values. |\n| sortKey | `string` | - | The `key` of the currently sorted column. Used to highlight the active sort indicator and determine toggle direction on next click. |\n| sortDirection | `SortDirection` | `'asc'` | Current sort direction for the column identified by `sortKey`. Controls which triangle indicator is highlighted in the header. |\n| onSort | `(key: string, direction: SortDirection) => void` | - | Sort change handler. Called when a sortable column header is clicked. Receives the column `key` and the new `SortDirection`. The component does NOT sort data internally -- you must sort `data` in your state and pass updated `sortKey`/`sortDirection`. |\n| striped | `boolean` | `false` | When true, alternating rows (odd index) get a `var(--ggui-color-container)` background. |\n| hoverable | `boolean` | `true` | When true, rows highlight with `var(--ggui-color-container)` on mouse enter, with a 150ms ease background-color transition. |\n| compact | `boolean` | `false` | When true, reduces cell padding: - Compact: `var(--ggui-spacing-1) var(--ggui-spacing-2)` - Normal: `var(--ggui-spacing-2) var(--ggui-spacing-4)` |\n| bordered | `boolean` | `false` | When true, adds a 1px border around the table wrapper and between cells. Wrapper border: `1px solid var(--ggui-color-outlineVariant)`. Cell borders: `1px solid var(--ggui-color-sunken)`. Wrapper border-radius: `var(--ggui-shape-radius-lg)`. |\n| caption | `string` | - | Accessible table caption. Rendered as a `<caption>` element with `caption-side: top`, `var(--ggui-font-size-sm)`, `var(--ggui-color-onSunken)`. |\n\n**Example:**\n```tsx\n<Table\n columns={[\n { key: 'name', header: 'Name', sortable: true },\n { key: 'role', header: 'Role' },\n { key: 'status', header: 'Status', render: (v) => <Badge variant={v as string}>{v as string}</Badge> },\n ]}\n data={users}\n sortKey={sortKey}\n sortDirection={sortDir}\n onSort={(key, dir) => { setSortKey(key); setSortDir(dir); }}\n striped\n/>\n```\n\n### Tabs\n\nTabs -- Accessible tab navigation with panels and keyboard support.\n\nRenders a `<div role=\"tablist\">` with `<button role=\"tab\">` elements and a\n`<div role=\"tabpanel\">` for the active tab's content. Supports controlled\n(`activeKey` + `onChange`) and uncontrolled (internal state) modes.\n\nKeyboard navigation: ArrowLeft/ArrowRight (and ArrowUp/ArrowDown) cycle\nthrough enabled tabs. Home/End jump to first/last. Focus follows selection.\nDisabled tabs are skipped during keyboard navigation.\n\n**IMPORTANT:** `onChange` receives the tab's `key` string directly, NOT a\nReact event.\n\nTab panel padding: `var(--ggui-spacing-4) 0` (top/bottom only).\nTransitions: color, background-color, border-color at 200ms ease-in-out.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| items | `TabItem[]` | - | Array of tab definitions. Must contain at least one item. |\n| activeKey | `string` | - | Controlled active tab key. When provided, the component is controlled and will not manage its own state. Must match one of `items[].key`. When omitted, defaults to the first item's key (uncontrolled mode). |\n| onChange | `(key: string) => void` | - | Tab change handler. Receives the selected tab's `key` string directly, NOT a React event. In controlled mode, you must update `activeKey` in response to this callback. |\n| variant | `'line' \\| 'pills' \\| 'enclosed'` | `'line'` | Visual style of the tab bar: - `'line'` -- underline indicator (2px solid primary-600 on active), border-bottom on tab list - `'pills'` -- filled pill buttons (primary-600 bg, white text on active), surfaceVariant container with radius-lg - `'enclosed'` -- bordered tab buttons with open bottom (card-style), border-bottom on tab list |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls tab button padding and font size: - `'sm'` -- padding `var(--ggui-spacing-1) var(--ggui-spacing-2)`, font `var(--ggui-font-size-xs)` - `'md'` -- padding `var(--ggui-spacing-2) var(--ggui-spacing-4)`, font `var(--ggui-font-size-sm)` - `'lg'` -- padding `var(--ggui-spacing-4) var(--ggui-spacing-6)`, font `var(--ggui-font-size-base)` |\n| fullWidth | `boolean` | `false` | When true, tab buttons expand equally to fill the container width (`flex: 1`, `justify-content: center` on each button). |\n\n**Example:**\n```tsx\n<Tabs\n variant=\"pills\"\n items={[\n { key: 'overview', label: 'Overview', content: <Overview /> },\n { key: 'settings', label: 'Settings', content: <Settings /> },\n ]}\n activeKey={tab}\n onChange={setTab}\n/>\n```\n\n### Toast\n\nToast -- Notification banner with auto-dismiss and slide-in animation.\n\nRenders a `<div role=\"alert\" aria-live=\"assertive\">` with a variant-specific\nicon, optional title, message body, and optional close button.\n\nAnimation: `ggui-slideInUp 200ms ease-out both` on mount (from the motion\ntoken system). The keyframes are provided by the MotionKeyframes provider.\n\nAuto-dismiss: when `onClose` is provided and `duration > 0`, a timer calls\n`onClose` after `duration` ms. Setting `duration` to `0` disables auto-dismiss.\nThe timer resets if `visible`, `duration`, or `onClose` changes.\n\nDimensions: `min-width: 280px`, `max-width: 420px`.\nShadow: `var(--ggui-shape-shadow-lg)`.\nBorder radius: `var(--ggui-shape-radius-lg)`.\n\nWhen `visible` is false, renders nothing (returns `null`).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| message | `ReactNode` | - | Message body content. Can be text or any ReactNode. |\n| variant | `'info' \\| 'success' \\| 'warning' \\| 'error'` | `'info'` | Visual style. Maps to background/border/text/icon color sets (same palette as Alert): - `'info'` -- bg `var(--ggui-color-info-50)`, border `var(--ggui-color-info-200)`, text `var(--ggui-color-info-800)` - `'success'` -- bg `var(--ggui-color-success-50)`, border `var(--ggui-color-success-200)`, text `var(--ggui-color-success-800)` - `'warning'` -- bg `var(--ggui-color-warning-50)`, border `var(--ggui-color-warning-200)`, text `var(--ggui-color-warning-800)` - `'error'` -- bg `var(--ggui-color-error-50)`, border `var(--ggui-color-error-200)`, text `var(--ggui-color-error-800)` |\n| title | `string` | - | Optional title rendered above the message in semibold (`var(--ggui-font-weight-semibold)`, `var(--ggui-font-size-sm)`). |\n| duration | `number` | `5000` | Auto-dismiss delay in milliseconds. After this duration, `onClose` is called automatically. Set to `0` to disable auto-dismiss (toast stays until manually closed). The timer is only active when both `visible` is true and `onClose` is provided. |\n| onClose | `() => void` | - | Callback fired on auto-dismiss timeout or when the close button is clicked. When provided, a close button (X icon, 16px) is rendered in the top-right area. When omitted, no close button is shown and auto-dismiss is disabled. |\n| visible | `boolean` | `true` | Controls rendering. When false, the component returns `null`. Toggling from false to true triggers the slide-in animation. |\n| position | `'top-right' \\| 'top-left' \\| 'bottom-right' \\| 'bottom-left' \\| 'top-center' \\| 'bottom-center'` | - | Intended screen position. This prop is defined on the interface but is NOT implemented by the Toast component itself -- positioning must be handled by a parent container or toast manager. |\n\n**Example:**\n```tsx\n<Toast variant=\"success\" title=\"Saved\" message=\"Your changes have been saved.\" onClose={() => setShow(false)} />\n```\n\n### Accordion\n\nAccordion -- Collapsible content sections with chevron rotation animation.\n\nRenders a vertical list of items, each with a `<button>` header (inside `<h3>`)\nand a `<div role=\"region\">` panel. Supports controlled (`expandedKeys` + `onChange`)\nand uncontrolled (internal state) modes.\n\nChevron animation: the trailing chevron icon rotates from 0deg (collapsed) to\n180deg (expanded) with `transition: transform 200ms ease-in-out`.\n\nHeader button: full-width flex layout (`justify-content: space-between`),\n`var(--ggui-font-size-sm)`, `var(--ggui-font-weight-medium)`,\n`var(--ggui-color-onContainer)`.\nHeader padding: `var(--ggui-spacing-2) var(--ggui-spacing-4)`.\nBackground transition: `background-color 100ms ease-in-out`.\n\n**IMPORTANT:** `onChange` receives the full array of currently expanded keys,\nNOT a single key or a React event. In single mode (`multiple: false`), this\narray will have at most one element.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| items | `AccordionItem[]` | - | Array of collapsible section definitions. |\n| expandedKeys | `string[]` | - | Controlled expanded state. Array of item `key` values that should be open. When provided, the component is controlled and will not manage its own expansion state. When omitted, defaults to `[]` (all collapsed, uncontrolled mode). |\n| onChange | `(expandedKeys: string[]) => void` | - | Expand/collapse handler. Receives the complete array of expanded keys after a toggle. In controlled mode, you must update `expandedKeys` in response to this callback. |\n| multiple | `boolean` | `false` | When true, multiple items can be open simultaneously. When false, opening one item closes any other open item (single-expand mode). |\n| variant | `'default' \\| 'bordered' \\| 'separated'` | `'default'` | Visual style controlling borders and spacing: - `'default'` -- top border on first item, bottom border on all items (`var(--ggui-color-outlineVariant)`), no gap between items - `'bordered'` -- connected card style with left/right/bottom borders on all items, top border on first, shared rounded corners (radius-lg on first/last) - `'separated'` -- each item is an independent card with full border (`1px solid var(--ggui-color-outlineVariant)`), `var(--ggui-shape-radius-lg)` radius, `var(--ggui-spacing-2)` gap between items |\n\n**Example:**\n```tsx\n<Accordion\n variant=\"separated\"\n items={[\n { key: 'faq1', title: 'How do I get started?', content: 'Sign up and...' },\n { key: 'faq2', title: 'What is the pricing?', content: 'We offer...' },\n ]}\n expandedKeys={expanded}\n onChange={setExpanded}\n multiple\n/>\n```\n\n### Support Types\n\n**ResponsiveColumns:**\n\nExplicit column count per viewport breakpoint, mobile-first. `base`\napplies from 0 up; each named key overrides at and above its\nbreakpoint width (`sm` 640px, `md` 768px, `lg` 1024px, `xl` 1280px).\nOmit `base` to default to a single column on the narrowest screens.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| base | `number` | Columns below the `sm` breakpoint. |\n| sm | `number` | Columns from 640px up. |\n| md | `number` | Columns from 768px up. |\n| lg | `number` | Columns from 1024px up. |\n| xl | `number` | Columns from 1280px up. |\n\n**SelectOption:**\n\nAn individual option within a `Select` dropdown.\n\nRendered as a native `<option>` element. When `disabled` is true, the option\nis visible but not selectable.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| value | `string` | The value submitted when this option is selected. Must be unique within the options array. |\n| label | `string` | The display text shown in the dropdown. |\n| disabled | `boolean` | When true, the option is visible but cannot be selected (grayed out by the browser). |\n\n**RadioOption:**\n\nAn individual option within a `RadioGroup`.\n\nRendered as a `<label>` containing a visually-hidden `<input type=\"radio\">`\nand a custom 18px circle indicator. Supports an optional description line\nbelow the label text.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| value | `string` | The value emitted via `RadioGroupProps.onChange` when this option is selected. Must be unique. |\n| label | `string` | Display text for this option. Styled with `var(--ggui-font-size-sm)` and `var(--ggui-color-onSunken)`. |\n| description | `string` | Optional secondary description rendered below the label in smaller, muted type (`var(--ggui-font-size-xs)`, `var(--ggui-color-onSunken)`). |\n| disabled | `boolean` | When true, this individual option is grayed out (`opacity: 0.5`) and cannot be selected, regardless of the group-level `disabled` prop. |\n\n**TableColumn:**\n\nColumn definition for the Table component.\n\nEach column maps a `key` in the row data object to a table column with\na header label, optional sorting, alignment, width, and custom rendering.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| key | `string` | Property key in the row data object. Used to extract cell values via `row[key]`. Must be unique across all columns in a Table. |\n| header | `string` | Column header text. Rendered in uppercase, `var(--ggui-font-size-xs)`, `var(--ggui-font-weight-semibold)`, `var(--ggui-color-onSunken)`, with `letter-spacing: 0.05em`. |\n| width | `number \\| string` | Fixed column width. Numbers are pixels, strings are CSS values (e.g., `'200px'`, `'30%'`). When omitted, the column auto-sizes based on content. |\n| align | `'left' \\| 'center' \\| 'right'` | Horizontal text alignment for both the header and data cells. |\n| sortable | `boolean` | When true, the header cell becomes clickable and shows sort direction indicators (ascending/descending triangles). Clicking toggles between `'asc'` and `'desc'`. The header gets `cursor: pointer`, `tabIndex: 0`, and keyboard support (Enter/Space to toggle). |\n| render | `(value: unknown, row: T, index: number) => ReactNode` | Custom cell renderer. When provided, called instead of rendering `row[key]` directly. Receives the cell value, the full row object, and the row index. |\n\n**TabItem:**\n\nDefinition of a single tab within a Tabs component.\n\nEach item provides a unique `key` for identification, a `label` for the\ntab button, and `content` for the associated panel.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| key | `string` | Unique identifier for this tab. Used to match `activeKey` and as the value passed to `onChange`. |\n| label | `ReactNode` | Tab button label. Can be text or any ReactNode. |\n| content | `ReactNode` | Panel content rendered below the tab bar when this tab is active. |\n| disabled | `boolean` | When true, the tab button shows `opacity: 0.5`, `cursor: not-allowed`, and cannot be selected via click or keyboard navigation. |\n| icon | `ReactNode` | Optional icon rendered before the label inside the tab button, with `var(--ggui-spacing-1)` gap between icon and label. |\n\n**AccordionItem:**\n\nDefinition of a single collapsible section within an Accordion.\n\nEach item provides a unique `key`, a clickable `title` for the header button,\nand `content` revealed when expanded.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| key | `string` | Unique identifier for this item. Used in `expandedKeys` and passed to `onChange`. |\n| title | `ReactNode` | Header label rendered inside the toggle button. Can be text or any ReactNode. |\n| content | `ReactNode` | Panel content rendered below the header when expanded. Styled with `var(--ggui-font-size-sm)`, `var(--ggui-color-onSunken)`, `line-height: var(--ggui-font-lineHeight-normal)`. Padding: `0 var(--ggui-spacing-4) var(--ggui-spacing-4)`. |\n| disabled | `boolean` | When true, the header button shows `opacity: 0.5`, `cursor: not-allowed`, and cannot be toggled. |\n\n\n## Components\n\nImport: `import { Component } from '@ggui-ai/design'`\n\n### SearchField\n\nA text input with a leading search icon and optional submit button.\n\nComposes: `Input` (native `<input type=\"search\">`), `Button`, `Spinner`, `Icon`.\n\nSupports controlled and uncontrolled usage. When `value` is `undefined` the\ncomponent tracks its own state internally. Pressing **Enter** triggers\n`onSearch` with the current value. When `loading` is `true` the search icon\nis replaced by a `Spinner` and the input is disabled.\n\nTokens used: `colors.gray[300]` border, `colors.gray[50]` disabled bg,\n`colors.gray[400]` icon color, `colors.gray[900]` text color.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| value | `string` | - | Current search value. When provided, the component is **controlled** and the caller must update this via `onChange`. When omitted, the component manages its own internal state. |\n| onChange | `(value: string) => void` | - | Called on every keystroke with the new input string (value directly, not a React `ChangeEvent`). |\n| onSearch | `(value: string) => void` | - | Called when the user presses **Enter** or clicks the search button (if `showButton` is `true`). Receives the current value directly. |\n| placeholder | `string` | `'Search...'` | Placeholder text shown when the input is empty. |\n| showButton | `boolean` | `false` | When `true`, renders a `Button` primitive to the right of the input. The button's label is set by `buttonText`. |\n| buttonText | `string` | `'Search'` | Label rendered inside the submit button. Only visible when `showButton` is `true`. |\n| loading | `boolean` | - | When `true`, replaces the search icon with a `Spinner`, disables the input, and disables the submit button. |\n| disabled | `boolean` | - | When `true`, the input and button are visually disabled and do not respond to interaction. The input background changes to `colors.gray[50]`. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls input height and font size. - `'sm'` -- 6px vertical padding, 14px font - `'md'` -- 10px vertical padding, 14px font - `'lg'` -- 12px vertical padding, 16px font The Button size maps `sm`->`sm`, `md`->`md`, `lg`->`md`. |\n\n**Example:**\n```tsx\n```tsx\n<SearchField\n value={query}\n onChange={setQuery}\n onSearch={(q) => fetchResults(q)}\n placeholder=\"Search products...\"\n showButton\n buttonText=\"Go\"\n size=\"md\"\n/>\n```\n```\n\n### FormField\n\nA wrapper that adds a label, optional description, error message,\nand helper text around any form input passed as `children`.\n\nComposes: no other primitives -- pure layout with semantic `<label>` and\n`<span>` elements.\n\nVisual hierarchy (top to bottom):\n1. **Label** (required) -- `fontSize.sm`, `fontWeight.medium`, `colors.gray[700]`\n2. **Required indicator** -- red asterisk (`colors.error[500]`) appended to label\n3. **Description** -- `fontSize.xs`, `colors.gray[500]`, 4px bottom margin\n4. **Children** -- the form control itself\n5. **Error / Helper text** -- `fontSize.xs`; error in `colors.error[500]`,\n helper in `colors.gray[500]`. Error takes priority when both are provided.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Text rendered inside the `<label>` element above the input. |\n| children | `ReactNode` | - | The form control (typically an `Input`, `Select`, or `Textarea` primitive) rendered between the label/description and the error/helper row. |\n| error | `string` | - | Error message displayed below `children` in `colors.error[500]`. When present, it takes precedence over `helperText`. |\n| helperText | `string` | - | Neutral guidance text displayed below `children` in `colors.gray[500]`. Hidden when `error` is present. |\n| required | `boolean` | - | When `true`, appends a red asterisk (`*`) after the label text. Does **not** add any HTML validation attributes -- handle that on the child input. |\n| description | `string` | - | Secondary description rendered between the label and the child control in `fontSize.xs` / `colors.gray[500]`. Use for longer guidance that does not belong in `helperText`. |\n\n**Example:**\n```tsx\n```tsx\n<FormField\n label=\"Email address\"\n required\n description=\"We will never share your email.\"\n error={errors.email}\n>\n <Input value={email} onChange={setEmail} placeholder=\"you@example.com\" />\n</FormField>\n```\n```\n\n### MenuItem\n\nA full-width clickable row for menus, sidebars, and action lists.\n\nComposes: none -- renders a native `<button>` element.\n\nBuilt-in transition: `background-color 0.15s` on hover.\n\nColor logic:\n- **Normal**: text `colors.gray[700]`, hover bg `colors.gray[100]`\n- **Active**: bg `colors.primary[50]`, text `colors.gray[700]`, `fontWeight.medium`\n- **Danger**: text `colors.error[600]`, hover bg `colors.error[50]`, active bg `colors.error[100]`\n- **Disabled**: text `colors.gray[400]`, `cursor: not-allowed`\n\nLayout: flexbox row with `8px` gap, `8px 12px` padding, `radius.md` border-radius.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | Primary text content of the menu item. |\n| icon | `ReactNode` | - | Icon or element rendered to the left of the label. Flex-shrink 0. |\n| rightElement | `ReactNode` | - | Element rendered to the right of the label (e.g., a keyboard shortcut badge or a count). Colored `colors.gray[400]`, flex-shrink 0. |\n| onClick | `() => void` | - | Called when the item is clicked. Suppressed when `disabled` is `true`. |\n| disabled | `boolean` | - | When `true`, the item is non-interactive: `colors.gray[400]` text, `cursor: not-allowed`, click handler suppressed. |\n| active | `boolean` | - | Marks this item as the current selection. Applies a tinted background (`colors.primary[50]`, or `colors.error[100]` when `danger` is also set) and `fontWeight.medium`. |\n| danger | `boolean` | - | Switches the item to destructive styling: `colors.error[600]` text, `colors.error[50]` hover background. |\n\n**Example:**\n```tsx\n```tsx\n<MenuItem\n label=\"Delete project\"\n icon={<Icon name=\"trash\" size={16} />}\n danger\n onClick={() => confirmDelete(projectId)}\n/>\n```\n```\n\n### Tag\n\nAn inline label for categories, filters, statuses, or selections.\nOptionally dismissable via a close button.\n\nComposes: none -- pure `<span>` with an optional close `<button>`.\n\nEach `variant` maps to a background / text / border color triple from the\ndesign tokens:\n- `'default'` -- `gray[100]` / `gray[700]` / `gray[200]`\n- `'primary'` -- `primary[50]` / `primary[700]` / `primary[200]`\n- `'success'` -- `success[50]` / `success[700]` / `success[200]`\n- `'warning'` -- `warning[50]` / `warning[700]` / `warning[200]`\n- `'error'` -- `error[50]` / `error[700]` / `error[200]`\n- `'info'` -- `info[50]` / `info[700]` / `info[200]`\n\nThe close button renders an inline SVG \"x\" icon (12x12) with `opacity: 0.7`\nand an `aria-label=\"Remove\"`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | Tag content -- typically a short text string. |\n| variant | `'default' \\| 'primary' \\| 'success' \\| 'warning' \\| 'error' \\| 'info'` | `'default'` | Semantic color variant applied to background, text, and border. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls padding, font size, and internal gap. - `'sm'` -- 2px/6px padding, `fontSize.xs`, 4px gap - `'md'` -- 4px/8px padding, `fontSize.xs`, 6px gap - `'lg'` -- 6px/10px padding, `fontSize.sm`, 6px gap |\n| closable | `boolean` | - | When `true`, renders a small close (\"x\") button after the content. Clicking it fires `onClose`. |\n| onClose | `() => void` | - | Called when the close button is clicked. Only relevant when `closable` is `true`. |\n| icon | `ReactNode` | - | Icon or element rendered before the text content. |\n\n**Example:**\n```tsx\n```tsx\n<Tag variant=\"success\" size=\"md\" closable onClose={() => removeFilter(id)}>\n Active\n</Tag>\n```\n```\n\n### Dropdown\n\nA click-triggered menu anchored to a trigger element. Manages its own\nopen/close state internally.\n\nComposes: `MenuItem` for each option.\n\nBehavior:\n- Clicking the trigger toggles the menu open/closed.\n- Selecting an option calls `onChange(option.value)` and closes the menu.\n- Clicking outside the container or pressing **Escape** closes the menu.\n- The currently selected option (matching `value`) is rendered with\n `MenuItem`'s `active` state.\n\nMenu panel: `colors.white` bg, `colors.gray[200]` border, `radius.lg`\nborder-radius, `shadow.lg`, `zIndex.dropdown`, 160px min-width, 4px padding.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| trigger | `ReactNode` | - | The element the user clicks to open the menu. Receives a wrapping `<div>` with `cursor: pointer` (or `not-allowed` when disabled). |\n| options | `DropdownOption[]` | - | Array of selectable options rendered as `MenuItem` rows. |\n| value | `string` | - | The `value` of the currently selected option. The matching `MenuItem` is rendered with `active` styling. |\n| onChange | `(value: string) => void` | - | Called with the `value` string of the selected option (not the full `DropdownOption` object). The menu closes immediately after. |\n| placement | `'bottom-start' \\| 'bottom-end' \\| 'top-start' \\| 'top-end'` | `'bottom-start'` | Where to anchor the menu panel relative to the trigger. - `'bottom-start'` -- below, aligned to left edge - `'bottom-end'` -- below, aligned to right edge - `'top-start'` -- above, aligned to left edge - `'top-end'` -- above, aligned to right edge |\n| disabled | `boolean` | - | When `true`, the trigger shows `cursor: not-allowed` and clicking it does not open the menu. |\n\n**Example:**\n```tsx\n```tsx\n<Dropdown\n trigger={<Button variant=\"outline\">Sort by</Button>}\n options={[\n { value: 'name', label: 'Name' },\n { value: 'date', label: 'Date created' },\n { value: 'delete', label: 'Delete', danger: true },\n ]}\n value={sortBy}\n onChange={setSortBy}\n placement=\"bottom-end\"\n/>\n```\n```\n\n### Autocomplete\n\nA text input with a filterable suggestion dropdown, keyboard navigation,\nand loading/empty states.\n\nComposes: `Input` primitive (with `label`, `error`, `placeholder` forwarded),\n`Spinner` (loading state).\n\nFiltering: options are filtered client-side by case-insensitive substring\nmatch against both `option.label` and `option.value`.\n\nKeyboard support:\n- **ArrowDown / ArrowUp** -- move highlight through filtered options\n (opens dropdown if closed)\n- **Enter** -- selects the highlighted option\n- **Escape** -- closes the dropdown\n\nOn selection, `onChange` is called with `option.label` (the display text)\nand `onSelect` is called with the full `AutocompleteOption` object.\n\nDropdown panel: `colors.white` bg, `colors.gray[200]` border, `radius.lg`\nborder-radius, `shadow.lg`, `zIndex.dropdown`, max-height 240px with\noverflow scroll.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| value | `string` | `''` | Current text in the input field. On selection, this is set to the selected option's `label`. |\n| onChange | `(value: string) => void` | - | Called on every keystroke with the new input string (value directly, not a React event). Also called on selection with `option.label`. |\n| onSelect | `(option: AutocompleteOption) => void` | - | Called when the user selects an option (click or Enter on highlighted item). Receives the full `AutocompleteOption` object, not just the value string. |\n| options | `AutocompleteOption[]` | - | The full list of available options. Filtering is handled internally via case-insensitive substring match on `label` and `value`. |\n| placeholder | `string` | - | Placeholder text forwarded to the inner `Input` primitive. |\n| label | `string` | - | Label text forwarded to the inner `Input` primitive. |\n| loading | `boolean` | - | When `true`, the dropdown shows a centered `Spinner` instead of the option list. The input remains interactive. |\n| disabled | `boolean` | - | When `true`, the inner `Input` is disabled and the dropdown does not open. |\n| error | `string` | - | Error message forwarded to the inner `Input` primitive. |\n| noResultsText | `string` | `'No results found'` | Text shown in the dropdown when filtering produces zero matches. Rendered centered in `colors.gray[500]` / `fontSize.sm`. |\n\n**Example:**\n```tsx\n```tsx\n<Autocomplete\n label=\"Country\"\n value={country}\n onChange={setCountry}\n onSelect={(opt) => setCountryCode(opt.value)}\n options={countries}\n placeholder=\"Type to search...\"\n noResultsText=\"No countries found\"\n/>\n```\n```\n\n### Breadcrumb\n\nA horizontal navigation trail showing the user's location within a\nhierarchy. Renders a `<nav aria-label=\"Breadcrumb\">`.\n\nComposes: `Link` primitive (for items with `href`).\n\nRendering rules per item:\n- **Last item**: static `<span>` with `colors.gray[900]`, `fontWeight: 500`,\n and `aria-current=\"page\"`.\n- **Non-last with `href`**: `Link` in `colors.gray[500]` with\n `underline=\"hover\"`. If `onItemClick` is provided, `e.preventDefault()`\n is called and the handler fires instead of navigating.\n- **Non-last without `href`**: unstyled `<button>` in `colors.gray[500]`.\n\nLayout: flexbox row, `8px` gap, `fontSize.sm`.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| items | `BreadcrumbItem[]` | - | Ordered array of breadcrumb segments from root to current page. |\n| separator | `ReactNode` | `'/'` | Separator rendered between each pair of items. Can be a string (e.g., `\"/\"`, `\">\"`) or a ReactNode (e.g., an `Icon`). Rendered in `colors.gray[400]`. |\n| onItemClick | `(item: BreadcrumbItem, index: number) => void` | - | Called when a non-last item is clicked. Receives the `BreadcrumbItem` and its zero-based `index`. When provided on items that have `href`, the default navigation is prevented via `e.preventDefault()`. |\n\n**Example:**\n```tsx\n```tsx\n<Breadcrumb\n items={[\n { label: 'Home', href: '/' },\n { label: 'Projects', href: '/projects' },\n { label: 'ggui' },\n ]}\n separator=\"/\"\n onItemClick={(item) => router.push(item.href!)}\n/>\n```\n```\n\n### Pagination\n\nPage navigation controls with previous/next arrows, numbered page buttons,\nand optional first/last jumps. Renders a `<nav aria-label=\"Pagination\">`.\n\nComposes: `Button` (ghost variant for prev/next/first/last arrows),\n`Icon` (`chevron-left`, `chevron-right`).\n\nBuilt-in transition: `all 0.15s` on page number buttons.\n\nPage windowing: when `totalPages > maxVisible`, the component shows the\nfirst page, last page, a window of pages around `currentPage`, and\nellipsis (\"...\") markers for gaps. The window adjusts when near the\nstart or end of the range.\n\nActive page button: `colors.primary[600]` bg, `colors.white` text,\n`fontWeight.medium`. Inactive: transparent bg, `colors.gray[700]` text.\n\nArrow buttons are automatically disabled at boundary pages (first/last).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| currentPage | `number` | - | Current active page. **1-indexed** (first page is `1`). |\n| totalPages | `number` | - | Total number of pages. Determines when last-page / next-page buttons disable. |\n| onPageChange | `(page: number) => void` | - | Called when the user clicks a page number, arrow, or first/last button. Receives the target page number (1-indexed) directly. |\n| showFirstLast | `boolean` | `true` | When `true`, renders double-chevron buttons for jumping to the first and last page. These buttons are disabled when already on the respective boundary. |\n| maxVisible | `number` | `5` | Maximum number of page buttons visible at once (including the first and last page, but excluding ellipsis markers). When `totalPages` exceeds this value, ellipsis gaps appear. |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls button dimensions and icon sizes. - `'sm'` -- 28px buttons, 14px icons, Button size `xs` - `'md'` -- 32px buttons, 16px icons, Button size `sm` - `'lg'` -- 40px buttons, 20px icons, Button size `md` |\n| disabled | `boolean` | - | When `true`, all page buttons and arrows are visually dimmed (`opacity: 0.5`) and clicks are suppressed. |\n\n**Example:**\n```tsx\n```tsx\n<Pagination\n currentPage={page}\n totalPages={20}\n onPageChange={setPage}\n maxVisible={7}\n size=\"md\"\n/>\n```\n```\n\n### EmptyState\n\nEmptyState -- placeholder for a region with no data: empty lists,\nzero search results, an error fallback. Render it instead of\nnothing whenever a data array could be empty \u2014 a list that shows\nnothing when empty looks broken.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| icon | `string \\| ReactNode` | - | A Lucide icon name (kebab-case), rendered large and subtle above the title, or a custom node. Omit for a text-only empty state. |\n| title | `string` | - | The headline, e.g. \"No results found\". |\n| description | `string` | - | Optional supporting line below the title. |\n| action | `ReactNode` | - | Optional call-to-action, typically a `<Button>`. |\n\n**Example:**\n```tsx\n{results.length === 0\n ? <EmptyState icon=\"search-x\" title=\"No matches\" description=\"Try a broader query.\" />\n : results.map((r) => <Row key={r.id}>\u2026</Row>)}\n```\n\n### Stat\n\nStat -- a single KPI / metric: a label, a large value, an optional\ntrend-coloured delta and icon. Reach for it whenever the UI is\n\"show a number\" \u2014 dashboards, weather and price cards, analytics\ntiles. Drop several into a `<Grid>` for a stat grid.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| label | `string` | - | The metric name, e.g. \"Revenue\". Rendered small and uppercase above the value. |\n| value | `string \\| number` | - | The headline value \u2014 the big number. A number, or a pre-formatted string (`\"$48.2k\"`, `\"18\xB0C\"`). |\n| delta | `string` | - | Optional change indicator, pre-formatted, e.g. `\"+12.5%\"` or `\"-3\"`. |\n| trend | `'up' \\| 'down' \\| 'neutral'` | `'neutral'` | Direction of `delta` \u2014 colours it: `'up'` success, `'down'` error, `'neutral'` muted. |\n| icon | `string \\| ReactNode` | - | Optional Lucide icon name (kebab-case) or custom node, shown next to the label. |\n\n**Example:**\n```tsx\n<Stat label=\"Revenue\" value=\"$48.2k\" delta=\"+12.5%\" trend=\"up\" icon=\"trending-up\" />\n```\n\n### Stepper\n\nStepper -- a display-only step indicator for multi-step flows\n(wizards, checkouts, onboarding): numbered markers joined by\nconnector lines, with completed (check), current (filled), and\nupcoming (outlined) states styled distinctly.\n\nStepper renders step state; it never owns it. Declare the step\nlabels as a top-level `const`, track the current index in\n`useState`, and advance it from your own Next/Back buttons (or\n`onStepClick`).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| steps | `string[]` | - | Ordered step labels, one per step. Declare as a top-level `const` array \u2014 never as state. |\n| current | `number` | - | Zero-based index of the active step. Steps before it render as completed (check mark), steps after it as upcoming. |\n| orientation | `'horizontal' \\| 'vertical'` | `'horizontal'` | Flow direction. `'horizontal'` joins the markers with connector lines in a row; `'vertical'` stacks the steps with short connector columns \u2014 better for sidebars or long step labels. |\n| onStepClick | `(index: number) => void` | - | When provided, each step becomes a real button and clicks receive the step's zero-based index. Typical use: jumping back to a completed step; guard forward jumps in the handler if the flow validates per step. |\n\n**Example:**\n```tsx\nconst STEPS = ['Account', 'Profile', 'Review'];\n// In the component: const [step, setStep] = useState(0);\n<Stack gap=\"lg\">\n <Stepper steps={STEPS} current={step} />\n {step === 0 && <AccountFields />}\n <Row gap=\"sm\" justify=\"end\">\n <Button variant=\"ghost\" disabled={step === 0} onClick={() => setStep(step - 1)}>Back</Button>\n <Button onClick={() => setStep(step + 1)}>Next</Button>\n </Row>\n</Stack>\n```\n\n### Markdown\n\nMarkdown -- renders markdown-formatted text as styled content:\nheadings, paragraphs, **bold** / *italic*, `inline code`, fenced\ncode blocks, bulleted and numbered lists, pipe tables, and links.\n\nUse it whenever a prop or context slot carries prose the agent\nauthors in markdown -- article bodies, AI-generated explanations,\ndescriptions, notes, help text. Pass the raw markdown string\nstraight through; never pre-parse it, never split it into per-line\n`Text` elements, and never rebuild formatting with nested\n`<strong>`/`<em>` markup yourself.\n\nSafe by construction: raw HTML in the source displays as literal\ntext (never markup), and links render inert unless their scheme is\nhttp(s)/mailto. Streaming-tolerant: a mid-stream prefix parses\ncleanly and completed constructs never change shape, so binding a\ngrowing string re-renders correctly.\n\nFor a single line of formatted text INSIDE another element (a table\ncell, a list row, a caption), use `MarkdownInline` instead -- same\n`markdown` prop, inline-level constructs only (no headings, lists,\nor code fences), renders inside the parent's typography.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| markdown | `string` | - | Markdown source text, passed through verbatim. Supports the conversational subset: `#`-`###` headings, paragraphs, bold, italic, inline code, fenced code blocks, `-`/`1.` lists, `\\|` pipe tables, and `[label](url)` links. Bind stateful or streamed prose directly. |\n\n**Example:**\n```tsx\n<Stack gap=\"md\">\n <Markdown markdown={recipe.instructions} />\n</Stack>\n```\n\n### MarkdownInline\n\nMarkdownInline -- renders ONE line of inline-level markdown (bold,\nitalic, inline code, links) inside the parent's typography -- a table\ncell, a list row, a caption, a summary line. Same `markdown` prop as\n`Markdown`; block constructs (headings, lists, code fences) are not\nrendered -- reach for `Markdown` for multi-paragraph bodies.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| markdown | `string` | - | Inline markdown source -- bold, italic, inline code, and `[label](url)` links. Block-level syntax renders as plain text. |\n\n**Example:**\n```tsx\n<Row gap=\"sm\">\n <Badge variant=\"success\">stable</Badge>\n <MarkdownInline markdown={release.summary} />\n</Row>\n```\n\n### Support Types\n\n**DropdownOption:**\n\nA single option inside a `Dropdown`. Each option maps to one `MenuItem`\ninternally.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| value | `string` | Unique identifier returned to `onChange` when this option is selected. |\n| label | `string` | Human-readable text shown in the menu row. |\n| icon | `ReactNode` | Optional icon rendered to the left of the label via `MenuItem.icon`. |\n| disabled | `boolean` | When `true`, the option is visible but non-interactive. |\n| danger | `boolean` | When `true`, the option uses destructive (red) styling via `MenuItem.danger`. |\n\n**AutocompleteOption:**\n\nA single option in the `Autocomplete` suggestion list.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| value | `string` | Unique identifier for this option. Also used for case-insensitive filtering against the input value. |\n| label | `string` | Primary display text. Also used for case-insensitive filtering and is written into the input on selection. |\n| description | `string` | Secondary description rendered below the label in `fontSize.xs` / `colors.gray[500]`. |\n| icon | `ReactNode` | Icon rendered to the left of the label/description block. Flex-shrink 0. |\n| disabled | `boolean` | When `true`, the option is visible but non-interactive (`cursor: not-allowed`, `colors.gray[400]` text). |\n\n**BreadcrumbItem:**\n\nA single segment in a `Breadcrumb` trail. Items with `href` render as\n`Link` primitives; items without render as plain `<button>` elements.\nThe last item in the array is always rendered as static text with\n`aria-current=\"page\"`.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| label | `string` | Display text for this breadcrumb segment. |\n| href | `string` | URL for this segment. When provided (and this is not the last item), the segment renders as a `Link` primitive with `underline=\"hover\"`. When omitted, it renders as a `<button>`. |\n| icon | `ReactNode` | Icon rendered immediately before the label. Its color follows the segment's text color: `colors.gray[500]` for navigable items, `colors.gray[900]` for the current (last) item. |\n\n\n## Compositions\n\nImport: `import { Component } from '@ggui-ai/design'`\n\n### Header\n\nProps for the `Header` composition.\n\nA horizontal page header that arranges a logo, navigation, and action slots\nin a flex row (`justify-content: space-between`). Internally renders a `<header>` element;\ndoes not compose other ggui primitives.\n\nWhen `sticky` is true the header gets `position: sticky; top: 0` with `zIndex.sticky`\nand a `shadow.sm` box-shadow.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| logo | `ReactNode` | - | Logo or brand element rendered at the start (flex-shrink: 0). |\n| navigation | `ReactNode` | - | Navigation content rendered in a `<nav>` element with `flex: 1` and a 32 px left margin. |\n| actions | `ReactNode` | - | Right-side action elements (buttons, avatar, etc.) rendered with a 12 px gap. |\n| sticky | `boolean` | `false` | When true, the header becomes `position: sticky` at the top of its scroll container with `zIndex.sticky` and `shadow.sm`. |\n| background | `string` | `colors.white` | Background color of the header. |\n| bordered | `boolean` | `true` | When true, renders a 1 px bottom border in `colors.gray[200]`. |\n\n**Example:**\n```tsx\n```tsx\n<Header\n logo={<img src=\"/logo.svg\" alt=\"Acme\" />}\n navigation={<a href=\"/docs\">Docs</a>}\n actions={<Button size=\"sm\">Sign In</Button>}\n sticky\n bordered\n/>\n```\n```\n\n### Sidebar\n\nProps for the `Sidebar` composition.\n\nA vertical navigation panel that composes the `Icon` primitive for chevron indicators.\nItems are rendered as `<button>` elements inside a scrollable `<nav>`. Nested items\nare indented 16 px per depth level. The sidebar animates width changes with a 200 ms\nCSS transition. Active items are highlighted with `colors.primary[50]` background\nand `colors.primary[700]` text.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| items | `SidebarItem[]` | - | Array of navigation items to render. |\n| activeId | `string` | - | ID of the currently active item. Matched items get a highlighted background and bold text. |\n| onItemClick | `(item: SidebarItem) => void` | - | Called when any item (including parent items with children) is clicked. |\n| collapsed | `boolean` | `false` | When true, hides labels and badges; only icons remain visible, centered in the collapsed width. Nested children are hidden entirely. |\n| header | `ReactNode` | - | Content rendered above the item list, separated by a bottom border. |\n| footer | `ReactNode` | - | Content rendered below the item list, separated by a top border. |\n| width | `number` | `256` | Width in pixels when expanded. |\n| collapsedWidth | `number` | `64` | Width in pixels when collapsed. |\n\n**Example:**\n```tsx\n```tsx\n<Sidebar\n items={[\n { id: 'home', label: 'Home', icon: <Icon name=\"home\" /> },\n { id: 'settings', label: 'Settings', icon: <Icon name=\"settings\" />,\n children: [\n { id: 'profile', label: 'Profile' },\n { id: 'billing', label: 'Billing' },\n ]},\n ]}\n activeId=\"home\"\n onItemClick={(item) => navigate(item.href)}\n collapsed={false}\n width={256}\n/>\n```\n```\n\n### CardGrid\n\nProps for the `CardGrid` composition.\n\nA CSS Grid wrapper that arranges children in equal-width columns. When `columns`\nis a number, it produces `repeat(N, 1fr)`. When it is a responsive object,\nit falls back to `repeat(auto-fit, minmax(280px, 1fr))` for fluid responsive behavior.\n\nDoes not compose any ggui primitives internally \u2014 it is a pure layout container.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| children | `ReactNode` | - | Card elements to arrange in the grid. |\n| columns | `number \\| { sm?: number; md?: number; lg?: number }` | `3` | Number of columns, or a responsive breakpoint map. - `number` \u2014 fixed column count via `repeat(N, 1fr)`. - `{ sm?, md?, lg? }` \u2014 triggers `repeat(auto-fit, minmax(280px, 1fr))` for fluid layout. |\n| gap | `number` | `16` | Gap between grid items in pixels. |\n\n**Example:**\n```tsx\n```tsx\n<CardGrid columns={3} gap={24}>\n <Card>A</Card>\n <Card>B</Card>\n <Card>C</Card>\n</CardGrid>\n```\n```\n\n### CommentThread\n\nProps for the `CommentThread` composition.\n\nA threaded comment section that composes `Avatar`, `Button`, `TextArea`, and `Spinner`\nprimitives. Comments are rendered recursively with 40 px indentation per nesting level.\nEach comment shows author avatar, name, timestamp, content, reactions, and a \"Reply\" toggle.\nWhen `currentUser` is provided, a new-comment input with avatar and submit button is shown\nabove the thread.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| comments | `Comment[]` | - | Array of top-level comments to render. Replies are nested within each comment. |\n| currentUser | `{ /** Display name for the current user's avatar. */ name: string; /** Avatar image URL for the current user. */ avatar?: string; }` | - | Current user info. When provided, a new-comment input area is rendered above the thread. |\n| onAddComment | `(content: string, parentId?: string) => void` | - | Called when the user submits a new top-level comment. |\n| onReply | `(commentId: string, content: string) => void` | - | Called when the user submits a reply to an existing comment. |\n| onReaction | `(commentId: string, emoji: string) => void` | - | Called when the user clicks an emoji reaction on a comment. |\n| loading | `boolean` | - | When true, shows a centered `Spinner` instead of the comment list. |\n\n**Example:**\n```tsx\n```tsx\n<CommentThread\n comments={[\n { id: '1', author: { name: 'Alice' }, content: 'Great work!', timestamp: new Date(),\n replies: [{ id: '2', author: { name: 'Bob' }, content: 'Thanks!', timestamp: new Date() }] }\n ]}\n currentUser={{ name: 'Alice', avatar: '/alice.jpg' }}\n onAddComment={(content) => post(content)}\n onReply={(commentId, content) => reply(commentId, content)}\n onReaction={(commentId, emoji) => react(commentId, emoji)}\n/>\n```\n```\n\n### DataTable\n\nProps for the `DataTable` composition.\n\nA sortable, selectable data table that composes `Checkbox`, `Spinner`, and `Icon`\nprimitives. Renders a `<table>` inside a bordered container with 8 px border-radius.\nThe header row has a `colors.gray[50]` background. Sortable columns show a chevron\nicon on click (toggles asc/desc). Selected rows are highlighted with `colors.primary[50]`.\nRow background transitions use a 150 ms ease. The \"select all\" checkbox supports an\nindeterminate state when a subset of rows is selected.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| columns | `DataTableColumn<T>[]` | - | Column definitions that control header, alignment, sorting, and rendering. |\n| data | `T[]` | - | Row data array. Each entry corresponds to one table row. |\n| rowKey | `string \\| ((row: T) => string)` | `'id'` | Property name or function used to derive a unique key for each row. |\n| loading | `boolean` | - | When true, shows a centered `Spinner` instead of table body rows. |\n| emptyText | `string` | `'No data'` | Text shown when `data` is empty and not loading. |\n| onSort | `(key: string, direction: 'asc' \\| 'desc') => void` | - | Called when a sortable column header is clicked. Toggles direction automatically (asc -> desc) if the same column is clicked again. |\n| sortKey | `string` | - | The column key currently being sorted. |\n| sortDirection | `'asc' \\| 'desc'` | - | The current sort direction. |\n| onRowClick | `(row: T, index: number) => void` | - | Called when a row is clicked. Rows get `cursor: pointer` when this handler is provided. |\n| selectable | `boolean` | - | When true, adds a checkbox column at the start of each row. |\n| selectedKeys | `string[]` | `[]` | Array of currently selected row keys. |\n| onSelectionChange | `(keys: string[]) => void` | - | Called when the set of selected row keys changes (via row checkbox or select-all). |\n\n**Example:**\n```tsx\n```tsx\n<DataTable\n columns={[\n { key: 'name', header: 'Name', sortable: true },\n { key: 'email', header: 'Email' },\n { key: 'role', header: 'Role', render: (v) => <Badge>{v}</Badge> },\n ]}\n data={users}\n rowKey=\"id\"\n selectable\n selectedKeys={selected}\n onSelectionChange={setSelected}\n onSort={(key, dir) => sort(key, dir)}\n sortKey=\"name\"\n sortDirection=\"asc\"\n/>\n```\n```\n\n### ChatWindow\n\nProps for the `ChatWindow` composition.\n\nA messaging interface that composes `Avatar`, `Button`, `Spinner`, and `Icon` primitives.\nLayout is a flex column filling 100% height with a bordered `radius.lg` container.\nMessages from the current user align right with `colors.primary[600]` bubbles and white text.\nOther users' messages align left with `colors.gray[100]` bubbles. The message area\nauto-scrolls to the bottom on new messages via `scrollIntoView({ behavior: 'smooth' })`.\nThe text input sends on Enter (Shift+Enter for newline).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| messages | `ChatMessage[]` | - | Array of chat messages to display in chronological order. |\n| currentUserId | `string` | - | ID of the current user. Messages from this user render right-aligned with a primary color bubble. |\n| onSendMessage | `(content: string) => void` | - | Called when the user submits a message (Enter key or send button). |\n| loading | `boolean` | - | When true, shows a centered `Spinner` instead of the message list. |\n| typing | `{ name: string } \\| null` | - | When non-null, displays a typing indicator below the last message (e.g., \"Alice is typing...\"). |\n| placeholder | `string` | `'Type a message...'` | Placeholder text for the message input field. |\n| header | `ReactNode` | - | Optional header content rendered above the message area, separated by a bottom border. |\n\n**Example:**\n```tsx\n```tsx\n<ChatWindow\n messages={messages}\n currentUserId=\"user-1\"\n onSendMessage={(content) => send(content)}\n typing={{ name: 'Alice' }}\n placeholder=\"Type a message...\"\n header={<h3>Chat with Alice</h3>}\n/>\n```\n```\n\n### NavigationBar\n\nProps for the `NavigationBar` composition.\n\nA horizontal or vertical navigation menu. Does not compose other ggui primitives\n(uses plain `<button>` and `<a>` elements). Active items are styled per variant:\n\n- `'default'` \u2014 active item gets `colors.primary[600]` text and medium font weight.\n- `'pills'` \u2014 active item gets a `radius.full` pill with `colors.primary[100]` background.\n- `'underline'` \u2014 active item gets a 2 px bottom border in `colors.primary[600]`.\n When horizontal, the entire nav also has a 1 px bottom border.\n\nAll items have a 150 ms transition on all properties.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| items | `NavItem[]` | - | Array of navigation items to render. |\n| activeId | `string` | - | ID of the currently active item. Controls visual highlighting per variant style. |\n| onItemClick | `(item: NavItem) => void` | - | Called when a non-disabled item is clicked. For `<a>` elements, `preventDefault` is called first. |\n| orientation | `'horizontal' \\| 'vertical'` | `'horizontal'` | Layout direction. - `'horizontal'` \u2014 flex-row with 4 px gap. - `'vertical'` \u2014 flex-column with 2 px gap. |\n| variant | `'default' \\| 'pills' \\| 'underline'` | `'default'` | Visual style for active/inactive items. - `'default'` \u2014 text color change only. - `'pills'` \u2014 rounded pill background on active items. - `'underline'` \u2014 bottom border on active items. |\n\n**Example:**\n```tsx\n```tsx\n<NavigationBar\n items={[\n { id: 'home', label: 'Home', icon: <Icon name=\"home\" /> },\n { id: 'about', label: 'About' },\n { id: 'contact', label: 'Contact' },\n ]}\n activeId=\"home\"\n onItemClick={(item) => navigate(item.id)}\n orientation=\"horizontal\"\n variant=\"pills\"\n/>\n```\n```\n\n### FileUploader\n\nProps for the `FileUploader` composition.\n\nA drag-and-drop file upload area that composes `Button`, `Progress`, and `Icon` primitives.\nThe drop zone is a dashed-border container that highlights in `colors.primary[400]`/`colors.primary[50]`\non drag-over. Below the drop zone, each file in `files` is listed with its name, size,\noptional progress bar (for uploading status), and a remove button.\n\nFile validation (max size, max count) is applied client-side before calling `onFilesSelected`.\nFiles exceeding `maxSize` are silently filtered out; files exceeding `maxFiles` are truncated.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| files | `UploadedFile[]` | `[]` | Array of files currently in the upload queue, shown below the drop zone. |\n| onFilesSelected | `(files: File[]) => void` | - | Called when the user selects files via click or drag-and-drop. Receives native `File` objects after client-side filtering (maxSize, maxFiles). |\n| onFileRemove | `(fileId: string) => void` | - | Called when the user clicks the remove button on a file entry. |\n| accept | `string` | - | Accepted file types passed to the hidden `<input type=\"file\" accept=\"...\">`. E.g., `'image/*,.pdf'`. |\n| multiple | `boolean` | `true` | When true, allows selecting multiple files at once. |\n| maxSize | `number` | - | Maximum file size in bytes. Files exceeding this are silently excluded from the selection. |\n| maxFiles | `number` | - | Maximum number of files. Excess files beyond `maxFiles - files.length` are truncated. |\n| disabled | `boolean` | - | When true, the drop zone is visually dimmed (opacity 0.5) and non-interactive. |\n| dragDrop | `boolean` | `true` | When true, enables drag-and-drop on the drop zone. When false, the prompt text changes to \"Click to browse files\". |\n\n**Example:**\n```tsx\n```tsx\n<FileUploader\n files={uploadedFiles}\n onFilesSelected={(files) => startUpload(files)}\n onFileRemove={(id) => removeFile(id)}\n accept=\"image/*,.pdf\"\n multiple\n maxSize={5 * 1024 * 1024}\n maxFiles={10}\n dragDrop\n/>\n```\n```\n\n### NotificationCenter\n\nProps for the `NotificationCenter` composition.\n\nA notification list with a header bar that composes `Button`, `Spinner`, and `Icon`\nprimitives. The header shows a \"Notifications\" title, an unread count badge\n(pill in `colors.primary[100]`), and \"Mark all read\" / \"Clear all\" buttons.\nEach notification item shows a colored status dot, title, message, timestamp,\n\"Mark as read\" link, and optional action. The list is scrollable (`overflowY: auto`).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| notifications | `Notification[]` | - | Array of notifications to display, in the order provided. |\n| onMarkAsRead | `(id: string) => void` | - | Called when the user clicks \"Mark as read\" on an individual notification. |\n| onMarkAllAsRead | `() => void` | - | Called when the user clicks the \"Mark all read\" header button. Only shown when there are unread items. |\n| onDismiss | `(id: string) => void` | - | Called when the user clicks the dismiss (X) button on an individual notification. |\n| onClearAll | `() => void` | - | Called when the user clicks the \"Clear all\" header button. Only shown when there are any notifications. |\n| emptyText | `string` | `'No notifications'` | Text shown when `notifications` is empty and not loading. |\n| loading | `boolean` | - | When true, shows a centered `Spinner` instead of the notification list. |\n\n**Example:**\n```tsx\n```tsx\n<NotificationCenter\n notifications={[\n { id: '1', title: 'Deployment complete', type: 'success', timestamp: new Date(), read: false },\n { id: '2', title: 'Build failed', type: 'error', message: 'Lint errors', timestamp: new Date() },\n ]}\n onMarkAsRead={(id) => markRead(id)}\n onMarkAllAsRead={() => markAllRead()}\n onDismiss={(id) => dismiss(id)}\n onClearAll={() => clearAll()}\n/>\n```\n```\n\n### Modal\n\nProps for the `Modal` composition.\n\nA dialog overlay that composes `Button`, `Icon`, and `Heading` primitives.\nThe overlay uses `animation.fadeIn` (`ggui-fadeIn`) and the dialog panel uses\n`animation.scaleIn` (`ggui-scaleIn`) \u2014 both GPU-composited (opacity + transform).\nWhen open, `document.body.style.overflow` is set to `'hidden'` to prevent background\nscrolling, and restored on close. The dialog has `role=\"dialog\"` and `aria-modal=\"true\"`.\n\nSize widths: `sm` = 400 px, `md` = 500 px, `lg` = 640 px, `xl` = 800 px,\n`full` = 100vw (no border-radius, no padding).\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| open | `boolean` | - | Controls modal visibility. When false, the component renders nothing. |\n| onClose | `() => void` | - | Called to close the modal (overlay click, escape key, or close button). |\n| title | `string` | - | Optional title rendered in the modal header via the `Heading` primitive (level 4). |\n| children | `ReactNode` | - | Modal body content rendered in a scrollable area (`overflowY: auto`). |\n| footer | `ReactNode` | - | Footer content rendered below the body, right-aligned with an 8 px gap, separated by a top border. |\n| size | `'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| 'full'` | `'md'` | Controls the width of the modal panel. - `'sm'` \u2014 400 px - `'md'` \u2014 500 px - `'lg'` \u2014 640 px - `'xl'` \u2014 800 px - `'full'` \u2014 100vw, no border-radius, stretches to fill viewport |\n| closeOnOverlayClick | `boolean` | `true` | When true, clicking the semi-transparent overlay behind the modal calls `onClose`. |\n| closeOnEscape | `boolean` | `true` | When true, pressing the Escape key calls `onClose`. |\n| showCloseButton | `boolean` | `true` | When true, renders a ghost close button (X icon) in the modal header. |\n\n**Example:**\n```tsx\n```tsx\n<Modal\n open={isOpen}\n onClose={() => setOpen(false)}\n title=\"Confirm Action\"\n size=\"md\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={() => setOpen(false)}>Cancel</Button>\n <Button onClick={handleConfirm}>Confirm</Button>\n </>\n }\n>\n <p>Are you sure you want to proceed?</p>\n</Modal>\n```\n```\n\n### CommandPalette\n\nProps for the `CommandPalette` composition.\n\nA searchable command menu (Cmd+K / Ctrl+K pattern) that composes `Spinner` and `Icon`\nprimitives. Appears as a centered overlay at 15vh from the top. Commands are filtered\nby label and description (case-insensitive substring match). Results are grouped under\nuppercase section headers. Keyboard navigation is fully supported:\nArrow Up/Down to navigate, Enter to select, Escape to close.\n\nWhen `recentIds` are provided and the search query is empty, matching commands appear\nin a \"Recent\" section at the top (deduplicated from their original groups).\n\nThe footer shows navigation hints: \"Up/Down Navigate\", \"Enter Select\", \"Esc Close\".\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| open | `boolean` | - | Controls palette visibility. When false, the component renders nothing. |\n| onClose | `() => void` | - | Called to close the palette (overlay click, Escape key, or after command selection). |\n| commands | `Command[]` | - | Full array of available commands. Filtered client-side by the search query. |\n| onSelect | `(command: Command) => void` | - | Called when a non-disabled command is selected (Enter key or click). The palette auto-closes after selection. |\n| placeholder | `string` | `'Search commands...'` | Placeholder text for the search input. |\n| recentIds | `string[]` | `[]` | IDs of recently used commands. When the query is empty, these appear in a \"Recent\" section at the top of the results. |\n| loading | `boolean` | - | When true, shows a centered `Spinner` instead of the command list. |\n\n**Example:**\n```tsx\n```tsx\n<CommandPalette\n open={isOpen}\n onClose={() => setOpen(false)}\n commands={[\n { id: 'new', label: 'New File', shortcut: 'Ctrl+N', group: 'File' },\n { id: 'save', label: 'Save', shortcut: 'Ctrl+S', group: 'File' },\n { id: 'theme', label: 'Toggle Theme', group: 'Preferences' },\n ]}\n onSelect={(cmd) => executeCommand(cmd.id)}\n recentIds={['save']}\n placeholder=\"Search commands...\"\n/>\n```\n```\n\n### Footer\n\nProps for the `Footer` composition.\n\nA site footer with `role=\"contentinfo\"` that lays out a brand slot, link columns,\nsocial icons, and a bottom bar. Does not compose other ggui primitives (uses plain\nHTML elements). Content is constrained to `max-width: 1280px` with auto margins.\nLink columns use a responsive flex layout (`flex: 0 1 180px`). The bottom bar\nincludes copyright text, social links, and bottom-bar links separated by a top border.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| brand | `ReactNode` | - | Brand element (logo, tagline) rendered in a flexible column (`flex: 1 1 280px`). |\n| columns | `FooterColumn[]` | - | Array of link columns rendered in a flex-wrap layout with 48 px gap. |\n| socialLinks | `FooterSocialLink[]` | - | Social media icon links rendered in the bottom bar. |\n| bottomText | `string` | - | Text displayed at the start of the bottom bar (e.g., copyright notice). |\n| bottomLinks | `FooterLink[]` | - | Links displayed in the bottom bar after social icons (e.g., Privacy, Terms). |\n| background | `string` | `colors.gray[50]` | Background color of the footer. |\n| bordered | `boolean` | `true` | When true, renders a 1 px top border in `colors.gray[200]`. |\n\n**Example:**\n```tsx\n```tsx\n<Footer\n brand={<img src=\"/logo.svg\" alt=\"Acme\" />}\n columns={[\n { title: 'Product', links: [{ label: 'Features', href: '/features' }] },\n { title: 'Company', links: [{ label: 'About', href: '/about' }] },\n ]}\n socialLinks={[\n { label: 'Twitter', href: 'https://twitter.com/acme', icon: <TwitterIcon /> },\n ]}\n bottomText=\"© 2026 Acme Inc.\"\n bottomLinks={[{ label: 'Privacy', href: '/privacy' }]}\n bordered\n/>\n```\n```\n\n### Hero\n\nProps for the `Hero` composition.\n\nA prominent landing-page hero section that renders heading, description, CTA buttons,\nand an optional media slot. Does not compose other ggui primitives (uses plain HTML\nelements styled with design tokens). Content is constrained to `max-width: 1280px`.\n\nLayout modes:\n- `align='center'` \u2014 single-column centered layout with `max-width: 800px` text area.\n- `align='left'` \u2014 two-column side-by-side layout (50/50 split with media slot).\n\nSize controls vertical padding and font sizes:\n- `'sm'` \u2014 48 px vertical padding, 3xl heading, lg description.\n- `'md'` \u2014 80 px vertical padding, 4xl heading, xl description.\n- `'lg'` \u2014 120 px vertical padding, 5xl heading, xl description.\n\nThe primary action button uses `colors.primary[600]` fill; the secondary action uses\nan outlined style. When `overlay` is true with a `backgroundImage`, text switches to white\nand borders become semi-transparent.\n\n**Props:**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| heading | `string` | - | Main heading text rendered as an `<h1>` with bold weight and tight line-height. |\n| description | `string` | - | Description paragraph rendered below the heading with relaxed line-height. |\n| primaryAction | `HeroAction` | - | Primary CTA button rendered with `colors.primary[600]` background and white text. |\n| secondaryAction | `HeroAction` | - | Secondary CTA button rendered with a transparent background and a 1 px border. |\n| media | `ReactNode` | - | Media element (image, video, illustration) rendered beside or below the text content. |\n| align | `'center' \\| 'left'` | `'center'` | Text and layout alignment. - `'center'` \u2014 centered single-column layout. - `'left'` \u2014 left-aligned text with media in a right column (50/50 split). |\n| size | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Controls vertical padding and heading/description font sizes. - `'sm'` \u2014 compact (48 px padding, 3xl/lg fonts). - `'md'` \u2014 standard (80 px padding, 4xl/xl fonts). - `'lg'` \u2014 spacious (120 px padding, 5xl/xl fonts). |\n| background | `string` | `colors.white (when no backgroundImage is set)` | Background color of the hero section. |\n| backgroundImage | `string` | - | Background image URL applied as `background-size: cover; background-position: center`. |\n| overlay | `boolean` | `false` | When true and `backgroundImage` is set, renders a semi-transparent black overlay (`rgba(0,0,0,0.5)`) and switches text to white/semi-transparent white for contrast. |\n\n**Example:**\n```tsx\n```tsx\n<Hero\n heading=\"Build Better UIs, Faster\"\n description=\"The universal interface layer between AI agents and humans.\"\n primaryAction={{ label: 'Get Started', onClick: () => navigate('/signup') }}\n secondaryAction={{ label: 'Learn More', href: '/docs' }}\n media={<img src=\"/hero.png\" alt=\"Hero\" />}\n align=\"left\"\n size=\"lg\"\n/>\n```\n```\n\n### Support Types\n\n**SidebarItem:**\n\nA single navigation entry in a `Sidebar`. Supports nested children for\ncollapsible sub-menus and an optional badge slot.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier used for active-state matching and React keys. |\n| label | `string` | Display label for the item. Hidden when the sidebar is collapsed. |\n| icon | `ReactNode` | Leading icon rendered before the label. Remains visible when collapsed. |\n| href | `string` | Optional URL associated with this item (not rendered as a link by default). |\n| badge | `ReactNode` | Trailing badge element (e.g., unread count). Hidden when collapsed. |\n| children | `SidebarItem[]` | Nested child items. When present, the item acts as a collapsible section (chevron indicator shown). |\n| disabled | `boolean` | When true, the item is visually dimmed and non-interactive (`cursor: not-allowed`). |\n\n**Comment:**\n\nA single comment entry in a `CommentThread`. Supports nested replies\nand emoji reactions with counts.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier for this comment. |\n| author | `{ /** Display name of the author. */ name: string; /** URL for the author's avatar image. Passed to the `Avatar` primitive. */ avatar?: string; }` | Comment author metadata. |\n| content | `string` | The comment body text. |\n| timestamp | `string \\| Date` | Timestamp of the comment. Rendered via `toLocaleString()` when a `Date` object. |\n| replies | `Comment[]` | Nested reply comments. Each reply is rendered indented 40 px deeper. |\n| reactions | `{ emoji: string; count: number }[]` | Emoji reactions with their aggregated counts (e.g., `{ emoji: \"\u{1F44D}\", count: 3 }`). |\n\n**DataTableColumn:**\n\nColumn definition for a `DataTable`. Controls header text, width, alignment,\nsorting, and custom cell rendering.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| key | `string` | Property key on the row object used to extract cell values. Also serves as the sort key. |\n| header | `string` | Column header text displayed in the `<thead>`. |\n| width | `number \\| string` | Column width as a CSS value (number for pixels, string for any CSS unit). |\n| sortable | `boolean` | When true, the column header is clickable and triggers `onSort`. An arrow icon indicates direction. |\n| render | `(value: unknown, row: T, index: number) => ReactNode` | Custom cell renderer. When omitted, the raw value is stringified via `String()`. |\n| align | `'left' \\| 'center' \\| 'right'` | Text alignment for both header and body cells. |\n\n**ChatMessage:**\n\nA single message in a `ChatWindow`. Includes sender metadata, delivery status,\nand a timestamp.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier for this message. |\n| content | `string` | The message body text. |\n| sender | `{ /** Unique ID of the sender. Compared to `currentUserId` to determine alignment. */ id: string; /** Display name of the sender. */ name: string; /** Avatar image URL. Only shown for non-current-user messages (xs size). */ avatar?: string; }` | Sender metadata used for avatar rendering and alignment. |\n| timestamp | `string \\| Date` | Message timestamp. Rendered as `HH:MM` via `toLocaleTimeString` when a `Date` object. |\n| status | `'sending' \\| 'sent' \\| 'delivered' \\| 'read' \\| 'error'` | Delivery status indicator shown on the current user's messages. - `'sending'` \u2014 shows a dot bullet - `'sent'` \u2014 shows a single checkmark - `'delivered'` \u2014 shows double checkmarks - `'read'` \u2014 shows double checkmarks (same visual as delivered) - `'error'` \u2014 shows an exclamation mark |\n\n**NavItem:**\n\nA single navigation entry in a `NavigationBar`. Supports nested children\nfor sub-menus (rendered by the parent via dropdown, not built-in).\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier used for active-state matching and React keys. |\n| label | `string` | Display label for the navigation link. |\n| href | `string` | URL for the item. When provided, renders an `<a>` element instead of `<button>`. |\n| icon | `ReactNode` | Optional icon rendered before the label. |\n| children | `NavItem[]` | Nested child items (for sub-menu structures; rendering is consumer-defined). |\n| disabled | `boolean` | When true, the item is visually dimmed (opacity 0.5) and non-interactive. |\n\n**UploadedFile:**\n\nA file entry in the `FileUploader` composition. Tracks upload progress,\nstatus, and optional error messages.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier for this file entry. Used as a key and for removal callbacks. |\n| name | `string` | Original file name. Rendered with text-overflow ellipsis when too long. |\n| size | `number` | File size in bytes. Formatted as B/KB/MB/GB for display. |\n| type | `string` | MIME type of the file (e.g., `'image/png'`). |\n| progress | `number` | Upload progress as a percentage (0-100). Shown via the `Progress` primitive when status is `'uploading'`. |\n| status | `'pending' \\| 'uploading' \\| 'success' \\| 'error'` | Current upload lifecycle status. - `'pending'` \u2014 file selected but upload not started. - `'uploading'` \u2014 upload in progress; `progress` bar is shown. - `'success'` \u2014 upload completed. - `'error'` \u2014 upload failed; `error` message is shown in `colors.error[500]`. |\n| error | `string` | Error message displayed when status is `'error'`. |\n| url | `string` | The remote URL of the uploaded file after successful upload. |\n\n**Notification:**\n\nA single notification entry in the `NotificationCenter`. Supports semantic type coloring,\nread state, and an optional inline action button.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier for this notification. |\n| title | `string` | Notification title rendered in medium weight. |\n| message | `string` | Optional body text rendered below the title in a smaller font. |\n| timestamp | `string \\| Date` | Timestamp of the notification. Rendered via `toLocaleString()` when a `Date` object. |\n| read | `boolean` | Read state. Unread notifications get a `colors.primary[50]` background and a colored status dot matching the notification type. |\n| type | `'info' \\| 'success' \\| 'warning' \\| 'error'` | Semantic type that controls the status dot color on unread notifications. - `'info'` \u2014 `colors.info[500]` (blue) - `'success'` \u2014 `colors.success[500]` (green) - `'warning'` \u2014 `colors.warning[500]` (amber) - `'error'` \u2014 `colors.error[500]` (red) |\n| icon | `ReactNode` | Optional icon rendered alongside the notification (not used by the default implementation). |\n| action | `{ /** Button label text. */ label: string; /** Click handler for the action. */ onClick: () => void; }` | Optional inline action button rendered next to the timestamp. |\n\n**Command:**\n\nA single command entry in a `CommandPalette`. Commands can be grouped,\nhave keyboard shortcuts, and support a disabled state.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| id | `string` | Unique identifier for this command. |\n| label | `string` | Display label for the command. Searchable by the palette's query filter. |\n| description | `string` | Optional description text shown below the label. Also searchable. |\n| icon | `ReactNode` | Icon rendered at the start of the command row. |\n| shortcut | `string` | Keyboard shortcut hint displayed at the end of the row in a `<kbd>` element. |\n| group | `string` | Group name for visual sectioning. Commands with the same group are rendered under a shared header. Defaults to `'Commands'` if omitted. |\n| disabled | `boolean` | When true, the command is visually dimmed and cannot be selected. |\n\n**FooterLink:**\n\nA single link entry in a footer column or the bottom bar. Supports both\n`href` navigation and `onClick` handlers (onClick takes precedence via `preventDefault`).\n\n| Property | Type | Description |\n|----------|------|-------------|\n| label | `string` | Display text for the link. |\n| href | `string` | URL for the link. |\n| onClick | `() => void` | Click handler. When provided, `preventDefault` is called on the anchor click. |\n\n**FooterColumn:**\n\nA named column of links in the `Footer` layout. Each column has an optional title\nand a list of links rendered as a vertical stack with 10 px gap.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| title | `string` | Column heading rendered as an `<h4>` with semibold weight. |\n| links | `FooterLink[]` | Links displayed in this column. |\n\n**FooterSocialLink:**\n\nA social media link in the `Footer` bottom bar. Rendered as an icon-only anchor\nwith `aria-label` for accessibility.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| label | `string` | Accessible label for the social link (used as `aria-label`). |\n| href | `string` | URL for the social media profile or page. |\n| icon | `ReactNode` | Icon element rendered inside the anchor (typically an SVG or `Icon` primitive). |\n\n**HeroAction:**\n\nA call-to-action button definition for the `Hero` composition.\nUsed for both the primary (filled) and secondary (outlined) action buttons.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| label | `string` | Button label text. |\n| onClick | `() => void` | Click handler for the button. |\n| href | `string` | Optional URL (not used by the default implementation; available for consumer routing). |\n\n\n## System Conventions\n\n### onChange Behavior (CRITICAL)\n\nAll form control onChange handlers receive the VALUE DIRECTLY, not a React event object.\n\n```tsx\n// CORRECT \u2014 onChange receives value directly\n<Input value={name} onChange={setName} />\n<Input value={email} onChange={(value) => setEmail(value)} />\n<Select value={country} onChange={setCountry} options={countries} />\n<Checkbox checked={agreed} onChange={setAgreed} />\n\n// WRONG \u2014 DO NOT use e.target.value!\n<Input value={name} onChange={(e) => setName(e.target.value)} /> // WILL BREAK\n```\n\nApplies to: Input, TextArea, Select, Checkbox, RadioGroup, Slider, Tabs, Accordion.\n\n### Available Motion & Animation\n\nRender `<MotionKeyframes />` once (anywhere in tree) to enable all keyframes.\n\n**Entrance/exit:** fadeIn, fadeOut, slideInUp, slideInDown, scaleIn, scaleOut\n**State feedback:** flash (background-color highlight), pulse (opacity breathing), bounce (scale overshoot)\n**Easing:** linear, easeIn, easeOut, easeInOut, spring (bouncy)\n**Durations:** instant(0ms), fast(100ms), normal(200ms), slow(300ms), slower(500ms)\n\n```tsx\n// Entrance animation on mount\n<div style={{ animation: 'ggui-fadeIn 200ms ease-out' }}>Content</div>\n\n// Stagger list items\n{items.map((item, i) => (\n <div key={item.id} style={{ animation: \\`ggui-slideInUp 300ms ease-out \\${i * 50}ms both\\` }}>\n {item.name}\n </div>\n))}\n\n// Flash highlight when data changes (e.g., stock price update)\n// useAnimationKey returns a key that increments when dep changes \u2192 remounts element \u2192 replays animation\nconst priceKey = useAnimationKey(stock.price);\n<div key={priceKey} style={{\n animation: animation.flash,\n '--ggui-flash-color': stock.change > 0 ? 'var(--ggui-color-success-100)' : 'var(--ggui-color-error-100)',\n} as React.CSSProperties}>\n {stock.price}\n</div>\n\n// Respect reduced-motion preference\nconst { motionEnabled } = useMotion();\n<div style={motionEnabled ? { animation: 'ggui-scaleIn 200ms ease-out' } : undefined}>\n Content\n</div>\n```\n\n### Elevation System\n\n6 levels mapping shadow intensity to z-index for layering:\n- Level 0: flat (no shadow, z: auto) \u2014 inline content\n- Level 1: sm shadow (z: auto) \u2014 cards, sections\n- Level 2: md shadow (z: 1000) \u2014 dropdowns, popovers\n- Level 3: lg shadow (z: 1200) \u2014 sticky banners\n- Level 4: xl shadow (z: 1400) \u2014 modals, dialogs\n- Level 5: 2xl shadow (z: 1800) \u2014 tooltips, toasts\n\n### Import Constraints\n\nOnly these imports are allowed:\n- `react`\n- `@ggui-ai/design` \u2014 the whole design system (primitives, components, compositions, and the `Clickable` / `Hoverable` / `Pressable` traits) is one import; there are no subpaths\n- `@ggui-ai/wire` (wire hooks)\n\nNo external libraries (lodash, date-fns, etc.). No fetch(). No eval().";
|
|
1408
1406
|
VALID_PRIMITIVES = [
|
|
1409
1407
|
"Container",
|
|
1410
1408
|
"Card",
|
|
@@ -1935,53 +1933,53 @@ var init_validation = __esm({
|
|
|
1935
1933
|
}
|
|
1936
1934
|
});
|
|
1937
1935
|
function extractWireCallSites(code) {
|
|
1938
|
-
const sf =
|
|
1936
|
+
const sf = ts4.createSourceFile(
|
|
1939
1937
|
"component.tsx",
|
|
1940
1938
|
code,
|
|
1941
|
-
|
|
1939
|
+
ts4.ScriptTarget.Latest,
|
|
1942
1940
|
/*setParentNodes*/
|
|
1943
1941
|
true,
|
|
1944
|
-
|
|
1942
|
+
ts4.ScriptKind.TSX
|
|
1945
1943
|
);
|
|
1946
1944
|
const sites = [];
|
|
1947
1945
|
function visit(node) {
|
|
1948
|
-
if (
|
|
1946
|
+
if (ts4.isCallExpression(node)) {
|
|
1949
1947
|
const callee = node.expression;
|
|
1950
|
-
if (
|
|
1948
|
+
if (ts4.isIdentifier(callee)) {
|
|
1951
1949
|
const kind = HOOK_TO_KIND[callee.text];
|
|
1952
1950
|
if (kind) {
|
|
1953
1951
|
const firstArg = node.arguments[0];
|
|
1954
|
-
if (firstArg &&
|
|
1952
|
+
if (firstArg && ts4.isStringLiteral(firstArg)) {
|
|
1955
1953
|
sites.push({ kind, name: firstArg.text });
|
|
1956
1954
|
}
|
|
1957
1955
|
}
|
|
1958
1956
|
}
|
|
1959
1957
|
}
|
|
1960
|
-
|
|
1958
|
+
ts4.forEachChild(node, visit);
|
|
1961
1959
|
}
|
|
1962
1960
|
visit(sf);
|
|
1963
1961
|
return sites;
|
|
1964
1962
|
}
|
|
1965
1963
|
function extractWireImports(code) {
|
|
1966
|
-
const sf =
|
|
1964
|
+
const sf = ts4.createSourceFile(
|
|
1967
1965
|
"component.tsx",
|
|
1968
1966
|
code,
|
|
1969
|
-
|
|
1967
|
+
ts4.ScriptTarget.Latest,
|
|
1970
1968
|
/*setParentNodes*/
|
|
1971
1969
|
true,
|
|
1972
|
-
|
|
1970
|
+
ts4.ScriptKind.TSX
|
|
1973
1971
|
);
|
|
1974
1972
|
const imported = /* @__PURE__ */ new Set();
|
|
1975
1973
|
for (const stmt of sf.statements) {
|
|
1976
|
-
if (!
|
|
1974
|
+
if (!ts4.isImportDeclaration(stmt)) continue;
|
|
1977
1975
|
const moduleSpecifier = stmt.moduleSpecifier;
|
|
1978
|
-
if (!
|
|
1976
|
+
if (!ts4.isStringLiteral(moduleSpecifier)) continue;
|
|
1979
1977
|
if (moduleSpecifier.text !== "@ggui-ai/wire") continue;
|
|
1980
1978
|
const clause = stmt.importClause;
|
|
1981
1979
|
if (!clause) continue;
|
|
1982
1980
|
const bindings = clause.namedBindings;
|
|
1983
1981
|
if (!bindings) continue;
|
|
1984
|
-
if (!
|
|
1982
|
+
if (!ts4.isNamedImports(bindings)) continue;
|
|
1985
1983
|
for (const el of bindings.elements) {
|
|
1986
1984
|
imported.add(el.name.text);
|
|
1987
1985
|
}
|
|
@@ -2133,11 +2131,11 @@ var init_json_schema_ts = __esm({
|
|
|
2133
2131
|
}
|
|
2134
2132
|
});
|
|
2135
2133
|
function extractPropsInterface(code) {
|
|
2136
|
-
const sf =
|
|
2134
|
+
const sf = ts4.createSourceFile("component.tsx", code, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
|
|
2137
2135
|
const props = [];
|
|
2138
2136
|
function visitMembers(members) {
|
|
2139
2137
|
for (const member of members) {
|
|
2140
|
-
if (
|
|
2138
|
+
if (ts4.isPropertySignature(member) && member.name) {
|
|
2141
2139
|
const name = member.name.getText(sf);
|
|
2142
2140
|
const type = member.type ? member.type.getText(sf) : "unknown";
|
|
2143
2141
|
const optional = !!member.questionToken;
|
|
@@ -2146,17 +2144,17 @@ function extractPropsInterface(code) {
|
|
|
2146
2144
|
}
|
|
2147
2145
|
}
|
|
2148
2146
|
function visit(node) {
|
|
2149
|
-
if (
|
|
2147
|
+
if (ts4.isInterfaceDeclaration(node) && node.name.text === "Props") {
|
|
2150
2148
|
visitMembers(node.members);
|
|
2151
2149
|
return;
|
|
2152
2150
|
}
|
|
2153
|
-
if (
|
|
2154
|
-
if (
|
|
2151
|
+
if (ts4.isTypeAliasDeclaration(node) && node.name.text === "Props") {
|
|
2152
|
+
if (ts4.isTypeLiteralNode(node.type)) {
|
|
2155
2153
|
visitMembers(node.type.members);
|
|
2156
2154
|
}
|
|
2157
2155
|
return;
|
|
2158
2156
|
}
|
|
2159
|
-
|
|
2157
|
+
ts4.forEachChild(node, visit);
|
|
2160
2158
|
}
|
|
2161
2159
|
visit(sf);
|
|
2162
2160
|
return props.length > 0 ? props : null;
|
|
@@ -2369,12 +2367,12 @@ function generateFix(code, message, sourceLine) {
|
|
|
2369
2367
|
}
|
|
2370
2368
|
}
|
|
2371
2369
|
function parseAndStore(vfs, virtualPath, content) {
|
|
2372
|
-
const sourceFile =
|
|
2370
|
+
const sourceFile = ts4.createSourceFile(
|
|
2373
2371
|
virtualPath,
|
|
2374
2372
|
content,
|
|
2375
|
-
|
|
2373
|
+
ts4.ScriptTarget.ES2020,
|
|
2376
2374
|
true,
|
|
2377
|
-
virtualPath.endsWith(".tsx") ?
|
|
2375
|
+
virtualPath.endsWith(".tsx") ? ts4.ScriptKind.TSX : ts4.ScriptKind.TS
|
|
2378
2376
|
);
|
|
2379
2377
|
vfs.set(virtualPath, { content, sourceFile });
|
|
2380
2378
|
}
|
|
@@ -2516,12 +2514,12 @@ function resolveModuleName(vfs, name, containingFile) {
|
|
|
2516
2514
|
return { resolvedModule: void 0 };
|
|
2517
2515
|
}
|
|
2518
2516
|
function createVfsHost(vfs, componentCode) {
|
|
2519
|
-
const componentFile =
|
|
2517
|
+
const componentFile = ts4.createSourceFile(
|
|
2520
2518
|
"Component.tsx",
|
|
2521
2519
|
componentCode,
|
|
2522
|
-
|
|
2520
|
+
ts4.ScriptTarget.ES2020,
|
|
2523
2521
|
true,
|
|
2524
|
-
|
|
2522
|
+
ts4.ScriptKind.TSX
|
|
2525
2523
|
);
|
|
2526
2524
|
const host = {
|
|
2527
2525
|
getSourceFile(fileName) {
|
|
@@ -2576,7 +2574,7 @@ function resolved(resolvedFileName) {
|
|
|
2576
2574
|
resolvedModule: {
|
|
2577
2575
|
resolvedFileName,
|
|
2578
2576
|
isExternalLibraryImport: true,
|
|
2579
|
-
extension:
|
|
2577
|
+
extension: ts4.Extension.Dts
|
|
2580
2578
|
}
|
|
2581
2579
|
};
|
|
2582
2580
|
}
|
|
@@ -2598,18 +2596,18 @@ async function typecheck(code, dtsMap) {
|
|
|
2598
2596
|
const prefixedCode = SYNTHETIC_PREFIX + code;
|
|
2599
2597
|
const lineOffset = SYNTHETIC_PREFIX.split("\n").length - 1;
|
|
2600
2598
|
const host = createVfsHost(effectiveVfs, prefixedCode);
|
|
2601
|
-
const program =
|
|
2602
|
-
const diagnostics =
|
|
2599
|
+
const program = ts4.createProgram(["Component.tsx"], COMPILER_OPTIONS, host);
|
|
2600
|
+
const diagnostics = ts4.getPreEmitDiagnostics(program);
|
|
2603
2601
|
const errors = [];
|
|
2604
2602
|
const warnings = [];
|
|
2605
2603
|
for (const diag of diagnostics) {
|
|
2606
2604
|
if (diag.file && diag.file.fileName !== "Component.tsx") continue;
|
|
2607
2605
|
if (!diag.file) continue;
|
|
2608
|
-
const diagLine =
|
|
2606
|
+
const diagLine = ts4.getLineAndCharacterOfPosition(diag.file, diag.start ?? 0).line;
|
|
2609
2607
|
if (diagLine < lineOffset) continue;
|
|
2610
|
-
const rawLine = diag.file ?
|
|
2608
|
+
const rawLine = diag.file ? ts4.getLineAndCharacterOfPosition(diag.file, diag.start ?? 0).line + 1 : 0;
|
|
2611
2609
|
const line = Math.max(1, rawLine - lineOffset);
|
|
2612
|
-
const message =
|
|
2610
|
+
const message = ts4.flattenDiagnosticMessageText(diag.messageText, "\n");
|
|
2613
2611
|
const code_ = diag.code;
|
|
2614
2612
|
const sourceLines = diag.file.text.split("\n");
|
|
2615
2613
|
const sourceLine = rawLine >= 1 && rawLine <= sourceLines.length ? sourceLines[rawLine - 1]?.trim() ?? "" : "";
|
|
@@ -2656,8 +2654,8 @@ var init_type_checker = __esm({
|
|
|
2656
2654
|
]);
|
|
2657
2655
|
vfsCache = null;
|
|
2658
2656
|
COMPILER_OPTIONS = {
|
|
2659
|
-
target:
|
|
2660
|
-
module:
|
|
2657
|
+
target: ts4.ScriptTarget.ES2020,
|
|
2658
|
+
module: ts4.ModuleKind.ESNext,
|
|
2661
2659
|
// Classic React JSX mode. The synthetic prefix (SYNTHETIC_PREFIX)
|
|
2662
2660
|
// supplies `import React from 'react'` plus a `declare global` JSX
|
|
2663
2661
|
// namespace shim: `@types/react` v19 removed the *global* `JSX`
|
|
@@ -2665,8 +2663,8 @@ var init_type_checker = __esm({
|
|
|
2665
2663
|
// for `JSX.IntrinsicElements` and the `key`/`ref` carve-out. Without
|
|
2666
2664
|
// the shim, `<div>` degrades to `any` and every typed component
|
|
2667
2665
|
// falsely rejects the intrinsic `key` prop.
|
|
2668
|
-
jsx:
|
|
2669
|
-
moduleResolution:
|
|
2666
|
+
jsx: ts4.JsxEmit.React,
|
|
2667
|
+
moduleResolution: ts4.ModuleResolutionKind.Bundler,
|
|
2670
2668
|
strict: false,
|
|
2671
2669
|
strictNullChecks: true,
|
|
2672
2670
|
// Catch undefined.foo() errors that cause runtime crashes
|
|
@@ -3158,17 +3156,17 @@ Only list ACTUAL composition problems. Use 'fail' only for a fundamentally incoh
|
|
|
3158
3156
|
}
|
|
3159
3157
|
});
|
|
3160
3158
|
function stripComments(code) {
|
|
3161
|
-
const scanner =
|
|
3162
|
-
|
|
3159
|
+
const scanner = ts4.createScanner(
|
|
3160
|
+
ts4.ScriptTarget.Latest,
|
|
3163
3161
|
/* skipTrivia */
|
|
3164
3162
|
false,
|
|
3165
|
-
|
|
3163
|
+
ts4.LanguageVariant.JSX,
|
|
3166
3164
|
code
|
|
3167
3165
|
);
|
|
3168
3166
|
let out = "";
|
|
3169
|
-
for (let token = scanner.scan(); token !==
|
|
3167
|
+
for (let token = scanner.scan(); token !== ts4.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
|
3170
3168
|
const text = scanner.getTokenText();
|
|
3171
|
-
out += token ===
|
|
3169
|
+
out += token === ts4.SyntaxKind.SingleLineCommentTrivia || token === ts4.SyntaxKind.MultiLineCommentTrivia ? text.replace(/[^\n]/g, " ") : text;
|
|
3172
3170
|
}
|
|
3173
3171
|
return out;
|
|
3174
3172
|
}
|
|
@@ -3191,20 +3189,20 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode
|
|
|
3191
3189
|
"Link"
|
|
3192
3190
|
]);
|
|
3193
3191
|
const HANDLER_ATTRS = ["onClick", "onChange", "onPress", "onSelect"];
|
|
3194
|
-
const sf =
|
|
3192
|
+
const sf = ts4.createSourceFile(
|
|
3195
3193
|
"source.tsx",
|
|
3196
3194
|
sourceCode,
|
|
3197
|
-
|
|
3195
|
+
ts4.ScriptTarget.Latest,
|
|
3198
3196
|
/* setParentNodes */
|
|
3199
3197
|
true,
|
|
3200
|
-
|
|
3198
|
+
ts4.ScriptKind.TSX
|
|
3201
3199
|
);
|
|
3202
3200
|
function tagNameText(node) {
|
|
3203
|
-
return
|
|
3201
|
+
return ts4.isIdentifier(node.tagName) ? node.tagName.text : void 0;
|
|
3204
3202
|
}
|
|
3205
3203
|
function attrExpression(node, attrName) {
|
|
3206
3204
|
for (const attr of node.attributes.properties) {
|
|
3207
|
-
if (
|
|
3205
|
+
if (ts4.isJsxAttribute(attr) && ts4.isIdentifier(attr.name) && attr.name.text === attrName && attr.initializer !== void 0 && ts4.isJsxExpression(attr.initializer)) {
|
|
3208
3206
|
return attr.initializer.expression;
|
|
3209
3207
|
}
|
|
3210
3208
|
}
|
|
@@ -3216,19 +3214,19 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode
|
|
|
3216
3214
|
if (anyElementIsInteractive) return true;
|
|
3217
3215
|
if (!TRAIT_HOST_TAGS.has(tag)) return false;
|
|
3218
3216
|
const asExpr = attrExpression(node, "as");
|
|
3219
|
-
return asExpr !== void 0 &&
|
|
3217
|
+
return asExpr !== void 0 && ts4.isIdentifier(asExpr) && TRAITS_THAT_FIRE.has(asExpr.text);
|
|
3220
3218
|
}
|
|
3221
3219
|
function extractCalleeName(expr) {
|
|
3222
3220
|
if (expr === void 0) return void 0;
|
|
3223
|
-
if (
|
|
3224
|
-
if (
|
|
3221
|
+
if (ts4.isIdentifier(expr)) return expr.text;
|
|
3222
|
+
if (ts4.isArrowFunction(expr) || ts4.isFunctionExpression(expr)) {
|
|
3225
3223
|
const body = expr.body;
|
|
3226
|
-
if (
|
|
3224
|
+
if (ts4.isCallExpression(body) && ts4.isIdentifier(body.expression)) {
|
|
3227
3225
|
return body.expression.text;
|
|
3228
3226
|
}
|
|
3229
|
-
if (
|
|
3227
|
+
if (ts4.isBlock(body)) {
|
|
3230
3228
|
for (const stmt of body.statements) {
|
|
3231
|
-
if (
|
|
3229
|
+
if (ts4.isExpressionStatement(stmt) && ts4.isCallExpression(stmt.expression) && ts4.isIdentifier(stmt.expression.expression)) {
|
|
3232
3230
|
return stmt.expression.expression.text;
|
|
3233
3231
|
}
|
|
3234
3232
|
}
|
|
@@ -3240,7 +3238,7 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode
|
|
|
3240
3238
|
let found;
|
|
3241
3239
|
function walk(node) {
|
|
3242
3240
|
if (found !== void 0) return;
|
|
3243
|
-
const opening =
|
|
3241
|
+
const opening = ts4.isJsxElement(node) ? node.openingElement : ts4.isJsxSelfClosingElement(node) ? node : void 0;
|
|
3244
3242
|
if (opening !== void 0) {
|
|
3245
3243
|
const tag = tagNameText(opening);
|
|
3246
3244
|
if (tag !== void 0 && (anyElementIsInteractive || INTERACTIVE_DESCENDANTS.has(tag))) {
|
|
@@ -3256,13 +3254,13 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode
|
|
|
3256
3254
|
}
|
|
3257
3255
|
}
|
|
3258
3256
|
}
|
|
3259
|
-
|
|
3257
|
+
ts4.forEachChild(node, walk);
|
|
3260
3258
|
}
|
|
3261
|
-
|
|
3259
|
+
ts4.forEachChild(outer, walk);
|
|
3262
3260
|
return found;
|
|
3263
3261
|
}
|
|
3264
3262
|
function visit(node) {
|
|
3265
|
-
if (
|
|
3263
|
+
if (ts4.isJsxElement(node) && isTraitHostWithFiringAs(node.openingElement)) {
|
|
3266
3264
|
const outerCallee = extractCalleeName(attrExpression(node.openingElement, "onClick")) ?? extractCalleeName(attrExpression(node.openingElement, "onPress"));
|
|
3267
3265
|
if (outerCallee !== void 0 && bindings.has(outerCallee)) {
|
|
3268
3266
|
const match = findMatchingInteractiveDescendant(node, outerCallee);
|
|
@@ -3283,7 +3281,7 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode
|
|
|
3283
3281
|
}
|
|
3284
3282
|
}
|
|
3285
3283
|
}
|
|
3286
|
-
|
|
3284
|
+
ts4.forEachChild(node, visit);
|
|
3287
3285
|
}
|
|
3288
3286
|
visit(sf);
|
|
3289
3287
|
return issues;
|
|
@@ -3302,6 +3300,63 @@ function isGadgetExportImported(source, pkg, exportName) {
|
|
|
3302
3300
|
}
|
|
3303
3301
|
return false;
|
|
3304
3302
|
}
|
|
3303
|
+
function detectComponentNeverReturns(sourceCode) {
|
|
3304
|
+
const sf = ts4.createSourceFile(
|
|
3305
|
+
"source.tsx",
|
|
3306
|
+
sourceCode,
|
|
3307
|
+
ts4.ScriptTarget.Latest,
|
|
3308
|
+
/* setParentNodes */
|
|
3309
|
+
true,
|
|
3310
|
+
ts4.ScriptKind.TSX
|
|
3311
|
+
);
|
|
3312
|
+
const isFunctionLike = (n) => ts4.isFunctionDeclaration(n) || ts4.isFunctionExpression(n) || ts4.isArrowFunction(n) || ts4.isMethodDeclaration(n) || ts4.isClassDeclaration(n) || ts4.isClassExpression(n);
|
|
3313
|
+
function resolveTopLevel(name) {
|
|
3314
|
+
for (const st of sf.statements) {
|
|
3315
|
+
if (ts4.isFunctionDeclaration(st) && st.name?.text === name) return st;
|
|
3316
|
+
if (ts4.isVariableStatement(st)) {
|
|
3317
|
+
for (const decl of st.declarationList.declarations) {
|
|
3318
|
+
if (ts4.isIdentifier(decl.name) && decl.name.text === name && decl.initializer !== void 0 && (ts4.isArrowFunction(decl.initializer) || ts4.isFunctionExpression(decl.initializer))) {
|
|
3319
|
+
return decl.initializer;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
return void 0;
|
|
3325
|
+
}
|
|
3326
|
+
let component;
|
|
3327
|
+
for (const st of sf.statements) {
|
|
3328
|
+
if (ts4.isFunctionDeclaration(st) && st.modifiers?.some((m) => m.kind === ts4.SyntaxKind.DefaultKeyword)) {
|
|
3329
|
+
component = st;
|
|
3330
|
+
break;
|
|
3331
|
+
}
|
|
3332
|
+
if (ts4.isExportAssignment(st) && st.isExportEquals !== true) {
|
|
3333
|
+
const expr = st.expression;
|
|
3334
|
+
if (ts4.isArrowFunction(expr) || ts4.isFunctionExpression(expr)) component = expr;
|
|
3335
|
+
else if (ts4.isIdentifier(expr)) component = resolveTopLevel(expr.text);
|
|
3336
|
+
break;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
if (component === void 0) return null;
|
|
3340
|
+
const body = component.body;
|
|
3341
|
+
if (body === void 0 || !ts4.isBlock(body)) return null;
|
|
3342
|
+
let returnsValue = false;
|
|
3343
|
+
let jsxStatementLine;
|
|
3344
|
+
const visit = (node) => {
|
|
3345
|
+
if (returnsValue) return;
|
|
3346
|
+
if (ts4.isReturnStatement(node)) {
|
|
3347
|
+
if (node.expression !== void 0) returnsValue = true;
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
if (jsxStatementLine === void 0 && ts4.isExpressionStatement(node) && (ts4.isJsxElement(node.expression) || ts4.isJsxFragment(node.expression) || ts4.isJsxSelfClosingElement(node.expression))) {
|
|
3351
|
+
jsxStatementLine = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
3352
|
+
}
|
|
3353
|
+
node.forEachChild((child) => {
|
|
3354
|
+
if (!isFunctionLike(child)) visit(child);
|
|
3355
|
+
});
|
|
3356
|
+
};
|
|
3357
|
+
for (const st of body.statements) if (!isFunctionLike(st)) visit(st);
|
|
3358
|
+
return returnsValue ? null : { jsxStatementLine };
|
|
3359
|
+
}
|
|
3305
3360
|
async function runTier0Checks(sourceCode, contract, buildErrors, gadgetTypes, designMode = DEFAULT_DESIGN_MODE) {
|
|
3306
3361
|
const issues = [];
|
|
3307
3362
|
const lines = sourceCode.split("\n");
|
|
@@ -3721,6 +3776,19 @@ ${errorDetail}`,
|
|
|
3721
3776
|
fix: "Add export default function Component(props: Props) { ... }"
|
|
3722
3777
|
});
|
|
3723
3778
|
}
|
|
3779
|
+
const neverReturns = detectComponentNeverReturns(sourceCode);
|
|
3780
|
+
if (neverReturns !== null) {
|
|
3781
|
+
issues.push({
|
|
3782
|
+
tier: 0,
|
|
3783
|
+
result: "fail",
|
|
3784
|
+
category: "compile",
|
|
3785
|
+
subcategory: "renders-nothing",
|
|
3786
|
+
severity: "critical",
|
|
3787
|
+
description: neverReturns.jsxStatementLine === void 0 ? "The component never returns its tree \u2014 it renders a blank frame" : `The component never returns its tree: the JSX at line ${neverReturns.jsxStatementLine} sits at statement position, so it is built and thrown away and the frame renders blank`,
|
|
3788
|
+
fix: "Wrap the tree in the return the boilerplate gave you: `return (` before the root element and `);` after it.",
|
|
3789
|
+
...neverReturns.jsxStatementLine === void 0 ? {} : { line: neverReturns.jsxStatementLine }
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3724
3792
|
if (contract) {
|
|
3725
3793
|
try {
|
|
3726
3794
|
const contractIssues = validateAllContracts(sourceCode, contract);
|
|
@@ -11979,18 +12047,18 @@ ${context}
|
|
|
11979
12047
|
}
|
|
11980
12048
|
var LABELED_COMPONENTS = /* @__PURE__ */ new Set(["Input", "TextArea", "Select"]);
|
|
11981
12049
|
function checkReactRules(code) {
|
|
11982
|
-
const sf =
|
|
12050
|
+
const sf = ts4.createSourceFile("component.tsx", code, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
|
|
11983
12051
|
const issues = [];
|
|
11984
12052
|
function visit(node) {
|
|
11985
|
-
if (
|
|
12053
|
+
if (ts4.isJsxSelfClosingElement(node) || ts4.isJsxOpeningElement(node)) {
|
|
11986
12054
|
const tagName = node.tagName.getText(sf);
|
|
11987
12055
|
if (LABELED_COMPONENTS.has(tagName)) {
|
|
11988
12056
|
const propNames = /* @__PURE__ */ new Set();
|
|
11989
12057
|
let hasSpread = false;
|
|
11990
12058
|
for (const attr of node.attributes.properties) {
|
|
11991
|
-
if (
|
|
12059
|
+
if (ts4.isJsxAttribute(attr)) {
|
|
11992
12060
|
propNames.add(attr.name.getText(sf));
|
|
11993
|
-
} else if (
|
|
12061
|
+
} else if (ts4.isJsxSpreadAttribute(attr)) {
|
|
11994
12062
|
hasSpread = true;
|
|
11995
12063
|
}
|
|
11996
12064
|
}
|
|
@@ -12005,7 +12073,7 @@ function checkReactRules(code) {
|
|
|
12005
12073
|
}
|
|
12006
12074
|
}
|
|
12007
12075
|
}
|
|
12008
|
-
|
|
12076
|
+
ts4.forEachChild(node, visit);
|
|
12009
12077
|
}
|
|
12010
12078
|
visit(sf);
|
|
12011
12079
|
return issues;
|
|
@@ -12777,6 +12845,9 @@ function truncateSourceForTrace(source) {
|
|
|
12777
12845
|
function newValidatorTraceId() {
|
|
12778
12846
|
return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
12779
12847
|
}
|
|
12848
|
+
|
|
12849
|
+
// src/evaluation/axis-checks/checks/universal.ts
|
|
12850
|
+
init_design_mode();
|
|
12780
12851
|
var ID_FIELD_CANDIDATES2 = ["id", "uuid", "symbol", "key", "slug", "code"];
|
|
12781
12852
|
function getItemsProperties(p) {
|
|
12782
12853
|
return p.items?.properties ?? p.schema?.items?.properties;
|
|
@@ -13174,6 +13245,126 @@ function runViewportSized(input) {
|
|
|
13174
13245
|
)
|
|
13175
13246
|
];
|
|
13176
13247
|
}
|
|
13248
|
+
var TERMINAL_WORDS = /* @__PURE__ */ new Set([
|
|
13249
|
+
"submit",
|
|
13250
|
+
"confirm",
|
|
13251
|
+
"approve",
|
|
13252
|
+
"schedule",
|
|
13253
|
+
"reserve",
|
|
13254
|
+
"book",
|
|
13255
|
+
"pay",
|
|
13256
|
+
"checkout",
|
|
13257
|
+
"purchase",
|
|
13258
|
+
"order"
|
|
13259
|
+
]);
|
|
13260
|
+
var TERMINAL_LABEL_RX = /\b(submit|confirm|approve|schedule|reserve|book|pay|checkout|purchase|place\s+order)\b/i;
|
|
13261
|
+
var GUARD_RX = /\b(disabled|aria-disabled)\s*=/;
|
|
13262
|
+
function nameWords(name) {
|
|
13263
|
+
return name.split(/(?=[A-Z])|[_\-\s]+/).map((w) => w.toLowerCase()).filter((w) => w.length > 0);
|
|
13264
|
+
}
|
|
13265
|
+
function findUnguardedTerminalControls(sourceCode) {
|
|
13266
|
+
if (GUARD_RX.test(sourceCode)) return [];
|
|
13267
|
+
const names = /* @__PURE__ */ new Set();
|
|
13268
|
+
for (const m of sourceCode.matchAll(/useAction(?:<[^>]*>)?\(\s*['"`]([^'"`]+)['"`]/g)) {
|
|
13269
|
+
const name = m[1] ?? "";
|
|
13270
|
+
if (nameWords(name).some((w) => TERMINAL_WORDS.has(w))) names.add(name);
|
|
13271
|
+
}
|
|
13272
|
+
for (const m of sourceCode.matchAll(/>([^<>{}]{1,60})<\/Button>/g)) {
|
|
13273
|
+
const label = (m[1] ?? "").replace(/\s+/g, " ").trim();
|
|
13274
|
+
if (label.length > 0 && TERMINAL_LABEL_RX.test(label)) names.add(label.slice(0, 40));
|
|
13275
|
+
}
|
|
13276
|
+
return [...names];
|
|
13277
|
+
}
|
|
13278
|
+
function runTerminalActionUnguarded(input) {
|
|
13279
|
+
if (input.compiledCode === null) return [];
|
|
13280
|
+
const found = findUnguardedTerminalControls(input.sourceCode);
|
|
13281
|
+
if (found.length === 0) return [];
|
|
13282
|
+
const list = found.map((t) => `"${t}"`).join(", ");
|
|
13283
|
+
return [
|
|
13284
|
+
mkIssue(
|
|
13285
|
+
"universal.terminal_action_unguarded",
|
|
13286
|
+
`${list} reads as an action the user means once, and nothing in this component disables the control after it fires \u2014 a double-click sends it twice.`,
|
|
13287
|
+
'Hold the fired state (`const [submitted, setSubmitted] = useState(false)`), set it in the handler, and pass `disabled={submitted}` to that control with a word that says so ("Submitted"). If the action IS meant to repeat \u2014 sending a message, adding an item \u2014 leave it armed and ignore this.',
|
|
13288
|
+
"warn"
|
|
13289
|
+
)
|
|
13290
|
+
];
|
|
13291
|
+
}
|
|
13292
|
+
function rootOpeningTag(sourceCode) {
|
|
13293
|
+
const start = sourceCode.indexOf("export default function");
|
|
13294
|
+
const body = start >= 0 ? sourceCode.slice(start) : sourceCode;
|
|
13295
|
+
const ret = /\breturn\s*\(?\s*</.exec(body);
|
|
13296
|
+
if (ret === null) return null;
|
|
13297
|
+
const at = ret.index + ret[0].length - 1;
|
|
13298
|
+
const nameMatch = /^<([A-Za-z][\w.]*)/.exec(body.slice(at));
|
|
13299
|
+
if (nameMatch === null) return null;
|
|
13300
|
+
let j = at + nameMatch[0].length;
|
|
13301
|
+
let depth = 0;
|
|
13302
|
+
let quote = null;
|
|
13303
|
+
for (; j < body.length; j++) {
|
|
13304
|
+
const ch = body[j];
|
|
13305
|
+
if (quote !== null) {
|
|
13306
|
+
if (ch === quote) quote = null;
|
|
13307
|
+
continue;
|
|
13308
|
+
}
|
|
13309
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
13310
|
+
else if (ch === "{") depth++;
|
|
13311
|
+
else if (ch === "}") depth--;
|
|
13312
|
+
else if (ch === ">" && depth === 0) break;
|
|
13313
|
+
}
|
|
13314
|
+
return { name: nameMatch[1], attrs: body.slice(at + nameMatch[0].length, j) };
|
|
13315
|
+
}
|
|
13316
|
+
var CONTAINER_PRESETS = maxWidth;
|
|
13317
|
+
var NOT_A_CAP = /* @__PURE__ */ new Set(["full", "none", "100%", "unset", "initial", "inherit"]);
|
|
13318
|
+
function capPx(value) {
|
|
13319
|
+
const v = value.trim();
|
|
13320
|
+
if (NOT_A_CAP.has(v)) return void 0;
|
|
13321
|
+
const resolved2 = CONTAINER_PRESETS[v] ?? v;
|
|
13322
|
+
if (NOT_A_CAP.has(resolved2)) return void 0;
|
|
13323
|
+
const px = /^(\d+(?:\.\d+)?)px$/.exec(resolved2);
|
|
13324
|
+
return px === null ? null : Number(px[1]);
|
|
13325
|
+
}
|
|
13326
|
+
function findRootWidthCap(sourceCode) {
|
|
13327
|
+
const root = rootOpeningTag(sourceCode);
|
|
13328
|
+
if (root === null) return null;
|
|
13329
|
+
const { name, attrs } = root;
|
|
13330
|
+
const prop = /\bmaxWidth\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)'|(\d+(?:\.\d+)?))\s*\})/.exec(attrs);
|
|
13331
|
+
if (prop !== null) {
|
|
13332
|
+
const raw = prop[1] ?? prop[2] ?? prop[3] ?? prop[4] ?? `${prop[5]}px`;
|
|
13333
|
+
const px = capPx(raw);
|
|
13334
|
+
return px === void 0 ? null : { element: name, cap: `maxWidth="${raw}"`, px };
|
|
13335
|
+
}
|
|
13336
|
+
const style = /\bmaxWidth\s*:\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`|(\d+(?:\.\d+)?))/.exec(attrs);
|
|
13337
|
+
if (style !== null) {
|
|
13338
|
+
const raw = style[1] ?? style[2] ?? style[3] ?? `${style[4]}px`;
|
|
13339
|
+
const px = capPx(raw);
|
|
13340
|
+
return px === void 0 ? null : { element: name, cap: `maxWidth: '${raw}'`, px };
|
|
13341
|
+
}
|
|
13342
|
+
const cls = /(?<![\w-])max-w-([\w[\]%.-]+)/.exec(attrs);
|
|
13343
|
+
if (cls !== null) {
|
|
13344
|
+
if (cls[1] === "full" || cls[1] === "none") return null;
|
|
13345
|
+
return { element: name, cap: `max-w-${cls[1]}`, px: null };
|
|
13346
|
+
}
|
|
13347
|
+
if (name === "Container") return { element: name, cap: "lg (Container default)", px: capPx("lg") ?? null };
|
|
13348
|
+
return null;
|
|
13349
|
+
}
|
|
13350
|
+
var WIDE_CANVASES = /* @__PURE__ */ new Set(["md", "lg", "xl"]);
|
|
13351
|
+
function runRootWidthCap(input) {
|
|
13352
|
+
if (input.compiledCode === null) return [];
|
|
13353
|
+
if (input.canvas === void 0 || !WIDE_CANVASES.has(input.canvas)) return [];
|
|
13354
|
+
const cap2 = findRootWidthCap(input.sourceCode);
|
|
13355
|
+
if (cap2 === null) return [];
|
|
13356
|
+
const width = CANVAS_VIEWPORTS[input.canvas].width;
|
|
13357
|
+
if (cap2.px !== null && cap2.px >= width) return [];
|
|
13358
|
+
const sized = cap2.px !== null ? `${cap2.px}px on a ${width}px canvas` : `on a ${width}px canvas`;
|
|
13359
|
+
return [
|
|
13360
|
+
mkIssue(
|
|
13361
|
+
"universal.root_width_cap",
|
|
13362
|
+
`The outermost element <${cap2.element}> carries a width cap (${cap2.cap}, ${sized}): on this canvas the card becomes a narrow strip with the frame's ground on either side. The frame owns the width; a cap belongs INSIDE the root, on the column that holds text.`,
|
|
13363
|
+
'Let the root fill the frame \u2014 drop the cap on the element you return at the top (or `maxWidth="full"`) \u2014 and put the reading measure on an inner column: `<Box padding="lg"><Container maxWidth="sm">\u2026</Container></Box>`.',
|
|
13364
|
+
"warn"
|
|
13365
|
+
)
|
|
13366
|
+
];
|
|
13367
|
+
}
|
|
13177
13368
|
var UNIVERSAL_CHECKS = [
|
|
13178
13369
|
{
|
|
13179
13370
|
id: "universal.icon_name_known",
|
|
@@ -13193,6 +13384,18 @@ var UNIVERSAL_CHECKS = [
|
|
|
13193
13384
|
values: ALL_RENDER_VALUES,
|
|
13194
13385
|
run: runViewportSized
|
|
13195
13386
|
},
|
|
13387
|
+
{
|
|
13388
|
+
id: "universal.root_width_cap",
|
|
13389
|
+
axis: "render",
|
|
13390
|
+
values: ALL_RENDER_VALUES,
|
|
13391
|
+
run: runRootWidthCap
|
|
13392
|
+
},
|
|
13393
|
+
{
|
|
13394
|
+
id: "universal.terminal_action_unguarded",
|
|
13395
|
+
axis: "render",
|
|
13396
|
+
values: ALL_RENDER_VALUES,
|
|
13397
|
+
run: runTerminalActionUnguarded
|
|
13398
|
+
},
|
|
13196
13399
|
{
|
|
13197
13400
|
id: "universal.accent_text_ink",
|
|
13198
13401
|
axis: "render",
|
|
@@ -14266,7 +14469,9 @@ function traceFacts(input) {
|
|
|
14266
14469
|
designMode: input.designMode ?? "constrained",
|
|
14267
14470
|
classification: input.classification.vector,
|
|
14268
14471
|
propsSpecKeys: Object.keys(propsSpec ?? {}),
|
|
14269
|
-
propsSpecPropertyKeys: Object.keys(propsSpec?.properties ?? {})
|
|
14472
|
+
propsSpecPropertyKeys: Object.keys(propsSpec?.properties ?? {}),
|
|
14473
|
+
// No default here, unlike designMode: an absent canvas is a fact about the caller, not a guess to paper over.
|
|
14474
|
+
...input.canvas !== void 0 ? { canvas: input.canvas } : {}
|
|
14270
14475
|
};
|
|
14271
14476
|
}
|
|
14272
14477
|
|
|
@@ -14304,7 +14509,9 @@ async function runCheck(input) {
|
|
|
14304
14509
|
contract,
|
|
14305
14510
|
originalPrompt: prompt,
|
|
14306
14511
|
classification: harness.classification,
|
|
14307
|
-
designMode: harness.designMode
|
|
14512
|
+
designMode: harness.designMode,
|
|
14513
|
+
// ggui#1117: the canvas the harness was built for, when it has one — never a default.
|
|
14514
|
+
...harness.canvas !== void 0 ? { canvas: harness.canvas } : {}
|
|
14308
14515
|
};
|
|
14309
14516
|
const axisRun = runGatedAxisChecks(check.axisChecks, axisInput);
|
|
14310
14517
|
issues.push(...axisRun.issues);
|
|
@@ -15058,7 +15265,8 @@ type ${typeName} = ${tsType};`
|
|
|
15058
15265
|
);
|
|
15059
15266
|
const callSig = tsType === "void" ? "() => void \u2014 fire and forget" : `(data: ${tsType}) => void`;
|
|
15060
15267
|
const toolHint = tool ? ` \u2192 nextStep: ${tool}` : "";
|
|
15061
|
-
|
|
15268
|
+
const onceHint = " \u2014 if this is meant ONCE, disable its control after it fires";
|
|
15269
|
+
actionHookCalls.push(` const ${key} = useAction<${typeName}>('${key}'); // ${callSig}${toolHint}${onceHint}`);
|
|
15062
15270
|
}
|
|
15063
15271
|
const streamChannels = contract?.streamSpec ?? {};
|
|
15064
15272
|
const streamChannelEntries = Object.entries(streamChannels);
|
|
@@ -15402,14 +15610,35 @@ Rules:
|
|
|
15402
15610
|
1. All request-specific data \u2192 props with defaults. City names, tickers, user names, dates, counts.
|
|
15403
15611
|
2. Layout and styling are universal. Colors, spacing, structure \u2014 these are the reusable part.
|
|
15404
15612
|
3. Default values come from the current request \u2014 so the component renders correctly standalone.
|
|
15405
|
-
4. Copy comes from the props. NEVER invent display text the contract does not carry \u2014 no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline.
|
|
15613
|
+
4. Copy comes from the props. NEVER invent display text the contract does not carry \u2014 no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. **Do NOT add a caps label above the heading** \u2014 not \`<Text caps>LET'S BEGIN</Text>\`, not \`WELCOME\`, not a section kicker like \`GETTING STARTED\` or \`OVERVIEW\` \u2014 UNLESS a prop supplies that exact text (\`<Text caps>{props.section}</Text>\`). When the request asks for a heading and a line, render the heading and the line and NOTHING above the heading. A heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render, and a caps eyebrow you wrote is the most common form of it.
|
|
15406
15614
|
5. Props interface must be typed and exported.`;
|
|
15407
|
-
var
|
|
15615
|
+
var TERMINAL_ACTIONS = `## Actions the user means once
|
|
15616
|
+
|
|
15617
|
+
A second press is a mistake only when it would make a SECOND THING HAPPEN IN THE WORLD \u2014 a second booking, a second charge, a second order, a second application sent. If pressing again would only write the SAME STATE AGAIN \u2014 saving, updating, renaming, setting a preference, toggling \u2014 nothing is duplicated and the control stays armed.
|
|
15618
|
+
|
|
15619
|
+
Guard a control only where you have evidence, not a feeling about the verb: the request itself names a one-time commitment \u2014 "submit the application", "place the order", "confirm the booking".
|
|
15620
|
+
|
|
15621
|
+
**When in doubt, leave it armed.** A control the user cannot press a second time has lost them something that worked \u2014 save, edit, save again is ordinary. A control left armed is only what every interface does by default.
|
|
15622
|
+
|
|
15623
|
+
When you do guard one:
|
|
15624
|
+
- Hold a small piece of state for it and set it in the handler: \`const [submitted, setSubmitted] = useState(false)\`.
|
|
15625
|
+
- \`disabled={submitted}\` on the control that fires it, and say so in the label or beside it ("Submitted", "Request sent") \u2014 a control that is disabled without explanation reads as broken.
|
|
15626
|
+
- A card re-rendered with new props starts fresh; the state above lives with the component, not with the request.
|
|
15627
|
+
|
|
15628
|
+
Nothing in the wire DECLARES one-shot-ness yet, which is why the test is what a second press would DO \u2014 not how final the word sounds.`;
|
|
15629
|
+
var FRAME_SIZING = `## Sizing \u2014 the frame owns the height and the width
|
|
15408
15630
|
|
|
15409
15631
|
The host sizes the iframe; the component never sizes itself to the viewport. NEVER write \`100vh\` / \`100dvh\` / \`100svh\` (or an \`h-screen\` / \`min-h-screen\` class) as a height on any element:
|
|
15410
15632
|
- An inline card takes its natural height. A content-sized frame is measured FROM the content, so a \`100vh\` root can never shrink and grows the frame on every re-measure.
|
|
15411
15633
|
- Under fullscreen the frame already stretches your root to its full height. Centre inside it with flex (\`display: flex; flex-direction: column; justify-content: center\`), never with a viewport height.
|
|
15412
|
-
- To fill a parent that has a height use \`height: 100%\` or \`flex: 1\`; a viewport unit is only ever a \`max-height\` cap on a scroll region
|
|
15634
|
+
- To fill a parent that has a height use \`height: 100%\` or \`flex: 1\`; a viewport unit is only ever a \`max-height\` cap on a scroll region.
|
|
15635
|
+
|
|
15636
|
+
The width works the same way, and the mistake is the mirror image: **your outermost element FILLS the frame; a width cap belongs INSIDE it, on the column that holds text.**
|
|
15637
|
+
|
|
15638
|
+
- Never put \`maxWidth\` (or a \`max-w-*\` class) on the element you return at the top. On a wide canvas that turns your card into a narrow strip with the frame's ground on either side of it, and on a narrow one it does nothing \u2014 the same composition reading differently at two sizes.
|
|
15639
|
+
- DO cap the reading measure inside: a column of prose is easiest to read at 60\u201370 characters, so wrap the TEXT in a \`<Container maxWidth="sm">\` (or your own \`max-width\`) and leave the root filling. A hero band, a toolbar, a table or an image then still uses the whole width, which is what makes a wide card look composed rather than padded.
|
|
15640
|
+
- Centre that inner column with the container's own centring, not by shrinking the root.
|
|
15641
|
+
- **They are two different elements.** Even when the whole card is one column of text, the root still fills and the column sits INSIDE it \u2014 \`<Box padding="lg"><Container maxWidth="sm">\u2026</Container></Box>\`, never a root that is itself the container. A centred card is centring inside a filled frame, not a shrunken frame.`;
|
|
15413
15642
|
var COMPONENT_STRUCTURE = `## Component Structure
|
|
15414
15643
|
|
|
15415
15644
|
Keep JSX nesting depth to 3\u20135 levels. When deeper, extract repeated/complex sections into helper components \u2014 named functions defined above the main Component in the same file. Helpers take data + callbacks via props; they don't own state.
|
|
@@ -15621,6 +15850,7 @@ ${axisSection}
|
|
|
15621
15850
|
CONTRACT_SURFACE,
|
|
15622
15851
|
DEFENSIVE_CODING,
|
|
15623
15852
|
GESTURE_ROUTING,
|
|
15853
|
+
TERMINAL_ACTIONS,
|
|
15624
15854
|
FREE_GESTURE_SURFACE,
|
|
15625
15855
|
ANTI_PATTERNS,
|
|
15626
15856
|
CROSS_REFERENCE_RULES,
|
|
@@ -15651,11 +15881,11 @@ ${inputs.primitivesDoc}`
|
|
|
15651
15881
|
}
|
|
15652
15882
|
var VIRTUAL_DTS_PATH = "/__gadget__.d.ts";
|
|
15653
15883
|
var EXTRACTOR_COMPILER_OPTIONS = {
|
|
15654
|
-
target:
|
|
15655
|
-
module:
|
|
15656
|
-
moduleResolution:
|
|
15884
|
+
target: ts4.ScriptTarget.ESNext,
|
|
15885
|
+
module: ts4.ModuleKind.ESNext,
|
|
15886
|
+
moduleResolution: ts4.ModuleResolutionKind.Bundler,
|
|
15657
15887
|
allowJs: false,
|
|
15658
|
-
jsx:
|
|
15888
|
+
jsx: ts4.JsxEmit.ReactJSX,
|
|
15659
15889
|
noEmit: true,
|
|
15660
15890
|
esModuleInterop: true,
|
|
15661
15891
|
// skipLibCheck so an unresolvable wrapper-internal import (the
|
|
@@ -15667,31 +15897,31 @@ var EXTRACTOR_COMPILER_OPTIONS = {
|
|
|
15667
15897
|
var callSignatureCache = /* @__PURE__ */ new Map();
|
|
15668
15898
|
var componentPropsCache = /* @__PURE__ */ new Map();
|
|
15669
15899
|
function buildExtractorContext(dtsContent, names) {
|
|
15670
|
-
const sourceFile =
|
|
15900
|
+
const sourceFile = ts4.createSourceFile(
|
|
15671
15901
|
VIRTUAL_DTS_PATH,
|
|
15672
15902
|
dtsContent,
|
|
15673
|
-
|
|
15903
|
+
ts4.ScriptTarget.ESNext,
|
|
15674
15904
|
/* setParentNodes */
|
|
15675
15905
|
true,
|
|
15676
|
-
|
|
15906
|
+
ts4.ScriptKind.TS
|
|
15677
15907
|
);
|
|
15678
|
-
const defaultLibName =
|
|
15908
|
+
const defaultLibName = ts4.getDefaultLibFileName(EXTRACTOR_COMPILER_OPTIONS);
|
|
15679
15909
|
const host = {
|
|
15680
15910
|
getSourceFile(fileName) {
|
|
15681
15911
|
if (fileName === VIRTUAL_DTS_PATH) return sourceFile;
|
|
15682
|
-
const libContent =
|
|
15683
|
-
|
|
15912
|
+
const libContent = ts4.sys.readFile(
|
|
15913
|
+
ts4.getDefaultLibFilePath(EXTRACTOR_COMPILER_OPTIONS).replace(
|
|
15684
15914
|
/[^/\\]+$/,
|
|
15685
15915
|
fileName
|
|
15686
15916
|
)
|
|
15687
15917
|
);
|
|
15688
15918
|
if (libContent !== void 0) {
|
|
15689
|
-
return
|
|
15919
|
+
return ts4.createSourceFile(
|
|
15690
15920
|
fileName,
|
|
15691
15921
|
libContent,
|
|
15692
|
-
|
|
15922
|
+
ts4.ScriptTarget.ESNext,
|
|
15693
15923
|
true,
|
|
15694
|
-
|
|
15924
|
+
ts4.ScriptKind.TS
|
|
15695
15925
|
);
|
|
15696
15926
|
}
|
|
15697
15927
|
return void 0;
|
|
@@ -15715,14 +15945,14 @@ function buildExtractorContext(dtsContent, names) {
|
|
|
15715
15945
|
},
|
|
15716
15946
|
fileExists(f) {
|
|
15717
15947
|
if (f === VIRTUAL_DTS_PATH) return true;
|
|
15718
|
-
return
|
|
15948
|
+
return ts4.sys.fileExists(f);
|
|
15719
15949
|
},
|
|
15720
15950
|
readFile(f) {
|
|
15721
15951
|
if (f === VIRTUAL_DTS_PATH) return dtsContent;
|
|
15722
|
-
return
|
|
15952
|
+
return ts4.sys.readFile(f);
|
|
15723
15953
|
}
|
|
15724
15954
|
};
|
|
15725
|
-
const program =
|
|
15955
|
+
const program = ts4.createProgram(
|
|
15726
15956
|
[VIRTUAL_DTS_PATH],
|
|
15727
15957
|
EXTRACTOR_COMPILER_OPTIONS,
|
|
15728
15958
|
host
|
|
@@ -15740,9 +15970,9 @@ function buildExtractorContext(dtsContent, names) {
|
|
|
15740
15970
|
}
|
|
15741
15971
|
const MAX_DEPTH2 = 4;
|
|
15742
15972
|
function renderType(t, depth) {
|
|
15743
|
-
const plain = () => checker.typeToString(t, void 0,
|
|
15973
|
+
const plain = () => checker.typeToString(t, void 0, ts4.TypeFormatFlags.NoTruncation);
|
|
15744
15974
|
if (depth >= MAX_DEPTH2) return plain();
|
|
15745
|
-
if ((t.getFlags() &
|
|
15975
|
+
if ((t.getFlags() & ts4.TypeFlags.Boolean) !== 0) return "boolean";
|
|
15746
15976
|
if (t.isUnion()) {
|
|
15747
15977
|
return t.types.map((member) => renderType(member, depth)).join(" | ");
|
|
15748
15978
|
}
|
|
@@ -15756,13 +15986,13 @@ function buildExtractorContext(dtsContent, names) {
|
|
|
15756
15986
|
const params = cs.getParameters().map((p) => {
|
|
15757
15987
|
const decl = p.valueDeclaration ?? p.declarations?.[0];
|
|
15758
15988
|
const pType = decl !== void 0 ? checker.getTypeOfSymbolAtLocation(p, decl) : checker.getDeclaredTypeOfSymbol(p);
|
|
15759
|
-
const optional = decl !== void 0 &&
|
|
15989
|
+
const optional = decl !== void 0 && ts4.isParameter(decl) && (decl.questionToken !== void 0 || decl.initializer !== void 0);
|
|
15760
15990
|
return `${p.getName()}${optional ? "?" : ""}: ${renderType(pType, depth + 1)}`;
|
|
15761
15991
|
}).join(", ");
|
|
15762
15992
|
const ret = renderType(cs.getReturnType(), depth + 1);
|
|
15763
15993
|
return `(${params}) => ${ret}`;
|
|
15764
15994
|
}
|
|
15765
|
-
const isObject = (t.getFlags() &
|
|
15995
|
+
const isObject = (t.getFlags() & ts4.TypeFlags.Object) !== 0;
|
|
15766
15996
|
const isArrayOrTuple = checker.isArrayType(t) || checker.isTupleType(t);
|
|
15767
15997
|
const typeArgs = t.typeArguments;
|
|
15768
15998
|
if (symbol !== void 0 && isObject && !isArrayOrTuple && !isWrapperLocal(symbol) && typeArgs !== void 0 && typeArgs.length > 0) {
|
|
@@ -15775,7 +16005,7 @@ function buildExtractorContext(dtsContent, names) {
|
|
|
15775
16005
|
const body = props.map((p) => {
|
|
15776
16006
|
const decl = p.valueDeclaration ?? p.declarations?.[0];
|
|
15777
16007
|
const pType = decl !== void 0 ? checker.getTypeOfSymbolAtLocation(p, decl) : checker.getDeclaredTypeOfSymbol(p);
|
|
15778
|
-
const optional = (p.getFlags() &
|
|
16008
|
+
const optional = (p.getFlags() & ts4.SymbolFlags.Optional) !== 0;
|
|
15779
16009
|
return `${p.getName()}${optional ? "?" : ""}: ${renderType(pType, depth + 1)}`;
|
|
15780
16010
|
}).join("; ");
|
|
15781
16011
|
return `{ ${body} }`;
|
|
@@ -15790,7 +16020,7 @@ function buildExtractorContext(dtsContent, names) {
|
|
|
15790
16020
|
const name = symbol.getName();
|
|
15791
16021
|
if (!requested.has(name)) continue;
|
|
15792
16022
|
if (signaturesByName.has(name)) continue;
|
|
15793
|
-
const resolved2 = (symbol.flags &
|
|
16023
|
+
const resolved2 = (symbol.flags & ts4.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
|
|
15794
16024
|
const declarations = resolved2.getDeclarations();
|
|
15795
16025
|
if (declarations === void 0 || declarations.length === 0) continue;
|
|
15796
16026
|
const declaration = declarations[0];
|
|
@@ -15805,7 +16035,7 @@ function printCallSignature(sig, ctx) {
|
|
|
15805
16035
|
const params = sig.getParameters().map((p) => {
|
|
15806
16036
|
const decl = p.valueDeclaration ?? p.declarations?.[0];
|
|
15807
16037
|
const pType = decl !== void 0 ? ctx.checker.getTypeOfSymbolAtLocation(p, decl) : ctx.checker.getDeclaredTypeOfSymbol(p);
|
|
15808
|
-
const optional = decl !== void 0 &&
|
|
16038
|
+
const optional = decl !== void 0 && ts4.isParameter(decl) && (decl.questionToken !== void 0 || decl.initializer !== void 0);
|
|
15809
16039
|
return `${p.getName()}${optional ? "?" : ""}: ${ctx.renderType(pType, 0)}`;
|
|
15810
16040
|
}).join(", ");
|
|
15811
16041
|
const ret = ctx.renderType(sig.getReturnType(), 0);
|
|
@@ -16019,6 +16249,7 @@ ${axisSection}
|
|
|
16019
16249
|
CONTRACT_SURFACE,
|
|
16020
16250
|
DEFENSIVE_CODING,
|
|
16021
16251
|
GESTURE_ROUTING,
|
|
16252
|
+
TERMINAL_ACTIONS,
|
|
16022
16253
|
INTERACTIVE_TRAITS,
|
|
16023
16254
|
ANTI_PATTERNS,
|
|
16024
16255
|
CROSS_REFERENCE_RULES,
|
|
@@ -16226,7 +16457,7 @@ Don't render text-only metrics. A weather widget without a sun/cloud, a stock ca
|
|
|
16226
16457
|
</Row>
|
|
16227
16458
|
\`\`\`
|
|
16228
16459
|
|
|
16229
|
-
Use \`<Icon name="..." />\` for line icons \u2014 ONLY the curated
|
|
16460
|
+
Use \`<Icon name="..." />\` for line icons \u2014 ONLY the design system's curated set renders (~200 kebab-case Lucide names); a name OUTSIDE it renders an empty box and fails the check. On a single-pass render you cannot look the set up, so use ONLY a name you are sure is in it. Safe common names: \`check\` \`x\` \`plus\` \`minus\` \`search\` \`settings\` \`user\` \`users\` \`calendar\` \`clock\` \`arrow-right\` \`arrow-left\` \`chevron-right\` \`chevron-down\` \`trash-2\` \`edit\` \`info\` \`alert-circle\` \`bell\` \`home\` \`file\` \`file-text\` \`star\` \`heart\` \`mail\` \`send\` \`download\` \`upload\` \`external-link\` \`filter\` \`check-circle\`. The numbered variants (\`edit-2\`, \`edit-3\`) and synonyms (\`pencil\`, \`pen\`, \`trash\`, \`x-circle\`) are NOT in the set \u2014 use \`edit\` and \`trash-2\`. If the icon you want is not clearly one of these, OMIT it: a card with no icon beats one with an empty box. Emoji render directly for status/weather/mood. For per-stat tiny accents, use a small icon next to the label.
|
|
16230
16461
|
|
|
16231
16462
|
### Spacing \u2014 generosity beats compactness
|
|
16232
16463
|
|
|
@@ -16704,6 +16935,7 @@ function createHarness(input) {
|
|
|
16704
16935
|
name,
|
|
16705
16936
|
classification,
|
|
16706
16937
|
designMode,
|
|
16938
|
+
...input.canvas !== void 0 ? { canvas: input.canvas } : {},
|
|
16707
16939
|
...input.profile !== void 0 ? { profile: input.profile } : {},
|
|
16708
16940
|
how,
|
|
16709
16941
|
what,
|
|
@@ -16760,7 +16992,7 @@ init_policy2();
|
|
|
16760
16992
|
init_validation();
|
|
16761
16993
|
|
|
16762
16994
|
// src/tools/get-wire.ts
|
|
16763
|
-
var WIRE_DOCUMENTATION = "# ggui Wire Hooks Reference\n\n> Wire hooks connect generated UI components to agent communication.\n> They are pre-imported in the boilerplate \u2014 use them directly.\n> All hooks must be called inside a GguiWireProvider (handled automatically by the renderer).\n\nImport: `import { useAction, useStream } from '@ggui-ai/wire'`\n\n## Communication Hooks\n\n\n\nThese are the wire primitives for component-agent communication. `useWiredTool` retired 2026-05-11 \u2014 agentCapabilities.tools is a catalog the AGENT invokes, not a component hook surface; user gestures use `useAction(name)` and the optional `nextStep` field on the action entry names the tool the agent SHOULD invoke next.\n\n### useAction\n\nFire an action to the agent. Fire-and-forget \u2014 no response, no pending state.\n\nEvery action is an event: it lands on the GguiSession's consume buffer and\nthe agent receives it on its next turn (via `ggui_consume`). The action\nentry's optional `nextStep` names the tool the agent SHOULD invoke in\nresponse \u2014 advisory only; the agent owns the call decision. Component code\nis identical either way: call `useAction(name)(payload)` and treat\n`nextStep` as informational (use it to inform button labels, icons, copy).\n\nRUNTIME DEDUP (backstop, not a feature). Same-`(name, payload)` calls within\none event-loop task are coalesced \u2014 the first wins, subsequent duplicates\nare suppressed. This is the structural defense against LLM-generated\nnested-interactive components where a Checkbox `onChange` and an outer\n`Card as={Clickable}` `onClick` both wire to the same `useAction` binding;\nthe inner gesture bubbles to the outer handler, so without dedup one user\nclick would fire the action twice (a toggle would run back-to-back and the\nuser's change would disappear). **Do not rely on this as a feature** \u2014 wire\neach `useAction` callback to exactly ONE interactive surface; the dedup\nexists only because the LLM's source code can be wrong in subtle ways and\nthe runtime is the only place that can see the actual event-bubble path.\nSee `dispatch-dedup.ts` for the full failure-mode rationale.\n\nNEVER SILENT. When the dedup fires, a `console.warn` is emitted in BOTH dev\nand prod with the full diagnostic. The suppression is always visible in\nbrowser DevTools; operators investigating a \"the second click does nothing\"\nreport see the warning immediately. A structured observer also exists as\n`WireConfig.onDispatchSuppressed`, but the first-party renderers never set\nit \u2014 it is reachable only from a hand-built `WireConfig` (see its\ndocstring), so the console signal is what operators get in practice.\n\n**Signature:** `useAction<T = unknown>(actionName: string): (data: T) => void`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| actionName | `string` | Action name from the action contract |\n\n**Returns:** `(data: T) => void`\n\n**Example:**\n```tsx\nconst submitForm = useAction<{name: string; email: string}>('formSubmit');\n\n// In JSX:\n<Button onClick={() => submitForm({ name, email })}>Submit</Button>\n```\n\n\n### useStream\n\nSubscribe to deliveries on a named stream channel.\n\nHonors the channel's per-delivery `mode` ('append' vs 'replace')\nand the optional `complete` terminal marker. Channels declared\n`mode: 'replace'` on the spec typically emit every delivery with\n`mode: 'replace'` \u2014 this hook folds them into a single-latest\nvalue without accumulating history.\n\n**Signature:** `useStream<T = unknown>(channelName: string): StreamResult<T>`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| channelName | `string` | Channel name from the GguiSession's streamSpec |\n\n**Returns:** `StreamResult<T>`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| latest | `T \\| null` | Most recent payload delivered on this channel, or null if none received yet. |\n| all | `T[]` | All payloads accumulated on this channel. - `mode: 'append'` deliveries are pushed to the tail (continuous stream). - `mode: 'replace'` deliveries collapse `all` to a single-element array containing the latest payload \u2014 matching the channel's \"full replacement\" semantics. |\n| isComplete | `boolean` | Truthy after the channel has delivered an envelope with `complete: true`. Subscribers flip into a \"channel closed\" rendering state based on this signal; further deliveries on a completed channel are still accumulated, since the underlying wire doesn't enforce quiescence. |\n\n**Example:**\n```tsx\nconst progress = useStream<{ percent: number; message: string }>('progress');\n\n// In JSX:\n{progress.latest && (\n <Progress value={progress.latest.percent} />\n)}\n<Text>{progress.all.length} updates received</Text>\n```\n\n\n## Context Hooks\n\n\n\nRead-only access to render, app, and auth context.\n\n### useAuth\n\nRead-only auth context. Token excluded \u2014 auth is added server-side.\n\n**Signature:** `useAuth(): AuthInfo`\n\n**Returns:** `AuthInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| userId | `string` | userId |\n| isAuthenticated | `boolean` | isAuthenticated |\n\n**Example:**\n```tsx\nconst auth = useAuth();\n\n// In JSX:\n{auth.isAuthenticated\n ? <Text>Welcome, user {auth.userId}</Text>\n : <Text>Please sign in</Text>\n}\n```\n\n\n### useApp\n\nRead-only app metadata.\n\n**Signature:** `useApp(): AppInfo`\n\n**Returns:** `AppInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| appId | `string` | appId |\n| appName | `string` | appName |\n| appDescription | `string` | appDescription |\n| appIcon | `string` | appIcon |\n\n**Example:**\n```tsx\nconst app = useApp();\n\n// In JSX:\n<Heading>{app.appName}</Heading>\n{app.appDescription && <Text>{app.appDescription}</Text>}\n```\n\n\n### useRender\n\nRead-only render context with connection status.\n\n**Signature:** `useRender(): GguiSessionInfo`\n\n**Returns:** `GguiSessionInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| sessionId | `string` | sessionId |\n| isConnected | `boolean` | isConnected |\n\n**Example:**\n```tsx\nconst render = useRender();\n\n// In JSX:\n<Badge variant={render.isConnected ? 'success' : 'error'}>\n {render.isConnected ? 'Connected' : 'Disconnected'}\n</Badge>\n```\n\n\n## Provider\n\n\n\nThe provider is set up automatically by the renderer. Generated components do not need to wrap themselves in a provider.\n\n### GguiWireProvider\n\nReact context provider that injects WireConfig for all wire hooks.\n\n**Props:**\n\n| Prop | Type | Description |\n|------|------|-------------|\n| config | `WireConfig` | config |\n| children | `ReactNode` | children |\n\n**Example:**\n```tsx\n<GguiWireProvider config={wireConfig}>\n <YourComponent />\n</GguiWireProvider>\n```\n\n\n## Internal: WireConfig\n\n\n\nThis is the configuration object passed to GguiWireProvider. Generated components do not interact with this directly \u2014 it is provided by the renderer.\n\n### WireConfig\n\nConfiguration injected by the provider \u2014 the renderer inside the\niframe.\n\nEvery method is typed against the contract generic `T` so typed\ncallers get compile-time enforcement:\n - `dispatch(name, data)` \u2014 `name` MUST be a declared actionSpec\n key; `data` MUST satisfy that action's schema.\n - `subscribe(channel, handler)` \u2014 same discipline for streamSpec.\n\nUntyped callers (`T = DataContract` default) degrade to the broad\nshape via the conditional `WireDispatchData` / `WireStreamPayload`\naliases \u2014 no call-site break.\n\nThe contract's `agentCapabilities.tools` catalog declares tools the AGENT\ninvokes (not the component); user gestures fire via\n`dispatch(name, data)` and the optional `nextStep` field on the\naction entry names the tool the agent SHOULD invoke next.\n\nThe renderer mounts exactly ONE GguiSession per iframe \u2014 `render.sessionId`\nis the stable identity the WireProvider was constructed with. No\nper-item scoping factory; with a one-GguiSession-per-mount lifecycle,\n\"scope\" collapses to identity.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| app | `{ readonly appId: string; readonly appName: string; readonly appDescription?: string; readonly appIcon?: string; }` | app |\n| render | `{ readonly sessionId: string; readonly isConnected: boolean; }` | render |\n| auth | `{ readonly userId?: string; readonly isAuthenticated: boolean; }` | auth |\n| dispatch | `<N extends string>( actionName: N, data: WireDispatchData<T, N>, ) => void` | Fire an action to the agent (fire-and-forget over WS). Typed callers get compile-time checked `name` + `data`; untyped callers (`T = DataContract`) keep the broad shape. |\n| subscribe | `<N extends string>( channelName: N, handler: (delivery: StreamDelivery<WireStreamPayload<T, N>>) => void, ) => () => void` | Subscribe to deliveries on a named stream channel. |\n| onDispatchSuppressed | `(info: DispatchSuppressedInfo) => void` | Optional structured observability for `useAction`'s task-scoped duplicate-dispatch suppression. Fires alongside the always-on `console.warn` whenever the runtime coalesces a same-(name, payload) re-dispatch within one event-loop task \u2014 the nested- interactive double-fire backstop. Reachability constraint: the first-party renderers do NOT set this field \u2014 `buildRootWireConfig` (`@ggui-ai/iframe-runtime`) and `<GguiRender>`'s internal config both omit it. It fires only when a host hand-builds a complete `WireConfig` and mounts `GguiWireProvider` itself. On every first-party render path the `console.warn` is the sole suppression signal. |\n";
|
|
16995
|
+
var WIRE_DOCUMENTATION = "# ggui Wire Hooks Reference\n\n> Wire hooks connect generated UI components to agent communication.\n> They are pre-imported in the boilerplate \u2014 use them directly.\n> All hooks must be called inside a GguiWireProvider (handled automatically by the renderer).\n\nImport: `import { useAction, useStream } from '@ggui-ai/wire'`\n\n## Communication Hooks\n\n\n\nThese are the wire primitives for component-agent communication. `useWiredTool` retired 2026-05-11 \u2014 agentCapabilities.tools is a catalog the AGENT invokes, not a component hook surface; user gestures use `useAction(name)` and the optional `nextStep` field on the action entry names the tool the agent SHOULD invoke next.\n\n### useAction\n\nFire an action to the agent. Fire-and-forget \u2014 no response, no pending state.\n\nEvery action is an event: it lands on the GguiSession's consume buffer and\nthe agent receives it on its next turn (via `ggui_consume`). The action\nentry's optional `nextStep` names the tool the agent SHOULD invoke in\nresponse \u2014 advisory only; the agent owns the call decision. Component code\nis identical either way: call `useAction(name)(payload)` and treat\n`nextStep` as informational (use it to inform button labels, icons, copy).\n\nRUNTIME DEDUP (backstop, not a feature). Same-`(name, payload)` calls within\none event-loop task are coalesced \u2014 the first wins, subsequent duplicates\nare suppressed. This is the structural defense against LLM-generated\nnested-interactive components where a Checkbox `onChange` and an outer\n`Card as={Clickable}` `onClick` both wire to the same `useAction` binding;\nthe inner gesture bubbles to the outer handler, so without dedup one user\nclick would fire the action twice (a toggle would run back-to-back and the\nuser's change would disappear). **Do not rely on this as a feature** \u2014 wire\neach `useAction` callback to exactly ONE interactive surface; the dedup\nexists only because the LLM's source code can be wrong in subtle ways and\nthe runtime is the only place that can see the actual event-bubble path.\nSee `dispatch-dedup.ts` for the full failure-mode rationale.\n\nNEVER SILENT. When the dedup fires, a `console.warn` is emitted in BOTH dev\nand prod with the full diagnostic. The suppression is always visible in\nbrowser DevTools; operators investigating a \"the second click does nothing\"\nreport see the warning immediately. A structured observer also exists as\n`WireConfig.onDispatchSuppressed`, but the first-party renderers never set\nit \u2014 it is reachable only from a hand-built `WireConfig` (see its\ndocstring), so the console signal is what operators get in practice.\n\n**Signature:** `useAction<T = unknown>(actionName: string): (data: T) => void`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| actionName | `string` | Action name from the action contract |\n\n**Returns:** `(data: T) => void`\n\n**Example:**\n```tsx\nconst submitForm = useAction<{name: string; email: string}>('formSubmit');\n\n// In JSX:\n<Button onClick={() => submitForm({ name, email })}>Submit</Button>\n```\n\n\n### useStream\n\nSubscribe to deliveries on a named stream channel.\n\nHonors the channel's per-delivery `mode` ('append' vs 'replace')\nand the optional `complete` terminal marker. Channels declared\n`mode: 'replace'` on the spec typically emit every delivery with\n`mode: 'replace'` \u2014 this hook folds them into a single-latest\nvalue without accumulating history.\n\n**Signature:** `useStream<T = unknown>(channelName: string): StreamResult<T>`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| channelName | `string` | Channel name from the GguiSession's streamSpec |\n\n**Returns:** `StreamResult<T>`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| latest | `T \\| null` | Most recent payload delivered on this channel, or null if none received yet. |\n| all | `T[]` | All payloads accumulated on this channel. - `mode: 'append'` deliveries are pushed to the tail (continuous stream). - `mode: 'replace'` deliveries collapse `all` to a single-element array containing the latest payload \u2014 matching the channel's \"full replacement\" semantics. |\n| isComplete | `boolean` | Truthy after the channel has delivered an envelope with `complete: true`. Subscribers flip into a \"channel closed\" rendering state based on this signal; further deliveries on a completed channel are still accumulated, since the underlying wire doesn't enforce quiescence. |\n\n**Example:**\n```tsx\nconst progress = useStream<{ percent: number; message: string }>('progress');\n\n// In JSX:\n{progress.latest && (\n <Progress value={progress.latest.percent} />\n)}\n<Text>{progress.all.length} updates received</Text>\n```\n\n\n## Context Hooks\n\n\n\nRead-only access to render, app, and auth context.\n\n### useAuth\n\nRead-only auth context. Token excluded \u2014 auth is added server-side.\n\n**Signature:** `useAuth(): AuthInfo`\n\n**Returns:** `AuthInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| userId | `string` | userId |\n| isAuthenticated | `boolean` | isAuthenticated |\n\n**Example:**\n```tsx\nconst auth = useAuth();\n\n// In JSX:\n{auth.isAuthenticated\n ? <Text>Welcome, user {auth.userId}</Text>\n : <Text>Please sign in</Text>\n}\n```\n\n\n### useApp\n\nRead-only app metadata.\n\n**Signature:** `useApp(): AppInfo`\n\n**Returns:** `AppInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| appId | `string` | appId |\n| appName | `string` | appName |\n| appDescription | `string` | appDescription |\n| appIcon | `string` | appIcon |\n\n**Example:**\n```tsx\nconst app = useApp();\n\n// In JSX:\n<Heading>{app.appName}</Heading>\n{app.appDescription && <Text>{app.appDescription}</Text>}\n```\n\n\n### useRender\n\nRead-only render context with connection status.\n\n**Signature:** `useRender(): GguiSessionInfo`\n\n**Returns:** `GguiSessionInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| sessionId | `string` | sessionId |\n| isConnected | `boolean` | isConnected |\n\n**Example:**\n```tsx\nconst render = useRender();\n\n// In JSX:\n<Badge variant={render.isConnected ? 'success' : 'error'}>\n {render.isConnected ? 'Connected' : 'Disconnected'}\n</Badge>\n```\n\n\n## Provider\n\n\n\nThe provider is set up automatically by the renderer. Generated components do not need to wrap themselves in a provider.\n\n### GguiWireProvider\n\nReact context provider that injects WireConfig for all wire hooks.\n\n**Props:**\n\n| Prop | Type | Description |\n|------|------|-------------|\n| config | `WireConfig` | config |\n| children | `ReactNode` | children |\n\n**Example:**\n```tsx\n<GguiWireProvider config={wireConfig}>\n <YourComponent />\n</GguiWireProvider>\n```\n\n\n## Internal: WireConfig\n\n\n\nThis is the configuration object passed to GguiWireProvider. Generated components do not interact with this directly \u2014 it is provided by the renderer.\n\n### WireConfig\n\nConfiguration injected by the provider \u2014 the renderer inside the\niframe.\n\nEvery method is typed against the contract generic `T` so typed\ncallers get compile-time enforcement:\n - `dispatch(name, data)` \u2014 `name` MUST be a declared actionSpec\n key; `data` MUST satisfy that action's schema.\n - `subscribe(channel, handler)` \u2014 same discipline for streamSpec.\n\nUntyped callers (`T = DataContract` default) degrade to the broad\nshape via the conditional `WireDispatchData` / `WireStreamPayload`\naliases \u2014 no call-site break.\n\nThe contract's `agentCapabilities.tools` catalog declares tools the AGENT\ninvokes (not the component); user gestures fire via\n`dispatch(name, data)` and the optional `nextStep` field on the\naction entry names the tool the agent SHOULD invoke next.\n\nThe renderer mounts exactly ONE GguiSession per iframe \u2014 `render.sessionId`\nis the stable identity the WireProvider was constructed with. No\nper-item scoping factory; with a one-GguiSession-per-mount lifecycle,\n\"scope\" collapses to identity.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| app | `{ readonly appId: string; readonly appName: string; readonly appDescription?: string; readonly appIcon?: string; }` | app |\n| render | `{ readonly sessionId: string; readonly isConnected: boolean; }` | render |\n| auth | `{ readonly userId?: string; readonly isAuthenticated: boolean; }` | auth |\n| dispatch | `<N extends string>( actionName: N, data: WireDispatchData<T, N>, ) => void` | Fire an action to the agent (fire-and-forget over WS). Typed callers get compile-time checked `name` + `data`; untyped callers (`T = DataContract`) keep the broad shape. |\n| subscribe | `<N extends string>( channelName: N, handler: (delivery: StreamDelivery<WireStreamPayload<T, N>>) => void, ) => () => void` | Subscribe to deliveries on a named stream channel. |\n| onDispatchSuppressed | `(info: DispatchSuppressedInfo) => void` | Optional structured observability for the runtime's two dispatch- suppression invariants \u2014 the task-scoped duplicate backstop (`reason: 'duplicate-dispatch'`, fired by `useAction`) and the render-lifetime one-shot guard (`reason: 'one-shot-spent'`, fired by the config's own `dispatch` when a declared `oneShot` action is gestured a second time, ggui#1108). Both fire ALONGSIDE the always-on `console.warn`; neither reaches the agent. Reachability constraint: the first-party renderers do NOT set this field \u2014 `buildRootWireConfig` (`@ggui-ai/iframe-runtime`) and `<GguiRender>`'s internal config both omit it. It fires only when a host hand-builds a complete `WireConfig` (or passes `onDispatchSuppressed` to `buildWireConfig`). On every first-party render path the `console.warn` is the sole suppression signal \u2014 which is what the `oneShot` contract requires as its minimum trace. |\n";
|
|
16764
16996
|
|
|
16765
16997
|
// src/harness/pitfalls.ts
|
|
16766
16998
|
var PITFALLS = [
|
|
@@ -17077,7 +17309,7 @@ var HOST_CLICK_TAGS = /* @__PURE__ */ new Set(["button", "a", "div", "span", "li
|
|
|
17077
17309
|
var HOST_CHANGE_TAGS = /* @__PURE__ */ new Set(["select", "input", "textarea"]);
|
|
17078
17310
|
function findWiring(input) {
|
|
17079
17311
|
const { sourceCode, hookName, hookArg } = input;
|
|
17080
|
-
const sf =
|
|
17312
|
+
const sf = ts4.createSourceFile("Component.tsx", sourceCode, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
|
|
17081
17313
|
const hookBindings = findHookBindings(sf, hookName, hookArg);
|
|
17082
17314
|
if (hookBindings.length === 0) {
|
|
17083
17315
|
return {
|
|
@@ -17100,7 +17332,7 @@ function findWiring(input) {
|
|
|
17100
17332
|
const customPropElements = [];
|
|
17101
17333
|
const submitButton = { found: false };
|
|
17102
17334
|
function visit(node) {
|
|
17103
|
-
if (
|
|
17335
|
+
if (ts4.isJsxAttribute(node) && node.initializer) {
|
|
17104
17336
|
const attrName = node.name.getText(sf);
|
|
17105
17337
|
const initRefs = findReferencedNames(node.initializer, callableNames);
|
|
17106
17338
|
if (initRefs.size > 0) {
|
|
@@ -17138,19 +17370,19 @@ function findWiring(input) {
|
|
|
17138
17370
|
}
|
|
17139
17371
|
}
|
|
17140
17372
|
}
|
|
17141
|
-
if (
|
|
17373
|
+
if (ts4.isJsxOpeningLikeElement(node)) {
|
|
17142
17374
|
const tag = getTagName(node);
|
|
17143
17375
|
if ((tag ?? "").toLowerCase() === "button") {
|
|
17144
17376
|
const typeAttr = node.attributes.properties.find(
|
|
17145
|
-
(p) =>
|
|
17377
|
+
(p) => ts4.isJsxAttribute(p) && p.name.getText(sf) === "type"
|
|
17146
17378
|
);
|
|
17147
|
-
if (typeAttr &&
|
|
17379
|
+
if (typeAttr && ts4.isJsxAttribute(typeAttr) && typeAttr.initializer) {
|
|
17148
17380
|
const v = typeAttr.initializer.getText(sf).toLowerCase();
|
|
17149
17381
|
if (v.includes("submit")) submitButton.found = true;
|
|
17150
17382
|
}
|
|
17151
17383
|
}
|
|
17152
17384
|
}
|
|
17153
|
-
|
|
17385
|
+
ts4.forEachChild(node, visit);
|
|
17154
17386
|
}
|
|
17155
17387
|
visit(sf);
|
|
17156
17388
|
if (sawClickOnHost) {
|
|
@@ -17187,32 +17419,32 @@ function findWiring(input) {
|
|
|
17187
17419
|
function findHookBindings(sf, hookName, hookArg) {
|
|
17188
17420
|
const bindings = [];
|
|
17189
17421
|
function visit(node) {
|
|
17190
|
-
if (
|
|
17422
|
+
if (ts4.isVariableDeclaration(node) && node.initializer && ts4.isCallExpression(node.initializer)) {
|
|
17191
17423
|
const call = node.initializer;
|
|
17192
17424
|
const callee = call.expression.getText(sf);
|
|
17193
17425
|
if (callee !== hookName) {
|
|
17194
|
-
|
|
17426
|
+
ts4.forEachChild(node, visit);
|
|
17195
17427
|
return;
|
|
17196
17428
|
}
|
|
17197
17429
|
const firstArg = call.arguments[0];
|
|
17198
|
-
if (!firstArg || !
|
|
17199
|
-
|
|
17430
|
+
if (!firstArg || !ts4.isStringLiteral(firstArg)) {
|
|
17431
|
+
ts4.forEachChild(node, visit);
|
|
17200
17432
|
return;
|
|
17201
17433
|
}
|
|
17202
17434
|
if (firstArg.text !== hookArg) {
|
|
17203
|
-
|
|
17435
|
+
ts4.forEachChild(node, visit);
|
|
17204
17436
|
return;
|
|
17205
17437
|
}
|
|
17206
|
-
if (
|
|
17438
|
+
if (ts4.isIdentifier(node.name)) {
|
|
17207
17439
|
bindings.push(node.name.text);
|
|
17208
|
-
} else if (
|
|
17440
|
+
} else if (ts4.isObjectBindingPattern(node.name)) {
|
|
17209
17441
|
for (const elem of node.name.elements) {
|
|
17210
17442
|
const target = elem.name;
|
|
17211
|
-
if (
|
|
17443
|
+
if (ts4.isIdentifier(target)) bindings.push(target.text);
|
|
17212
17444
|
}
|
|
17213
17445
|
}
|
|
17214
17446
|
}
|
|
17215
|
-
|
|
17447
|
+
ts4.forEachChild(node, visit);
|
|
17216
17448
|
}
|
|
17217
17449
|
visit(sf);
|
|
17218
17450
|
return bindings;
|
|
@@ -17220,24 +17452,24 @@ function findHookBindings(sf, hookName, hookArg) {
|
|
|
17220
17452
|
function expandAliases(sf, bindings) {
|
|
17221
17453
|
const all = new Set(bindings);
|
|
17222
17454
|
function visit(node) {
|
|
17223
|
-
if (
|
|
17455
|
+
if (ts4.isVariableDeclaration(node) && node.initializer && ts4.isIdentifier(node.name)) {
|
|
17224
17456
|
const newName = node.name.text;
|
|
17225
17457
|
const init = node.initializer;
|
|
17226
|
-
if (
|
|
17458
|
+
if (ts4.isIdentifier(init) && all.has(init.text)) {
|
|
17227
17459
|
all.add(newName);
|
|
17228
17460
|
}
|
|
17229
|
-
if (
|
|
17461
|
+
if (ts4.isArrowFunction(init) || ts4.isFunctionExpression(init)) {
|
|
17230
17462
|
if (bodyReferences(init.body, all)) {
|
|
17231
17463
|
all.add(newName);
|
|
17232
17464
|
}
|
|
17233
17465
|
}
|
|
17234
|
-
if (
|
|
17466
|
+
if (ts4.isCallExpression(init)) {
|
|
17235
17467
|
if (bodyReferences(init, all)) {
|
|
17236
17468
|
all.add(newName);
|
|
17237
17469
|
}
|
|
17238
17470
|
}
|
|
17239
17471
|
}
|
|
17240
|
-
|
|
17472
|
+
ts4.forEachChild(node, visit);
|
|
17241
17473
|
}
|
|
17242
17474
|
visit(sf);
|
|
17243
17475
|
return all;
|
|
@@ -17246,11 +17478,11 @@ function bodyReferences(body, names) {
|
|
|
17246
17478
|
let found = false;
|
|
17247
17479
|
function walk(n) {
|
|
17248
17480
|
if (found) return;
|
|
17249
|
-
if (
|
|
17481
|
+
if (ts4.isIdentifier(n) && names.has(n.text)) {
|
|
17250
17482
|
found = true;
|
|
17251
17483
|
return;
|
|
17252
17484
|
}
|
|
17253
|
-
|
|
17485
|
+
ts4.forEachChild(n, walk);
|
|
17254
17486
|
}
|
|
17255
17487
|
walk(body);
|
|
17256
17488
|
return found;
|
|
@@ -17258,16 +17490,16 @@ function bodyReferences(body, names) {
|
|
|
17258
17490
|
function findReferencedNames(node, names) {
|
|
17259
17491
|
const found = /* @__PURE__ */ new Set();
|
|
17260
17492
|
function walk(n) {
|
|
17261
|
-
if (
|
|
17493
|
+
if (ts4.isIdentifier(n) && names.has(n.text)) {
|
|
17262
17494
|
found.add(n.text);
|
|
17263
17495
|
}
|
|
17264
|
-
|
|
17496
|
+
ts4.forEachChild(n, walk);
|
|
17265
17497
|
}
|
|
17266
17498
|
walk(node);
|
|
17267
17499
|
return found;
|
|
17268
17500
|
}
|
|
17269
17501
|
function getTagName(el) {
|
|
17270
|
-
if (
|
|
17502
|
+
if (ts4.isJsxOpeningLikeElement(el)) {
|
|
17271
17503
|
return el.tagName.getText();
|
|
17272
17504
|
}
|
|
17273
17505
|
return null;
|
|
@@ -18718,7 +18950,7 @@ function getComponentDocumentation(name) {
|
|
|
18718
18950
|
const section = nextMarker === -1 ? PRIMITIVES_DOCUMENTATION.slice(startIdx) : PRIMITIVES_DOCUMENTATION.slice(startIdx, nextMarker);
|
|
18719
18951
|
return section.replace(/\\n/g, "\n").replace(/\\"/g, '"').slice(0, 3e3);
|
|
18720
18952
|
}
|
|
18721
|
-
async function autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode = DEFAULT_DESIGN_MODE) {
|
|
18953
|
+
async function autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode = DEFAULT_DESIGN_MODE, axis) {
|
|
18722
18954
|
const commitStart = Date.now();
|
|
18723
18955
|
const raw = workspace.read();
|
|
18724
18956
|
if (!raw && raw !== "") {
|
|
@@ -18758,12 +18990,25 @@ async function autoCommit(workspace, commitMeta, message, contract, contextPolic
|
|
|
18758
18990
|
gadgetTypes,
|
|
18759
18991
|
designMode
|
|
18760
18992
|
);
|
|
18761
|
-
const
|
|
18993
|
+
const axisFacts = axis !== void 0 ? {
|
|
18994
|
+
sourceCode: formatted,
|
|
18995
|
+
...contract !== void 0 ? { contract } : {},
|
|
18996
|
+
originalPrompt: axis.originalPrompt,
|
|
18997
|
+
classification: axis.classification,
|
|
18998
|
+
designMode,
|
|
18999
|
+
...axis.canvas !== void 0 ? { canvas: axis.canvas } : {}
|
|
19000
|
+
} : void 0;
|
|
19001
|
+
if (axisFacts !== void 0 && !buildSuccess) traceAxisChecksSkipped(axisFacts, "build failed \u2014 no check ran");
|
|
19002
|
+
const axisRun = axis !== void 0 && axisFacts !== void 0 && buildSuccess ? runGatedAxisChecks(axis.checks, { ...axisFacts, compiledCode }) : void 0;
|
|
19003
|
+
const axisIssues = axisRun?.issues ?? [];
|
|
19004
|
+
const checkIssues = [...tier0Issues, ...axisIssues];
|
|
19005
|
+
const tier0Fails = checkIssues.filter((i) => i.result === "fail");
|
|
18762
19006
|
const selfCheckPassed = tier0Fails.length === 0;
|
|
18763
|
-
const violations =
|
|
19007
|
+
const violations = checkIssues.filter((i) => i.result === "fail").map((i) => contextPolicy?.labeledTier0 ? `[${i.priority ?? "P0"}-${i.category}] ${i.description}
|
|
18764
19008
|
Fix: ${i.fix}` : `[${i.category}] ${i.description}
|
|
18765
19009
|
Fix: ${i.fix}`);
|
|
18766
|
-
const
|
|
19010
|
+
const axisWarnings = axisIssues.filter((i) => i.result === "warn").map((i) => `[${i.subcategory}] ${i.description} Fix: ${i.fix}`);
|
|
19011
|
+
const softWarnings = [...getSoftWarnings(raw), ...axisWarnings];
|
|
18767
19012
|
const selfCheckMs = Date.now() - selfCheckStart;
|
|
18768
19013
|
const gitStart = Date.now();
|
|
18769
19014
|
const oid = await workspace.commit(message);
|
|
@@ -18771,7 +19016,7 @@ async function autoCommit(workspace, commitMeta, message, contract, contextPolic
|
|
|
18771
19016
|
const gitMs = Date.now() - gitStart;
|
|
18772
19017
|
const status = buildSuccess && selfCheckPassed ? "PASS" : "FAIL";
|
|
18773
19018
|
console.log(
|
|
18774
|
-
`[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length}`
|
|
19019
|
+
`[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length} | axis=${axisRun === void 0 ? "off" : `${axisIssues.length - axisWarnings.length}F/${axisWarnings.length}W`}`
|
|
18775
19020
|
);
|
|
18776
19021
|
if (violations.length > 0) {
|
|
18777
19022
|
for (const v of violations) {
|
|
@@ -18801,7 +19046,7 @@ ${errors.join("\n")}
|
|
|
18801
19046
|
Fix the issues.`
|
|
18802
19047
|
};
|
|
18803
19048
|
}
|
|
18804
|
-
async function executeTool(workspace, tool, input, commitMeta, contract, applyPatch2, contextPolicy, gadgetTypes, designMode = DEFAULT_DESIGN_MODE) {
|
|
19049
|
+
async function executeTool(workspace, tool, input, commitMeta, contract, applyPatch2, contextPolicy, gadgetTypes, designMode = DEFAULT_DESIGN_MODE, axis) {
|
|
18805
19050
|
switch (tool) {
|
|
18806
19051
|
case "write":
|
|
18807
19052
|
case "rewrite": {
|
|
@@ -18820,7 +19065,7 @@ async function executeTool(workspace, tool, input, commitMeta, contract, applyPa
|
|
|
18820
19065
|
const lineCount = code.split("\n").length;
|
|
18821
19066
|
const message = input.commit_message || `write ${lineCount} lines`;
|
|
18822
19067
|
console.log(`[coding-agent] write: ${lineCount} lines \u2192 auto-commit`);
|
|
18823
|
-
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
|
|
19068
|
+
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode, axis);
|
|
18824
19069
|
}
|
|
18825
19070
|
case "apply_changes": {
|
|
18826
19071
|
const currentFile = workspace.read();
|
|
@@ -18995,7 +19240,7 @@ The file now reflects your latest patch. Submit a follow-up apply_changes target
|
|
|
18995
19240
|
await workspace.stage();
|
|
18996
19241
|
const message = input.commit_message || "apply changes";
|
|
18997
19242
|
console.log(`[coding-agent] apply_changes: ${changes.length} changes applied \u2192 auto-commit`);
|
|
18998
|
-
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
|
|
19243
|
+
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode, axis);
|
|
18999
19244
|
}
|
|
19000
19245
|
case "get_components_info": {
|
|
19001
19246
|
const names = input.names ?? [];
|
|
@@ -19445,7 +19690,14 @@ ${iconNamesCache}`;
|
|
|
19445
19690
|
harness.what.gadgetTypes,
|
|
19446
19691
|
// Which tier-0 legs fire — the design-vocabulary legs stand down in
|
|
19447
19692
|
// `free` mode; every contract leg runs in both.
|
|
19448
|
-
harness.designMode
|
|
19693
|
+
harness.designMode,
|
|
19694
|
+
// ggui#1122: the harness's pre-filtered axis checks run at auto-commit — the serve path.
|
|
19695
|
+
{
|
|
19696
|
+
checks: harness.check.axisChecks,
|
|
19697
|
+
classification: harness.classification,
|
|
19698
|
+
originalPrompt: ctx.originalPrompt,
|
|
19699
|
+
...harness.canvas !== void 0 ? { canvas: harness.canvas } : {}
|
|
19700
|
+
}
|
|
19449
19701
|
);
|
|
19450
19702
|
const toolMs = Date.now() - toolStart;
|
|
19451
19703
|
if (forceEscape && ctx.dupeBreak && call.name === "apply_changes") {
|
|
@@ -20247,6 +20499,7 @@ function createGenerateTaskRunner(input) {
|
|
|
20247
20499
|
systemPrompt: session.systemPrompt,
|
|
20248
20500
|
harness: session.harness,
|
|
20249
20501
|
contract: params.contract,
|
|
20502
|
+
originalPrompt: params.userPrompt,
|
|
20250
20503
|
commitMeta: session.commitMeta,
|
|
20251
20504
|
originalProps: session.originalProps,
|
|
20252
20505
|
costTracker: session.costTracker,
|
|
@@ -21010,6 +21263,10 @@ interface Props ${tsInterface}
|
|
|
21010
21263
|
if (entry.nextStep) {
|
|
21011
21264
|
line += `
|
|
21012
21265
|
Next-step hint: agent intends to call \`${entry.nextStep}\``;
|
|
21266
|
+
}
|
|
21267
|
+
if (entry.confirm === true) {
|
|
21268
|
+
line += `
|
|
21269
|
+
Author flag: \`confirm\` \u2014 the author marked this action as one to confirm with the user before it fires.`;
|
|
21013
21270
|
}
|
|
21014
21271
|
return line;
|
|
21015
21272
|
}).join("\n");
|