@ggui-ai/ui-gen 0.14.0 → 0.16.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 +21 -4
- package/dist/adapters/generation-dispatch.js +915 -351
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +28 -23
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +17 -12
- package/dist/advanced/index.js +968 -375
- package/dist/advanced/index.js.map +1 -1
- package/dist/boilerplate.d.ts +16 -1
- package/dist/boilerplate.js +579 -245
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +17 -3
- package/dist/check/index.js +34 -26
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-D6TKuv9V.d.ts → create-ui-generator-CCIeQaEe.d.ts} +77 -10
- package/dist/design-mode-BHEE7flj.d.ts +67 -0
- package/dist/{dispatch-BuZfTh9r.d.ts → dispatch-BA5dJcg-.d.ts} +4 -1
- package/dist/evaluation/axis-checks/index.d.ts +4 -3
- package/dist/evaluation/axis-checks/index.js +3 -1
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +2 -1
- package/dist/evaluation/axis-checks/registry.js +1 -0
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +143 -3
- package/dist/evaluation/index.js +3866 -92
- package/dist/evaluation/index.js.map +1 -1
- package/dist/{generate-BTfvELgl.d.ts → generate-8L_bc2iP.d.ts} +10 -1
- package/dist/harness/check/runtime-render/index.d.ts +2 -1
- package/dist/harness/index.d.ts +2 -1
- package/dist/harness/index.js +589 -248
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +3 -3
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -1
- package/dist/harness/prompts.js +3 -0
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +17 -5
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +19 -3
- package/dist/harness/runtime.js +602 -261
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +28 -1
- package/dist/harness/types-public.js +589 -248
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +2 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +975 -373
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +2 -1
- package/dist/{prompts-BVhtH3M1.d.ts → prompts-k6m6reqe.d.ts} +11 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/{types-public-CkNWEov9.d.ts → types-public-D24JLJgl.d.ts} +49 -5
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +4 -4
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +2 -1
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +57 -4
- package/src/adapters/provider-router.ts +7 -6
- package/src/adapters/tools.ts +5 -5
- package/src/advanced/generator.ts +11 -10
- package/src/advanced/index.ts +5 -4
- package/src/boilerplate/free-design-prompt.ts +329 -0
- package/src/boilerplate/generate.ts +10 -1
- package/src/boilerplate/hard-sections.ts +242 -0
- package/src/boilerplate/render.ts +32 -8
- package/src/boilerplate/system-prompt.ts +81 -229
- package/src/boilerplate/templates/base-free.tsx.tmpl +24 -0
- package/src/check/run-tier0.ts +80 -20
- package/src/check/type-checker.ts +12 -2
- package/src/coding-agent/tools.ts +8 -2
- package/src/contract-context.ts +28 -0
- package/src/create-harness.ts +11 -0
- package/src/create-ui-generator.ts +71 -25
- package/src/design-mode.ts +182 -0
- package/src/design-system-docs.ts +17 -13
- package/src/evaluation/axis-checks/dispatch.ts +4 -0
- package/src/evaluation/axis-checks/extras.ts +5 -0
- package/src/evaluation/cost-tracker.ts +2 -3
- package/src/evaluation/index.ts +46 -2
- package/src/evaluation/llm-evaluator.ts +77 -21
- package/src/evaluation/types-public.ts +103 -8
- package/src/evaluation/visual-evaluator.ts +301 -29
- package/src/harness/coding/dupe-break.ts +0 -0
- package/src/harness/coding/init-session.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +3 -0
- package/src/harness/coding/run-eval-round.ts +15 -2
- package/src/harness/llm-trace-sink.ts +3 -3
- package/src/harness/result-types.ts +15 -3
- package/src/harness/runtime.ts +18 -0
- package/src/harness/types-public.ts +27 -0
- package/src/index.ts +26 -0
- package/src/internal/extract-call-signatures.ts +2 -2
- package/src/providers/bedrock.ts +4 -4
- package/src/run-check.ts +1 -0
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/validation/allowed-imports.ts +8 -2
- package/src/validation/primitives.ts +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import 'zod';
|
|
3
|
+
import { getCssTokens, FORBIDDEN_IMPORT_SPECIFIERS } from '@ggui-ai/design/rendering';
|
|
3
4
|
import { classifyUi, MODEL_REGISTRY, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
|
|
4
5
|
import * as esbuild from 'esbuild';
|
|
5
6
|
import { build } from 'esbuild';
|
|
@@ -18,7 +19,6 @@ import { tmpdir } from 'os';
|
|
|
18
19
|
import { Readable } from 'stream';
|
|
19
20
|
import { pipeline } from 'stream/promises';
|
|
20
21
|
import { createBrotliDecompress, createUnzip } from 'zlib';
|
|
21
|
-
import { getCssTokens } from '@ggui-ai/design/rendering';
|
|
22
22
|
import git from 'isomorphic-git';
|
|
23
23
|
import { Volume, createFsFromVolume } from 'memfs';
|
|
24
24
|
import { createPatch, applyPatch } from 'diff';
|
|
@@ -62,6 +62,73 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
62
62
|
mod
|
|
63
63
|
));
|
|
64
64
|
|
|
65
|
+
// src/design-mode.ts
|
|
66
|
+
function canvasDisplayModeForShell(shell) {
|
|
67
|
+
return shell === "chat" || shell === "inline" ? "inline" : "fullscreen";
|
|
68
|
+
}
|
|
69
|
+
function canvasForRendering(shellType, screen) {
|
|
70
|
+
const shell = shellType ?? "fullscreen";
|
|
71
|
+
if (canvasDisplayModeForShell(shell) === "inline") return "xs-chat-card";
|
|
72
|
+
if (shell === "spatial") return "md";
|
|
73
|
+
switch (screen ?? "universal") {
|
|
74
|
+
case "mobile":
|
|
75
|
+
return "mobile-fullscreen-small";
|
|
76
|
+
case "tablet":
|
|
77
|
+
return "md";
|
|
78
|
+
default:
|
|
79
|
+
return "lg";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function describeCanvas(canvas) {
|
|
83
|
+
const d = CANVAS_DESCRIPTORS[canvas];
|
|
84
|
+
return `This component renders on the \`${canvas}\` canvas (${d.label}): ${d.minWidthPx}\u2013${d.maxWidthPx}px wide, ${d.host}. Design for the whole range \u2014 the host may size the iframe anywhere inside it.`;
|
|
85
|
+
}
|
|
86
|
+
var DEFAULT_DESIGN_MODE, CANVAS_VIEWPORTS, CANVAS_DESCRIPTORS;
|
|
87
|
+
var init_design_mode = __esm({
|
|
88
|
+
"src/design-mode.ts"() {
|
|
89
|
+
DEFAULT_DESIGN_MODE = "constrained";
|
|
90
|
+
CANVAS_VIEWPORTS = {
|
|
91
|
+
"xs-chat-card": { width: 400, height: 640 },
|
|
92
|
+
"mobile-fullscreen-small": { width: 390, height: 844 },
|
|
93
|
+
md: { width: 768, height: 1024 },
|
|
94
|
+
lg: { width: 1024, height: 768 },
|
|
95
|
+
xl: { width: 1440, height: 900 }
|
|
96
|
+
};
|
|
97
|
+
CANVAS_DESCRIPTORS = {
|
|
98
|
+
"xs-chat-card": {
|
|
99
|
+
label: "extra-small chat card",
|
|
100
|
+
minWidthPx: 320,
|
|
101
|
+
maxWidthPx: 480,
|
|
102
|
+
host: "an inline card inside a chat message bubble \u2014 the host draws the card border, shadow and rounded corners; the component fills the bubble width and takes its natural height"
|
|
103
|
+
},
|
|
104
|
+
"mobile-fullscreen-small": {
|
|
105
|
+
label: "small mobile fullscreen",
|
|
106
|
+
minWidthPx: 320,
|
|
107
|
+
maxWidthPx: 599,
|
|
108
|
+
host: "a phone-sized viewport the component owns edge to edge \u2014 single column, touch targets at least 44px, the component manages its own scroll"
|
|
109
|
+
},
|
|
110
|
+
md: {
|
|
111
|
+
label: "medium panel",
|
|
112
|
+
minWidthPx: 600,
|
|
113
|
+
maxWidthPx: 1023,
|
|
114
|
+
host: "a tablet viewport or a floating panel (spatial / picture-in-picture) \u2014 one or two columns, medium density"
|
|
115
|
+
},
|
|
116
|
+
lg: {
|
|
117
|
+
label: "large fullscreen",
|
|
118
|
+
minWidthPx: 1024,
|
|
119
|
+
maxWidthPx: 1439,
|
|
120
|
+
host: "a desktop viewport the component owns edge to edge \u2014 multi-column layouts, hover states and pointer interactions are available"
|
|
121
|
+
},
|
|
122
|
+
xl: {
|
|
123
|
+
label: "extra-large fullscreen",
|
|
124
|
+
minWidthPx: 1440,
|
|
125
|
+
maxWidthPx: 1920,
|
|
126
|
+
host: "a wide desktop viewport the component owns edge to edge \u2014 cap content width or use the extra room deliberately; never stretch a single column across the whole width"
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
65
132
|
// src/boilerplate/json-schema-ts.ts
|
|
66
133
|
function jsonSchemaTypeToTs(schema) {
|
|
67
134
|
const unionMembers = schema.oneOf ?? schema.anyOf;
|
|
@@ -166,17 +233,23 @@ function priorityForIssue(category) {
|
|
|
166
233
|
function getActionableIssues(result, mode) {
|
|
167
234
|
return result.issues.filter((i) => i.result === "fail" || i.result === "warn");
|
|
168
235
|
}
|
|
169
|
-
function
|
|
170
|
-
|
|
236
|
+
function criteriaFor(designMode = "constrained") {
|
|
237
|
+
if (designMode === "constrained") return CRITERIA;
|
|
238
|
+
return CRITERIA.map(
|
|
239
|
+
(c) => FREE_DESIGN_CRITERIA_OVERRIDES.find((o) => o.id === c.id) ?? c
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
function getCriteriaByPriority(priority, designMode = "constrained") {
|
|
243
|
+
return criteriaFor(designMode).filter((c) => c.priority === priority);
|
|
171
244
|
}
|
|
172
|
-
function getCriterionById(id) {
|
|
173
|
-
return
|
|
245
|
+
function getCriterionById(id, designMode = "constrained") {
|
|
246
|
+
return criteriaFor(designMode).find((c) => c.id === id);
|
|
174
247
|
}
|
|
175
|
-
function buildCodingCriteriaSummary() {
|
|
248
|
+
function buildCodingCriteriaSummary(designMode = "constrained") {
|
|
176
249
|
const lines = ["## Priority (P0 first, then P1, then P2)", ""];
|
|
177
250
|
for (const priority of ["P0", "P1", "P2"]) {
|
|
178
251
|
const label = priority === "P0" ? "Must (compile + complete)" : priority === "P1" ? "Should (safety)" : "Nice (quality)";
|
|
179
|
-
const criteria = getCriteriaByPriority(priority);
|
|
252
|
+
const criteria = getCriteriaByPriority(priority, designMode);
|
|
180
253
|
lines.push(`**${priority} \u2014 ${label}:**`);
|
|
181
254
|
for (const c of criteria) {
|
|
182
255
|
lines.push(`- ${c.codingGuidance}`);
|
|
@@ -185,7 +258,7 @@ function buildCodingCriteriaSummary() {
|
|
|
185
258
|
}
|
|
186
259
|
return lines.join("\n");
|
|
187
260
|
}
|
|
188
|
-
var LLM_EVAL_STATIC_CRITERIA, CRITERIA;
|
|
261
|
+
var LLM_EVAL_STATIC_CRITERIA, CRITERIA, FREE_DESIGN_CRITERIA_OVERRIDES;
|
|
189
262
|
var init_types_public = __esm({
|
|
190
263
|
"src/evaluation/types-public.ts"() {
|
|
191
264
|
LLM_EVAL_STATIC_CRITERIA = [
|
|
@@ -374,6 +447,31 @@ Intentional custom colors (status indicators) are acceptable when no semantic to
|
|
|
374
447
|
Only list ACTUAL violations. Use 'fail' only for pervasive violations.`
|
|
375
448
|
}
|
|
376
449
|
];
|
|
450
|
+
FREE_DESIGN_CRITERIA_OVERRIDES = [
|
|
451
|
+
{
|
|
452
|
+
id: "tokens",
|
|
453
|
+
name: "Color tokens",
|
|
454
|
+
priority: "P1",
|
|
455
|
+
tier: 0,
|
|
456
|
+
failOutcome: "warn",
|
|
457
|
+
codingGuidance: "Every color is a bare var(--ggui-color-*) from the token manifest \u2014 no hex, rgb()/hsl(), CSS named colors or literal fallbacks, in style props and <style> blocks alike. Spacing, typography, radius and shadow geometry may be literals.",
|
|
458
|
+
evalInstruction: "Flag hardcoded hex colors, rgba/hsl functions, CSS named colors, and literal fallbacks inside var(--ggui-*) references. Do NOT flag literal spacing, font sizes, radii or pixel values \u2014 they are allowed in this mode."
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
id: "visual",
|
|
462
|
+
name: "Visual composition",
|
|
463
|
+
priority: "P2",
|
|
464
|
+
tier: 2,
|
|
465
|
+
failOutcome: "warn",
|
|
466
|
+
codingGuidance: "Compose deliberately: one clear focal point, consistent spacing rhythm, a coherent theme-token palette, a fit to the rendering canvas.",
|
|
467
|
+
evalInstruction: `Evaluate VISUAL COMPOSITION: Is the component composed deliberately for its canvas?
|
|
468
|
+
|
|
469
|
+
Judge: visual hierarchy (one clear focal point, headings distinct from body text), consistent spacing rhythm and alignment, a coherent palette drawn from the theme tokens, fit to the canvas (fluid width, no fixed widths, nothing cramped or stretched).
|
|
470
|
+
Do NOT reward or penalise the use of design-system primitives \u2014 raw HTML with inline CSS is a first-class choice here. Literal spacing / typography / radius values are fine.
|
|
471
|
+
|
|
472
|
+
Only list ACTUAL composition problems. Use 'fail' only for a fundamentally incoherent composition.`
|
|
473
|
+
}
|
|
474
|
+
];
|
|
377
475
|
}
|
|
378
476
|
});
|
|
379
477
|
|
|
@@ -2084,7 +2182,7 @@ var init_policy2 = __esm({
|
|
|
2084
2182
|
var PRIMITIVES_DOCUMENTATION, VALID_PRIMITIVES;
|
|
2085
2183
|
var init_primitives = __esm({
|
|
2086
2184
|
"src/validation/primitives.ts"() {
|
|
2087
|
-
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-surface)`\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' \\| '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-surface`); pair with `shadow=\"md\"\\|\"lg\"` for elevated cards, or use `'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' \\| '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). Pair with `'inverse'` for legible text. - `'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).\n\nDefault text color: `var(--ggui-color-onSurface)`.\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-onSurface))` | 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), `'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\">Account</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-onSurface)`.\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-onSurface))` | 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-surfaceVariant)` background, `var(--ggui-color-onSurfaceVariant)` text, no border - `'outline'` -- transparent background, `1px solid var(--ggui-color-primary-600)` border, primary-600 text - `'ghost'` -- transparent background, `var(--ggui-color-onSurfaceVariant)` 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-surface)` (normal),\n `var(--ggui-color-surface)` (disabled)\n- Text: `var(--ggui-color-onSurface)`\n- Border radius: `var(--ggui-shape-radius-md)`\n- Label: `var(--ggui-font-size-sm)`, `var(--ggui-font-weight-medium)`,\n `var(--ggui-color-onSurfaceVariant)`\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-onSurfaceVariant)`. |\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-onSurfaceVariant)`. 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-surface)`. |\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-surface)` (normal),\n `var(--ggui-color-surface)` (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-onSurfaceVariant)`. |\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-onSurfaceVariant)`. 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-surface)`. |\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-surface)` (normal),\n `var(--ggui-color-surface)` (disabled)\n- Text: `var(--ggui-color-onSurface)` when a value is selected,\n `var(--ggui-color-onSurfaceVariant)` 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-onSurfaceVariant)`. |\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-onSurfaceVariant)`. 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-surface)` 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-surface)` (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-onSurfaceVariant)`\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-onSurfaceVariant)`. |\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-onSurfaceVariant)`). |\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-onSurfaceVariant)`. |\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-surface)` 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-onSurfaceVariant)`. |\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-onSurfaceVariant)`. |\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'` | `'default'` | Visual style. Maps to background/text color pairings: - `'default'` -- bg `var(--ggui-color-surfaceVariant)`, text `var(--ggui-color-onSurfaceVariant)` - `'primary'` -- bg `var(--ggui-color-primary-100)`, text `var(--ggui-color-primary-700)` - `'secondary'` -- bg `var(--ggui-color-outlineVariant)`, text `var(--ggui-color-onSurface)` - `'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 -- 185 Lucide icons + emoji passthrough.\n\nThree resolution layers:\n1. **Lucide icon:** pass any common Lucide icon name (e.g. `sun`, `cloud-rain`, `heart`, `shopping-cart`).\n Accepts kebab-case, camelCase, or PascalCase. Renders as stroke SVG.\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 (kebab-case, camelCase, or PascalCase all work). 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-onSurface)`\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-surfaceVariant)`).\nHover: `var(--ggui-color-surface)` background with 150ms ease transition.\nStriped: alternating rows (odd index) get `var(--ggui-color-surface)`.\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-surface)` background. |\n| hoverable | `boolean` | `true` | When true, rows highlight with `var(--ggui-color-surface)` 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-surfaceVariant)`. 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-onSurfaceVariant)`. |\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-onSurface)`.\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-onSurfaceVariant)`. |\n| description | `string` | Optional secondary description rendered below the label in smaller, muted type (`var(--ggui-font-size-xs)`, `var(--ggui-color-onSurfaceVariant)`). |\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-onSurfaceVariant)`, 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-onSurfaceVariant)`, `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().";
|
|
2185
|
+
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' \\| '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, or use `'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' \\| '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). Pair with `'inverse'` for legible text. - `'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).\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), `'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\">Account</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, primary-600 text - `'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 -- 185 Lucide icons + emoji passthrough.\n\nThree resolution layers:\n1. **Lucide icon:** pass any common Lucide icon name (e.g. `sun`, `cloud-rain`, `heart`, `shopping-cart`).\n Accepts kebab-case, camelCase, or PascalCase. Renders as stroke SVG.\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 (kebab-case, camelCase, or PascalCase all work). 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().";
|
|
2088
2186
|
VALID_PRIMITIVES = [
|
|
2089
2187
|
"Container",
|
|
2090
2188
|
"Card",
|
|
@@ -2148,16 +2246,15 @@ var init_primitives = __esm({
|
|
|
2148
2246
|
];
|
|
2149
2247
|
}
|
|
2150
2248
|
});
|
|
2151
|
-
|
|
2152
|
-
// src/validation/allowed-imports.ts
|
|
2153
2249
|
function isAllowedImport(specifier, gadgetPackages) {
|
|
2250
|
+
if (FORBIDDEN_IMPORT_SPECIFIERS.includes(specifier)) return false;
|
|
2154
2251
|
for (const base of ALLOWED_IMPORT_BASES) {
|
|
2155
2252
|
if (specifier === base || specifier.startsWith(`${base}/`)) return true;
|
|
2156
2253
|
}
|
|
2157
2254
|
return gadgetPackages?.has(specifier) ?? false;
|
|
2158
2255
|
}
|
|
2159
2256
|
function describeAllowedImports() {
|
|
2160
|
-
return "react, @ggui-ai/design, @ggui-ai/wire, @ggui-ai/gadgets, or a gadget package declared on the contract";
|
|
2257
|
+
return "react, @ggui-ai/design, @ggui-ai/wire, @ggui-ai/gadgets, or a gadget package declared on the contract \u2014 never @ggui-ai/wire/internal (the writer side of the wire; components read useRender().isConnected)";
|
|
2161
2258
|
}
|
|
2162
2259
|
var ALLOWED_IMPORT_BASES;
|
|
2163
2260
|
var init_allowed_imports = __esm({
|
|
@@ -3202,7 +3299,8 @@ async function loadVfs() {
|
|
|
3202
3299
|
function resolveRelativeInVfs(vfs, containingFile, importPath) {
|
|
3203
3300
|
const normalized = containingFile.startsWith("/") ? containingFile.slice(1) : containingFile;
|
|
3204
3301
|
const dir = path.posix.dirname(normalized);
|
|
3205
|
-
const
|
|
3302
|
+
const specifier = importPath.replace(/\.(?:m|c)?js$/, "");
|
|
3303
|
+
const resolved2 = path.posix.normalize(`${dir}/${specifier}`);
|
|
3206
3304
|
const withDts = `${resolved2}.d.ts`;
|
|
3207
3305
|
if (vfs.has(withDts)) return withDts;
|
|
3208
3306
|
const withIndexDts = `${resolved2}/index.d.ts`;
|
|
@@ -3562,10 +3660,11 @@ function stripComments(code) {
|
|
|
3562
3660
|
}
|
|
3563
3661
|
return out;
|
|
3564
3662
|
}
|
|
3565
|
-
function detectCertainDoubleWiredActions(sourceCode, actionBindings) {
|
|
3663
|
+
function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode = DEFAULT_DESIGN_MODE) {
|
|
3566
3664
|
const issues = [];
|
|
3567
3665
|
if (actionBindings.length === 0) return issues;
|
|
3568
3666
|
const bindings = new Set(actionBindings);
|
|
3667
|
+
const anyElementIsInteractive = designMode === "free";
|
|
3569
3668
|
const TRAIT_HOST_TAGS = /* @__PURE__ */ new Set(["Card", "Box", "Stack", "Row"]);
|
|
3570
3669
|
const TRAITS_THAT_FIRE = /* @__PURE__ */ new Set(["Clickable", "Pressable"]);
|
|
3571
3670
|
const INTERACTIVE_DESCENDANTS = /* @__PURE__ */ new Set([
|
|
@@ -3601,7 +3700,9 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings) {
|
|
|
3601
3700
|
}
|
|
3602
3701
|
function isTraitHostWithFiringAs(node) {
|
|
3603
3702
|
const tag = tagNameText(node);
|
|
3604
|
-
if (tag === void 0
|
|
3703
|
+
if (tag === void 0) return false;
|
|
3704
|
+
if (anyElementIsInteractive) return true;
|
|
3705
|
+
if (!TRAIT_HOST_TAGS.has(tag)) return false;
|
|
3605
3706
|
const asExpr = attrExpression(node, "as");
|
|
3606
3707
|
return asExpr !== void 0 && ts2.isIdentifier(asExpr) && TRAITS_THAT_FIRE.has(asExpr.text);
|
|
3607
3708
|
}
|
|
@@ -3630,7 +3731,7 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings) {
|
|
|
3630
3731
|
const opening = ts2.isJsxElement(node) ? node.openingElement : ts2.isJsxSelfClosingElement(node) ? node : void 0;
|
|
3631
3732
|
if (opening !== void 0) {
|
|
3632
3733
|
const tag = tagNameText(opening);
|
|
3633
|
-
if (tag !== void 0 && INTERACTIVE_DESCENDANTS.has(tag)) {
|
|
3734
|
+
if (tag !== void 0 && (anyElementIsInteractive || INTERACTIVE_DESCENDANTS.has(tag))) {
|
|
3634
3735
|
for (const attrName of HANDLER_ATTRS) {
|
|
3635
3736
|
const callee = extractCalleeName(attrExpression(opening, attrName));
|
|
3636
3737
|
if (callee === expectedBinding) {
|
|
@@ -3656,14 +3757,15 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings) {
|
|
|
3656
3757
|
if (match !== void 0) {
|
|
3657
3758
|
const outerTag = tagNameText(node.openingElement);
|
|
3658
3759
|
const outerLine = sf.getLineAndCharacterOfPosition(node.openingElement.getStart()).line + 1;
|
|
3760
|
+
const outerLabel = anyElementIsInteractive ? `<${outerTag}>` : `<${outerTag} as={...}>`;
|
|
3659
3761
|
issues.push({
|
|
3660
3762
|
tier: 0,
|
|
3661
3763
|
result: "fail",
|
|
3662
3764
|
category: "interactivity",
|
|
3663
3765
|
subcategory: "double-wired-action:certain",
|
|
3664
3766
|
severity: "critical",
|
|
3665
|
-
description: `Nested-interactive double-wire: outer
|
|
3666
|
-
fix: `Pick ONE surface for the gesture: either drop \`as={...}\` + onClick on the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR remove the inner <${match.tag}> and let the outer <${outerTag} as={...}> own it. Don't wire both to the same useAction binding.`,
|
|
3767
|
+
description: `Nested-interactive double-wire: outer ${outerLabel} at line ${outerLine} and inner <${match.tag}> at line ${match.line} both dispatch the same useAction binding '${outerCallee}'. One user click on the inner control fires its handler AND bubbles to the outer handler \u2014 '${outerCallee}' dispatches TWICE, the action runs back-to-back, and a toggle-style action silently reverts the user's change.`,
|
|
3768
|
+
fix: anyElementIsInteractive ? `Pick ONE gesture surface for '${outerCallee}': either remove the onClick from the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR drop the inner <${match.tag}>'s handler and let the outer <${outerTag}> own it. Never wire both to the same useAction binding \u2014 stopPropagation is not a fix.` : `Pick ONE surface for the gesture: either drop \`as={...}\` + onClick on the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR remove the inner <${match.tag}> and let the outer <${outerTag} as={...}> own it. Don't wire both to the same useAction binding.`,
|
|
3667
3769
|
line: outerLine
|
|
3668
3770
|
});
|
|
3669
3771
|
}
|
|
@@ -3688,9 +3790,10 @@ function isGadgetExportImported(source, pkg, exportName) {
|
|
|
3688
3790
|
}
|
|
3689
3791
|
return false;
|
|
3690
3792
|
}
|
|
3691
|
-
async function runTier0Checks(sourceCode, contract, buildErrors, gadgetTypes) {
|
|
3793
|
+
async function runTier0Checks(sourceCode, contract, buildErrors, gadgetTypes, designMode = DEFAULT_DESIGN_MODE) {
|
|
3692
3794
|
const issues = [];
|
|
3693
3795
|
const lines = sourceCode.split("\n");
|
|
3796
|
+
const enforceDesignVocabulary = designMode === "constrained";
|
|
3694
3797
|
if (buildErrors && buildErrors.length > 0) {
|
|
3695
3798
|
const errorDetail = buildErrors.map((e) => {
|
|
3696
3799
|
const match = e.match(/<stdin>:(\d+):\d+: ERROR: (.+)/);
|
|
@@ -3819,7 +3922,7 @@ ${errorDetail}`,
|
|
|
3819
3922
|
subcategory: "hardcoded-color-fn",
|
|
3820
3923
|
severity: "critical",
|
|
3821
3924
|
description: `Hardcoded ${colorFnMatch[1]}() breaks theme switching \u2014 use design tokens.`,
|
|
3822
|
-
fix: "Replace with a primitive variant OR a
|
|
3925
|
+
fix: "Replace with a primitive variant OR a layering role: var(--ggui-color-container), var(--ggui-color-onContainer), var(--ggui-color-outline), etc.",
|
|
3823
3926
|
line: lineNum
|
|
3824
3927
|
});
|
|
3825
3928
|
}
|
|
@@ -3836,13 +3939,13 @@ ${errorDetail}`,
|
|
|
3836
3939
|
subcategory: "off-manifest-token",
|
|
3837
3940
|
severity: "critical",
|
|
3838
3941
|
description: `Token "${name}" is not in the design system's token manifest \u2014 no theme defines it, so it renders unset.`,
|
|
3839
|
-
fix: `Choose a token that exists: the closed vocabulary is the manifest the design-system docs list (colors: var(--ggui-color-primary-500), var(--ggui-color-
|
|
3942
|
+
fix: `Choose a token that exists: the closed vocabulary is the manifest the design-system docs list (colors: var(--ggui-color-primary-500), var(--ggui-color-onContainer), \u2026; spacing: var(--ggui-spacing-4); type: var(--ggui-font-size-sm)). Never invent a name and never add a literal fallback.`,
|
|
3840
3943
|
line: lineNum
|
|
3841
3944
|
});
|
|
3842
3945
|
}
|
|
3843
|
-
const rawSpacingMatch = line.match(
|
|
3946
|
+
const rawSpacingMatch = enforceDesignVocabulary ? line.match(
|
|
3844
3947
|
/\b(gap|padding|paddingX|paddingY|margin|radius)\s*=\s*["'][\d.]+(?:px|rem|em)["']/
|
|
3845
|
-
);
|
|
3948
|
+
) : null;
|
|
3846
3949
|
if (rawSpacingMatch) {
|
|
3847
3950
|
issues.push({
|
|
3848
3951
|
tier: 0,
|
|
@@ -3936,12 +4039,12 @@ ${errorDetail}`,
|
|
|
3936
4039
|
subcategory: "named-color",
|
|
3937
4040
|
severity: "critical",
|
|
3938
4041
|
description: `Hardcoded CSS named color "${named}" breaks theme switching \u2014 use design tokens.`,
|
|
3939
|
-
fix: `Replace with a typed slot (Text tone="muted" / Box surface="accent" / Badge variant="success") OR a semantic token: var(--ggui-color-
|
|
4042
|
+
fix: `Replace with a typed slot (Text tone="muted" / Box surface="accent" / Badge variant="success") OR a semantic token: var(--ggui-color-onSunken), var(--ggui-color-primary-500), etc. The keyword "${named}" maps to a fixed RGB; the operator's theme has no effect on it.`,
|
|
3940
4043
|
line: lineNum
|
|
3941
4044
|
});
|
|
3942
4045
|
}
|
|
3943
4046
|
}
|
|
3944
|
-
const pxMatch = line.match(/(?:padding|margin|gap|borderRadius)\s*:\s*['"]?\d+px/);
|
|
4047
|
+
const pxMatch = enforceDesignVocabulary ? line.match(/(?:padding|margin|gap|borderRadius)\s*:\s*['"]?\d+px/) : null;
|
|
3945
4048
|
if (pxMatch && !line.includes("var(--ggui-")) {
|
|
3946
4049
|
issues.push({
|
|
3947
4050
|
tier: 0,
|
|
@@ -3949,11 +4052,11 @@ ${errorDetail}`,
|
|
|
3949
4052
|
category: "tokens",
|
|
3950
4053
|
subcategory: "raw-pixels",
|
|
3951
4054
|
description: "Raw pixel value in spacing \u2014 must use design tokens",
|
|
3952
|
-
fix:
|
|
4055
|
+
fix: 'Replace with a bare var(--ggui-spacing-*) token \u2014 no literal fallback (the runtime injects every token) \u2014 or the scale name on the primitive prop (gap="md", padding="lg")',
|
|
3953
4056
|
line: lineNum
|
|
3954
4057
|
});
|
|
3955
4058
|
}
|
|
3956
|
-
const numericSpacingMatch = line.match(/\b(padding|paddingX|paddingY|gap|margin)\s*=\s*\{?\s*(\d+)\s*\}?/);
|
|
4059
|
+
const numericSpacingMatch = enforceDesignVocabulary ? line.match(/\b(padding|paddingX|paddingY|gap|margin)\s*=\s*\{?\s*(\d+)\s*\}?/) : null;
|
|
3957
4060
|
if (numericSpacingMatch && !line.includes("var(--ggui-")) {
|
|
3958
4061
|
issues.push({
|
|
3959
4062
|
tier: 0,
|
|
@@ -3967,7 +4070,7 @@ ${errorDetail}`,
|
|
|
3967
4070
|
}
|
|
3968
4071
|
}
|
|
3969
4072
|
const clickablePattern = /<(?:Card|Box|Stack|Row)\s[^>]*onClick/;
|
|
3970
|
-
for (let i = 0; i < lines.length; i++) {
|
|
4073
|
+
for (let i = 0; enforceDesignVocabulary && i < lines.length; i++) {
|
|
3971
4074
|
if (clickablePattern.test(lines[i]) && !lines[i].includes("as={Clickable}") && !lines[i].includes("as={Pressable}")) {
|
|
3972
4075
|
issues.push({
|
|
3973
4076
|
tier: 0,
|
|
@@ -3980,9 +4083,8 @@ ${errorDetail}`,
|
|
|
3980
4083
|
});
|
|
3981
4084
|
}
|
|
3982
4085
|
}
|
|
3983
|
-
const
|
|
3984
|
-
|
|
3985
|
-
].map((m) => m[1]).filter((name) => typeof name === "string");
|
|
4086
|
+
const bindingRe = designMode === "free" ? /(?:const|let)\s+(\w+)\s*=\s*useAction\s*(?:<[^>]*>)?\s*\(/g : /(?:const|let)\s+(\w+)\s*=\s*useAction\s*\(/g;
|
|
4087
|
+
const actionBindings = [...sourceCode.matchAll(bindingRe)].map((m) => m[1]).filter((name) => typeof name === "string");
|
|
3986
4088
|
for (const binding of actionBindings) {
|
|
3987
4089
|
const callSites = (sourceCode.match(new RegExp(`\\b${binding}\\s*\\(`, "g")) ?? []).length;
|
|
3988
4090
|
if (callSites >= 2) {
|
|
@@ -3998,7 +4100,7 @@ ${errorDetail}`,
|
|
|
3998
4100
|
}
|
|
3999
4101
|
if (actionBindings.length > 0) {
|
|
4000
4102
|
issues.push(
|
|
4001
|
-
...detectCertainDoubleWiredActions(sourceCode, actionBindings)
|
|
4103
|
+
...detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode)
|
|
4002
4104
|
);
|
|
4003
4105
|
}
|
|
4004
4106
|
if (/\buseRender\s*\(/.test(sourceCode)) {
|
|
@@ -4043,7 +4145,7 @@ ${errorDetail}`,
|
|
|
4043
4145
|
const ALIGN_VALUES = /* @__PURE__ */ new Set(["start", "center", "end", "stretch"]);
|
|
4044
4146
|
const JUSTIFY_VALUES = /* @__PURE__ */ new Set(["start", "center", "end", "between", "around", "evenly"]);
|
|
4045
4147
|
const tagRegex = /<(Stack|Row)\b([^>]*)>/gs;
|
|
4046
|
-
for (const tag of sourceCode.matchAll(tagRegex)) {
|
|
4148
|
+
for (const tag of enforceDesignVocabulary ? sourceCode.matchAll(tagRegex) : []) {
|
|
4047
4149
|
const tagName = tag[1];
|
|
4048
4150
|
const attrs = tag[2];
|
|
4049
4151
|
const tagLine = sourceCode.slice(0, tag.index).split("\n").length;
|
|
@@ -4329,8 +4431,8 @@ Import \`${exportName}\` ONLY from '${use.package}' \u2014 that is the package i
|
|
|
4329
4431
|
}
|
|
4330
4432
|
return issues;
|
|
4331
4433
|
}
|
|
4332
|
-
async function runTier0(sourceCode, contract) {
|
|
4333
|
-
const issues = await runTier0Checks(sourceCode, contract);
|
|
4434
|
+
async function runTier0(sourceCode, contract, designMode = DEFAULT_DESIGN_MODE) {
|
|
4435
|
+
const issues = await runTier0Checks(sourceCode, contract, void 0, void 0, designMode);
|
|
4334
4436
|
const pass = [];
|
|
4335
4437
|
const failedCategories = new Set(issues.map((i) => i.category));
|
|
4336
4438
|
const tier0Categories = ["compile", "security", "contract", "types", "imports", "tokens"];
|
|
@@ -4344,6 +4446,7 @@ async function runTier0(sourceCode, contract) {
|
|
|
4344
4446
|
var MANIFEST_TOKENS, TOKEN_REF_RE, HOOK_NAME_FOR, CONTRACT_FIELD_FOR;
|
|
4345
4447
|
var init_run_tier0 = __esm({
|
|
4346
4448
|
"src/check/run-tier0.ts"() {
|
|
4449
|
+
init_design_mode();
|
|
4347
4450
|
init_contract_validation();
|
|
4348
4451
|
init_type_checker();
|
|
4349
4452
|
init_react_linter();
|
|
@@ -4726,9 +4829,15 @@ function coerceStringArray(value) {
|
|
|
4726
4829
|
}
|
|
4727
4830
|
function buildMotherPrompt(ctx) {
|
|
4728
4831
|
const parts = [];
|
|
4729
|
-
|
|
4832
|
+
const designMode = ctx.designMode ?? DEFAULT_DESIGN_MODE;
|
|
4833
|
+
const free = designMode === "free";
|
|
4834
|
+
parts.push(
|
|
4835
|
+
free ? `You are evaluating a generated React component. It was composed freely \u2014 raw HTML elements, inline styles and <style> blocks are allowed and the ggui design package is optional \u2014 against a data contract and a theme-token color rule.
|
|
4836
|
+
The code has already passed automated checks (compilation, security, imports, color tokens).
|
|
4837
|
+
Your job is to evaluate ONE specific quality criterion per call.` : `You are evaluating a generated React component built with the ggui design system.
|
|
4730
4838
|
The code has already passed automated checks (compilation, security, imports, design tokens).
|
|
4731
|
-
Your job is to evaluate ONE specific quality criterion per call.`
|
|
4839
|
+
Your job is to evaluate ONE specific quality criterion per call.`
|
|
4840
|
+
);
|
|
4732
4841
|
parts.push(`## About This Prompt
|
|
4733
4842
|
This system prompt has 2 sections:
|
|
4734
4843
|
1. **Context** \u2014 the original user request and data contract
|
|
@@ -4781,11 +4890,7 @@ ${JSON.stringify(ctx.contract).slice(0, 6e3)}
|
|
|
4781
4890
|
parts.push(`## Design System
|
|
4782
4891
|
${ctx.designSystemSummary}`);
|
|
4783
4892
|
}
|
|
4784
|
-
parts.push(
|
|
4785
|
-
- Spacing props (\`gap\`, \`padding\`, \`margin\`) take a t-shirt-scale name: \`gap="md"\`, \`padding="lg"\` (\`none|xs|sm|md|lg|xl|2xl\`). **These ARE design tokens** \u2014 each resolves to a \`--ggui-spacing-*\` variable. A raw \`var(--ggui-spacing-*)\` string is an accepted escape hatch. NEVER flag a scale name as "hardcoded". Only a numeric prop (\`padding={24}\`) or a raw CSS length (\`gap="13px"\`) bypasses the scale and should be warned.
|
|
4786
|
-
- All colors must use CSS variables: \`color="var(--ggui-color-primary-600)"\` \u2014 hardcoded #hex or rgb() is a fail
|
|
4787
|
-
- Component props that take enum/scale values \u2014 \`variant="primary"\`, \`size="lg"\`, \`gap="md"\`, \`padding="lg"\`, \`shadow="md"\`, \`radius="lg"\`, \`tone="muted"\`, \`surface="accent"\` \u2014 are all design system tokens and are always valid
|
|
4788
|
-
- One \`export default function Component\` \u2014 helper components as named functions above it are fine
|
|
4893
|
+
parts.push(`${free ? FREE_STYLING_RULES : CONSTRAINED_STYLING_RULES}
|
|
4789
4894
|
|
|
4790
4895
|
## Evaluation Criteria Reference
|
|
4791
4896
|
|
|
@@ -4829,11 +4934,7 @@ ${ctx.designSystemSummary}`);
|
|
|
4829
4934
|
- Error boundaries or error messages for failure cases
|
|
4830
4935
|
- Empty states ("No items found") when data arrays could be empty
|
|
4831
4936
|
|
|
4832
|
-
|
|
4833
|
-
- Uses CSS variables (var(--ggui-*)) for colors, not hardcoded values
|
|
4834
|
-
- Consistent use of spacing tokens
|
|
4835
|
-
- Proper use of component variants (primary, outline, ghost)
|
|
4836
|
-
- Shadow, border-radius from design system tokens
|
|
4937
|
+
${free ? FREE_VISUAL_REFERENCE : CONSTRAINED_VISUAL_REFERENCE}
|
|
4837
4938
|
|
|
4838
4939
|
## Primitive Accessibility (built-in \u2014 do NOT flag as missing)
|
|
4839
4940
|
|
|
@@ -4867,8 +4968,7 @@ role, aria-*, label, or keyboard support \u2014 it is already there:
|
|
|
4867
4968
|
to text is CORRECTLY silent \u2014 never ask for \`aria-hidden\`, it is there.
|
|
4868
4969
|
|
|
4869
4970
|
REAL accessibility gaps worth flagging:
|
|
4870
|
-
|
|
4871
|
-
(no role, no keyboard) \u2014 or any hand-rolled interactive element.
|
|
4971
|
+
${free ? FREE_RAW_CLICK_TARGET_GAP : CONSTRAINED_RAW_CLICK_TARGET_GAP}
|
|
4872
4972
|
- A hand-rolled control whose state (checked / selected / expanded /
|
|
4873
4973
|
pressed) is conveyed ONLY by styling \u2014 e.g. a styled-div "checkbox"
|
|
4874
4974
|
with a check icon + strikethrough label but no \`role="checkbox"\` /
|
|
@@ -4923,13 +5023,13 @@ function makeTier2Tool(criterion) {
|
|
|
4923
5023
|
}
|
|
4924
5024
|
};
|
|
4925
5025
|
}
|
|
4926
|
-
function getCriterionPrompt(criterion, numberedSource) {
|
|
5026
|
+
function getCriterionPrompt(criterion, numberedSource, designMode) {
|
|
4927
5027
|
const sourceBlock = `
|
|
4928
5028
|
Source code (with line numbers):
|
|
4929
5029
|
\`\`\`tsx
|
|
4930
5030
|
${numberedSource}
|
|
4931
5031
|
\`\`\``;
|
|
4932
|
-
const criterionDef = getCriterionById(criterion);
|
|
5032
|
+
const criterionDef = getCriterionById(criterion, designMode);
|
|
4933
5033
|
if (criterionDef && criterionDef.tier > 0) {
|
|
4934
5034
|
return `${criterionDef.evalInstruction}
|
|
4935
5035
|
${sourceBlock}
|
|
@@ -5076,7 +5176,7 @@ Return your evaluation using the evaluate_visual tool.
|
|
|
5076
5176
|
|
|
5077
5177
|
Example outputs:
|
|
5078
5178
|
- Clean: {"result": "pass"}
|
|
5079
|
-
- Minor issues: {"result": "warn", "issues": ["Line 45: hardcoded '#f3f4f6' should use var(--ggui-color-
|
|
5179
|
+
- Minor issues: {"result": "warn", "issues": ["Line 45: hardcoded '#f3f4f6' should use var(--ggui-color-sunken)", "Line 80: numeric padding={16} should use a scale name like padding='md'"]}
|
|
5080
5180
|
- Severe: {"result": "fail", "issues": ["Component uses entirely custom styling with no design system tokens"]}
|
|
5081
5181
|
|
|
5082
5182
|
Use 'fail' only for pervasive design system violations. Use 'warn' for isolated cases.`;
|
|
@@ -5098,10 +5198,10 @@ function getDefaultEvalModel(provider) {
|
|
|
5098
5198
|
return "claude-haiku-4-5-20251001";
|
|
5099
5199
|
}
|
|
5100
5200
|
}
|
|
5101
|
-
async function evalCriterion(criterion, tier, motherPrompt, numberedSource, agent, model) {
|
|
5201
|
+
async function evalCriterion(criterion, tier, motherPrompt, numberedSource, agent, model, designMode) {
|
|
5102
5202
|
const start = Date.now();
|
|
5103
5203
|
const tool = tier === 1 ? makeTier1Tool(criterion) : makeTier2Tool(criterion);
|
|
5104
|
-
const userPrompt = getCriterionPrompt(criterion, numberedSource);
|
|
5204
|
+
const userPrompt = getCriterionPrompt(criterion, numberedSource, designMode);
|
|
5105
5205
|
try {
|
|
5106
5206
|
const response = await agent.callTools(
|
|
5107
5207
|
model,
|
|
@@ -5197,7 +5297,8 @@ async function preWarmEval(context, _config) {
|
|
|
5197
5297
|
const motherPrompt = buildMotherPrompt({
|
|
5198
5298
|
originalPrompt: context.originalPrompt,
|
|
5199
5299
|
contract: context.contract,
|
|
5200
|
-
designSystemSummary: context.designContext
|
|
5300
|
+
designSystemSummary: context.designContext,
|
|
5301
|
+
designMode: context.designMode
|
|
5201
5302
|
});
|
|
5202
5303
|
return { motherPrompt, dynamicCriteria: [] };
|
|
5203
5304
|
}
|
|
@@ -5230,10 +5331,12 @@ async function runLLMEvaluation(context, config, preWarmedContext) {
|
|
|
5230
5331
|
const providerName = config.provider;
|
|
5231
5332
|
const model = config.model || getDefaultEvalModel(providerName);
|
|
5232
5333
|
const routerProvider = providerName === "claude" ? "anthropic" : providerName;
|
|
5334
|
+
const designMode = context.designMode ?? DEFAULT_DESIGN_MODE;
|
|
5233
5335
|
const motherPrompt = preWarmedContext?.motherPrompt ?? buildMotherPrompt({
|
|
5234
5336
|
originalPrompt: context.originalPrompt,
|
|
5235
5337
|
contract: context.contract,
|
|
5236
|
-
designSystemSummary: context.designContext
|
|
5338
|
+
designSystemSummary: context.designContext,
|
|
5339
|
+
designMode
|
|
5237
5340
|
});
|
|
5238
5341
|
const numberedSource = context.sourceCode.split("\n").map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
|
|
5239
5342
|
const agent = createAgent({
|
|
@@ -5244,8 +5347,8 @@ async function runLLMEvaluation(context, config, preWarmedContext) {
|
|
|
5244
5347
|
const evalStart = Date.now();
|
|
5245
5348
|
const totalCriteriaCount = TIER_1_CRITERIA.length + TIER_2_CRITERIA.length;
|
|
5246
5349
|
const results = await Promise.all([
|
|
5247
|
-
...TIER_1_CRITERIA.map((c) => evalCriterion(c, 1, motherPrompt, numberedSource, agent, model)),
|
|
5248
|
-
...TIER_2_CRITERIA.map((c) => evalCriterion(c, 2, motherPrompt, numberedSource, agent, model))
|
|
5350
|
+
...TIER_1_CRITERIA.map((c) => evalCriterion(c, 1, motherPrompt, numberedSource, agent, model, designMode)),
|
|
5351
|
+
...TIER_2_CRITERIA.map((c) => evalCriterion(c, 2, motherPrompt, numberedSource, agent, model, designMode))
|
|
5249
5352
|
]);
|
|
5250
5353
|
const allIssues = [];
|
|
5251
5354
|
const passCategories = [];
|
|
@@ -5289,12 +5392,39 @@ async function runLLMEvaluation(context, config, preWarmedContext) {
|
|
|
5289
5392
|
outputTokens: totalOut
|
|
5290
5393
|
};
|
|
5291
5394
|
}
|
|
5292
|
-
var TIER_1_CRITERIA, TIER_2_CRITERIA, TIER_1_CATEGORIES, TIER_2_CATEGORIES, VALID_CATEGORIES;
|
|
5395
|
+
var FREE_STYLING_RULES, CONSTRAINED_STYLING_RULES, CONSTRAINED_VISUAL_REFERENCE, FREE_VISUAL_REFERENCE, CONSTRAINED_RAW_CLICK_TARGET_GAP, FREE_RAW_CLICK_TARGET_GAP, TIER_1_CRITERIA, TIER_2_CRITERIA, TIER_1_CATEGORIES, TIER_2_CATEGORIES, VALID_CATEGORIES;
|
|
5293
5396
|
var init_llm_evaluator = __esm({
|
|
5294
5397
|
"src/evaluation/llm-evaluator.ts"() {
|
|
5295
5398
|
init_types_public();
|
|
5296
5399
|
init_llm_router();
|
|
5297
5400
|
init_types_public();
|
|
5401
|
+
init_design_mode();
|
|
5402
|
+
FREE_STYLING_RULES = `## Styling Rules (important for evaluation)
|
|
5403
|
+
- Color: every color must be a bare \`var(--ggui-color-*)\` token \u2014 a hardcoded #hex, rgb()/hsl() or CSS named color is a fail, and so is a literal fallback inside \`var(--ggui-*, \u2026)\`.
|
|
5404
|
+
- Spacing, typography, radius, shadow geometry and layout values MAY be literals (\`padding: '20px'\`, \`fontSize: 18\`, \`borderRadius: 12\`) \u2014 NEVER flag them.
|
|
5405
|
+
- Raw HTML elements with inline styles or \`<style>\` blocks are a first-class choice \u2014 never flag "not using the design system" or reward using it.
|
|
5406
|
+
- When \`@ggui-ai/design\` primitives ARE used, their enum/scale props (\`variant="primary"\`, \`size="lg"\`, \`gap="md"\`, \`tone="muted"\`, \`surface="accent"\`) are valid.
|
|
5407
|
+
- One \`export default function Component\` \u2014 helper components as named functions above it are fine`;
|
|
5408
|
+
CONSTRAINED_STYLING_RULES = `## Design System Rules (important for evaluation)
|
|
5409
|
+
- Spacing props (\`gap\`, \`padding\`, \`margin\`) take a t-shirt-scale name: \`gap="md"\`, \`padding="lg"\` (\`none|xs|sm|md|lg|xl|2xl\`). **These ARE design tokens** \u2014 each resolves to a \`--ggui-spacing-*\` variable. A raw \`var(--ggui-spacing-*)\` string is an accepted escape hatch. NEVER flag a scale name as "hardcoded". Only a numeric prop (\`padding={24}\`) or a raw CSS length (\`gap="13px"\`) bypasses the scale and should be warned.
|
|
5410
|
+
- All colors must use CSS variables: \`color="var(--ggui-color-primary-600)"\` \u2014 hardcoded #hex or rgb() is a fail
|
|
5411
|
+
- Component props that take enum/scale values \u2014 \`variant="primary"\`, \`size="lg"\`, \`gap="md"\`, \`padding="lg"\`, \`shadow="md"\`, \`radius="lg"\`, \`tone="muted"\`, \`surface="accent"\` \u2014 are all design system tokens and are always valid
|
|
5412
|
+
- One \`export default function Component\` \u2014 helper components as named functions above it are fine`;
|
|
5413
|
+
CONSTRAINED_VISUAL_REFERENCE = `**visual**: Design system consistency.
|
|
5414
|
+
- Uses CSS variables (var(--ggui-*)) for colors, not hardcoded values
|
|
5415
|
+
- Consistent use of spacing tokens
|
|
5416
|
+
- Proper use of component variants (primary, outline, ghost)
|
|
5417
|
+
- Shadow, border-radius from design system tokens`;
|
|
5418
|
+
FREE_VISUAL_REFERENCE = `**visual**: Visual composition.
|
|
5419
|
+
- Clear hierarchy (one focal point, headings distinct from body), consistent spacing rhythm
|
|
5420
|
+
- Coherent palette drawn from the theme color tokens
|
|
5421
|
+
- Fit to the canvas: fluid width, nothing cramped or stretched
|
|
5422
|
+
- No reward or penalty for using (or not using) the design-system primitives`;
|
|
5423
|
+
CONSTRAINED_RAW_CLICK_TARGET_GAP = `- A raw \`<div>\` / \`<span>\` used as a click target without \`as={Clickable}\`
|
|
5424
|
+
(no role, no keyboard) \u2014 or any hand-rolled interactive element.`;
|
|
5425
|
+
FREE_RAW_CLICK_TARGET_GAP = `- A raw \`<div>\` / \`<span>\` used as a click target with no \`role="button"\`,
|
|
5426
|
+
\`tabIndex\` and keyboard handler \u2014 a real \`<button>\` / \`<a href>\` is the
|
|
5427
|
+
correct raw element and is never a gap.`;
|
|
5298
5428
|
TIER_1_CRITERIA = LLM_EVAL_STATIC_CRITERIA.filter((c) => c.tier === 1).map((c) => c.criterion);
|
|
5299
5429
|
TIER_2_CRITERIA = LLM_EVAL_STATIC_CRITERIA.filter((c) => c.tier === 2).map((c) => c.criterion);
|
|
5300
5430
|
TIER_1_CATEGORIES = /* @__PURE__ */ new Set(["functionality", "crash"]);
|
|
@@ -8615,13 +8745,22 @@ var init_build = __esm({
|
|
|
8615
8745
|
// src/evaluation/visual-evaluator.ts
|
|
8616
8746
|
var visual_evaluator_exports = {};
|
|
8617
8747
|
__export(visual_evaluator_exports, {
|
|
8748
|
+
EXECUTABLE_PATH_LAUNCH_ARGS: () => EXECUTABLE_PATH_LAUNCH_ARGS,
|
|
8618
8749
|
buildRenderHTML: () => buildRenderHTML,
|
|
8619
8750
|
bundleForRendering: () => bundleForRendering,
|
|
8620
8751
|
callMultimodalLLM: () => callMultimodalLLM,
|
|
8752
|
+
captureScreenshot: () => captureScreenshot,
|
|
8621
8753
|
resolveDesignPackageDir: () => resolveDesignPackageDir,
|
|
8754
|
+
resolveLaunchOptions: () => resolveLaunchOptions,
|
|
8755
|
+
resolveUiGenPackageDir: () => resolveUiGenPackageDir,
|
|
8622
8756
|
runVisualEval: () => runVisualEval,
|
|
8623
|
-
runVisualEvaluation: () => runVisualEvaluation
|
|
8757
|
+
runVisualEvaluation: () => runVisualEvaluation,
|
|
8758
|
+
summarizeVisualResult: () => summarizeVisualResult
|
|
8624
8759
|
});
|
|
8760
|
+
function resolveUiGenPackageDir() {
|
|
8761
|
+
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
8762
|
+
return resolve(selfDir, "..", "..");
|
|
8763
|
+
}
|
|
8625
8764
|
function resolveDesignPackageDir() {
|
|
8626
8765
|
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
8627
8766
|
return resolve(selfDir, "..", "..", "..", "design");
|
|
@@ -8636,12 +8775,32 @@ async function bundleForRendering(compiledCode, sampleProps) {
|
|
|
8636
8775
|
const entryCode = `
|
|
8637
8776
|
import React from 'react';
|
|
8638
8777
|
import { createRoot } from 'react-dom/client';
|
|
8778
|
+
import { GguiWireProvider } from '@ggui-ai/wire';
|
|
8639
8779
|
import Component from './component.tsx';
|
|
8640
8780
|
|
|
8641
8781
|
const props = ${JSON.stringify(sampleProps)};
|
|
8782
|
+
const wireConfig = {
|
|
8783
|
+
app: { appId: 'visual-eval', appName: 'Visual evaluation' },
|
|
8784
|
+
render: { sessionId: 'visual-eval', isConnected: true },
|
|
8785
|
+
auth: { isAuthenticated: false },
|
|
8786
|
+
dispatch: () => {},
|
|
8787
|
+
subscribe: () => () => {},
|
|
8788
|
+
};
|
|
8789
|
+
class RenderErrorBoundary extends React.Component<{ children: React.ReactNode }, { message: string | null }> {
|
|
8790
|
+
state = { message: null as string | null };
|
|
8791
|
+
static getDerivedStateFromError(err: unknown) { return { message: err instanceof Error ? err.message : String(err) }; }
|
|
8792
|
+
render() {
|
|
8793
|
+
return this.state.message === null
|
|
8794
|
+
? this.props.children
|
|
8795
|
+
: React.createElement('div', { className: 'error' }, 'Render error: ' + this.state.message);
|
|
8796
|
+
}
|
|
8797
|
+
}
|
|
8642
8798
|
const root = createRoot(document.getElementById('root')!);
|
|
8643
8799
|
try {
|
|
8644
|
-
root.render(
|
|
8800
|
+
root.render(
|
|
8801
|
+
React.createElement(GguiWireProvider, { config: wireConfig },
|
|
8802
|
+
React.createElement(RenderErrorBoundary, null, React.createElement(Component, props))),
|
|
8803
|
+
);
|
|
8645
8804
|
} catch (err) {
|
|
8646
8805
|
root.render(React.createElement('div', { className: 'error' }, 'Render error: ' + (err as Error).message));
|
|
8647
8806
|
}
|
|
@@ -8667,6 +8826,10 @@ try {
|
|
|
8667
8826
|
"@ggui-ai/design/compositions": resolve(designPkgDir, "src", "compositions", "index.ts"),
|
|
8668
8827
|
"@ggui-ai/design/interact": resolve(designPkgDir, "src", "interact", "index.ts")
|
|
8669
8828
|
},
|
|
8829
|
+
// Bare imports in the generated component resolve against ui-gen's
|
|
8830
|
+
// own node_modules — the temp entry dir has none. Without this every
|
|
8831
|
+
// wire-bearing component (`@ggui-ai/wire`) failed to bundle.
|
|
8832
|
+
nodePaths: [resolve(resolveUiGenPackageDir(), "node_modules")],
|
|
8670
8833
|
logLevel: "silent"
|
|
8671
8834
|
});
|
|
8672
8835
|
return result.outputFiles[0]?.text ?? "";
|
|
@@ -8717,17 +8880,37 @@ ${bundledCode}
|
|
|
8717
8880
|
</body>
|
|
8718
8881
|
</html>`;
|
|
8719
8882
|
}
|
|
8720
|
-
async function
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8883
|
+
async function loadSparticuzChromium() {
|
|
8884
|
+
const chromium = await Promise.resolve().then(() => (init_build(), build_exports));
|
|
8885
|
+
return chromium.default;
|
|
8886
|
+
}
|
|
8887
|
+
async function resolveLaunchOptions(viewport, deps = {}) {
|
|
8888
|
+
const env = deps.env ?? process.env;
|
|
8889
|
+
const override = env.PUPPETEER_EXECUTABLE_PATH?.trim();
|
|
8890
|
+
if (override) {
|
|
8891
|
+
return {
|
|
8892
|
+
executablePath: override,
|
|
8893
|
+
args: [...EXECUTABLE_PATH_LAUNCH_ARGS],
|
|
8726
8894
|
defaultViewport: viewport,
|
|
8727
|
-
executablePath: await chromium.default.executablePath(),
|
|
8728
8895
|
headless: true
|
|
8729
8896
|
};
|
|
8730
|
-
|
|
8897
|
+
}
|
|
8898
|
+
const chromium = await (deps.loadChromium ?? loadSparticuzChromium)();
|
|
8899
|
+
return {
|
|
8900
|
+
args: chromium.args,
|
|
8901
|
+
defaultViewport: viewport,
|
|
8902
|
+
executablePath: await chromium.executablePath(),
|
|
8903
|
+
headless: true
|
|
8904
|
+
};
|
|
8905
|
+
}
|
|
8906
|
+
async function launchWithPuppeteer(options) {
|
|
8907
|
+
const puppeteer = await import('puppeteer-core');
|
|
8908
|
+
return puppeteer.default.launch(options);
|
|
8909
|
+
}
|
|
8910
|
+
async function captureScreenshot(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}) {
|
|
8911
|
+
try {
|
|
8912
|
+
const launchOptions = await resolveLaunchOptions(viewport, deps);
|
|
8913
|
+
const browser = await (deps.launch ?? launchWithPuppeteer)(launchOptions);
|
|
8731
8914
|
try {
|
|
8732
8915
|
const page = await browser.newPage();
|
|
8733
8916
|
await page.setContent(html, { waitUntil: "load", timeout: 15e3 });
|
|
@@ -8735,9 +8918,9 @@ async function captureScreenshot(html, viewport = { width: 1280, height: 800 })
|
|
|
8735
8918
|
});
|
|
8736
8919
|
await page.waitForSelector("#root > *", { timeout: 1e4 }).catch(() => {
|
|
8737
8920
|
});
|
|
8738
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
8921
|
+
await new Promise((r) => setTimeout(r, deps.settleMs ?? 1e3));
|
|
8739
8922
|
const screenshot = await page.screenshot({ type: "png", fullPage: true });
|
|
8740
|
-
return screenshot;
|
|
8923
|
+
return Buffer.from(screenshot);
|
|
8741
8924
|
} finally {
|
|
8742
8925
|
await browser.close();
|
|
8743
8926
|
}
|
|
@@ -8764,22 +8947,56 @@ Evaluate the screenshot of the generated component.`;
|
|
|
8764
8947
|
1500
|
|
8765
8948
|
);
|
|
8766
8949
|
}
|
|
8767
|
-
async function runVisualEvaluation(context, config) {
|
|
8950
|
+
async function runVisualEvaluation(context, config, deps = {}) {
|
|
8768
8951
|
const startTime = Date.now();
|
|
8952
|
+
const judge = deps.judge ?? callMultimodalLLM;
|
|
8769
8953
|
const bundledCode = await bundleForRendering(
|
|
8770
8954
|
context.compiledCode,
|
|
8771
8955
|
config.sampleProps ?? {}
|
|
8772
8956
|
);
|
|
8773
8957
|
console.log(`[visual-eval] bundled: ${bundledCode.length}B (${Date.now() - startTime}ms)`);
|
|
8774
8958
|
const html = buildRenderHTML(bundledCode, context.cssTokens);
|
|
8775
|
-
const
|
|
8959
|
+
const model = config.model ?? getDefaultVisualModel(config.provider);
|
|
8960
|
+
if (config.canvases !== void 0 && config.canvases.length > 0) {
|
|
8961
|
+
const perCanvas = [];
|
|
8962
|
+
const perCanvasResults = [];
|
|
8963
|
+
for (const canvas of config.canvases) {
|
|
8964
|
+
const viewport = CANVAS_VIEWPORTS[canvas];
|
|
8965
|
+
const screenshot2 = await captureScreenshot(html, viewport, deps);
|
|
8966
|
+
if (!screenshot2) {
|
|
8967
|
+
console.warn(`[visual-eval] no browser available at canvas ${canvas} \u2014 skipping visual evaluation`);
|
|
8968
|
+
return null;
|
|
8969
|
+
}
|
|
8970
|
+
const response2 = await judge(config, model, VISUAL_EVAL_PROMPT, screenshot2, context.originalPrompt);
|
|
8971
|
+
const result2 = parseVisualResponse(response2.text, config.passThreshold);
|
|
8972
|
+
result2.inputTokens = response2.inputTokens;
|
|
8973
|
+
result2.outputTokens = response2.outputTokens;
|
|
8974
|
+
perCanvasResults.push(result2);
|
|
8975
|
+
perCanvas.push({
|
|
8976
|
+
canvas,
|
|
8977
|
+
viewport,
|
|
8978
|
+
score: result2.finalScore,
|
|
8979
|
+
passed: result2.passed,
|
|
8980
|
+
screenshotPng: screenshot2
|
|
8981
|
+
});
|
|
8982
|
+
console.log(
|
|
8983
|
+
`[visual-eval] canvas=${canvas} ${viewport.width}\xD7${viewport.height} score=${result2.finalScore} ${result2.passed ? "pass" : "FAIL"} | in=${response2.inputTokens} out=${response2.outputTokens}`
|
|
8984
|
+
);
|
|
8985
|
+
}
|
|
8986
|
+
const aggregate = aggregateCanvasResults(perCanvas, perCanvasResults);
|
|
8987
|
+
const elapsed2 = Date.now() - startTime;
|
|
8988
|
+
console.log(
|
|
8989
|
+
`[visual-eval] score=${aggregate.finalScore} (mean of ${perCanvas.length} canvases; ${aggregate.passed ? "every canvas passed" : `${perCanvas.filter((c) => !c.passed).length} failed`}) (${elapsed2}ms)`
|
|
8990
|
+
);
|
|
8991
|
+
return aggregate;
|
|
8992
|
+
}
|
|
8993
|
+
const screenshot = await captureScreenshot(html, config.viewport, deps);
|
|
8776
8994
|
if (!screenshot) {
|
|
8777
8995
|
console.warn("[visual-eval] no browser available \u2014 skipping visual evaluation");
|
|
8778
8996
|
return null;
|
|
8779
8997
|
}
|
|
8780
8998
|
console.log(`[visual-eval] screenshot: ${screenshot.length}B (${Date.now() - startTime}ms)`);
|
|
8781
|
-
const
|
|
8782
|
-
const response = await callMultimodalLLM(
|
|
8999
|
+
const response = await judge(
|
|
8783
9000
|
config,
|
|
8784
9001
|
model,
|
|
8785
9002
|
VISUAL_EVAL_PROMPT,
|
|
@@ -8793,6 +9010,48 @@ async function runVisualEvaluation(context, config) {
|
|
|
8793
9010
|
console.log(`[visual-eval] score=${result.finalScore} (${elapsed}ms) | in=${response.inputTokens} out=${response.outputTokens}`);
|
|
8794
9011
|
return result;
|
|
8795
9012
|
}
|
|
9013
|
+
function aggregateCanvasResults(perCanvas, results) {
|
|
9014
|
+
const n = results.length;
|
|
9015
|
+
const mean = (pick) => Math.round(results.reduce((sum, r) => sum + pick(r), 0) / n);
|
|
9016
|
+
const dimensions = {
|
|
9017
|
+
completeness: mean((r) => r.dimensions.completeness),
|
|
9018
|
+
visualPolish: mean((r) => r.dimensions.visualPolish),
|
|
9019
|
+
interactivity: mean((r) => r.dimensions.interactivity),
|
|
9020
|
+
accessibility: mean((r) => r.dimensions.accessibility),
|
|
9021
|
+
codeQuality: mean((r) => r.dimensions.codeQuality)
|
|
9022
|
+
};
|
|
9023
|
+
const issues = [];
|
|
9024
|
+
const critiques = [];
|
|
9025
|
+
let inputTokens = 0;
|
|
9026
|
+
let outputTokens = 0;
|
|
9027
|
+
results.forEach((r, i) => {
|
|
9028
|
+
const canvas = perCanvas[i].canvas;
|
|
9029
|
+
for (const issue of r.issues) issues.push({ ...issue, description: `[${canvas}] ${issue.description}` });
|
|
9030
|
+
if (r.critique) critiques.push(`${canvas}: ${r.critique}`);
|
|
9031
|
+
inputTokens += r.inputTokens ?? 0;
|
|
9032
|
+
outputTokens += r.outputTokens ?? 0;
|
|
9033
|
+
});
|
|
9034
|
+
return {
|
|
9035
|
+
passed: perCanvas.every((c) => c.passed),
|
|
9036
|
+
finalScore: mean((r) => r.finalScore),
|
|
9037
|
+
dimensions,
|
|
9038
|
+
issues,
|
|
9039
|
+
...critiques.length > 0 ? { critique: critiques.join("\n") } : {},
|
|
9040
|
+
inputTokens,
|
|
9041
|
+
outputTokens,
|
|
9042
|
+
canvases: [...perCanvas]
|
|
9043
|
+
};
|
|
9044
|
+
}
|
|
9045
|
+
function summarizeVisualResult(result) {
|
|
9046
|
+
if (result.canvases === void 0) return void 0;
|
|
9047
|
+
const canvases = result.canvases.map((c) => ({
|
|
9048
|
+
canvas: c.canvas,
|
|
9049
|
+
viewport: c.viewport,
|
|
9050
|
+
score: c.score,
|
|
9051
|
+
passed: c.passed
|
|
9052
|
+
}));
|
|
9053
|
+
return { score: result.finalScore, passed: result.passed, canvases };
|
|
9054
|
+
}
|
|
8796
9055
|
function parseVisualResponse(text, passThreshold) {
|
|
8797
9056
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
8798
9057
|
if (!jsonMatch) {
|
|
@@ -8840,8 +9099,8 @@ function getDefaultVisualModel(provider) {
|
|
|
8840
9099
|
}
|
|
8841
9100
|
async function runVisualEval(context, config) {
|
|
8842
9101
|
const result = await runVisualEvaluation(context, config);
|
|
8843
|
-
if (!result) return [];
|
|
8844
|
-
|
|
9102
|
+
if (!result) return { issues: [] };
|
|
9103
|
+
const issues = (result.issues || []).map((issue) => ({
|
|
8845
9104
|
tier: 2,
|
|
8846
9105
|
result: issue.severity === "critical" ? "fail" : "warn",
|
|
8847
9106
|
category: "visual",
|
|
@@ -8850,11 +9109,16 @@ async function runVisualEval(context, config) {
|
|
|
8850
9109
|
description: issue.description,
|
|
8851
9110
|
fix: issue.fix || ""
|
|
8852
9111
|
}));
|
|
9112
|
+
const summary = summarizeVisualResult(result);
|
|
9113
|
+
return summary === void 0 ? { issues } : { issues, summary };
|
|
8853
9114
|
}
|
|
8854
|
-
var VISUAL_EVAL_PROMPT;
|
|
9115
|
+
var EXECUTABLE_PATH_LAUNCH_ARGS, DEFAULT_SCREENSHOT_VIEWPORT, VISUAL_EVAL_PROMPT;
|
|
8855
9116
|
var init_visual_evaluator = __esm({
|
|
8856
9117
|
"src/evaluation/visual-evaluator.ts"() {
|
|
8857
9118
|
init_llm_router();
|
|
9119
|
+
init_design_mode();
|
|
9120
|
+
EXECUTABLE_PATH_LAUNCH_ARGS = ["--no-sandbox", "--disable-dev-shm-usage"];
|
|
9121
|
+
DEFAULT_SCREENSHOT_VIEWPORT = { width: 1280, height: 800 };
|
|
8858
9122
|
VISUAL_EVAL_PROMPT = `You are a visual quality evaluator for ggui \u2014 a platform where AI agents generate React UI components on demand.
|
|
8859
9123
|
|
|
8860
9124
|
## Context
|
|
@@ -9222,7 +9486,7 @@ __export(get_primitives_ts_exports, {
|
|
|
9222
9486
|
var PRIMITIVES_DOCUMENTATION_TS;
|
|
9223
9487
|
var init_get_primitives_ts = __esm({
|
|
9224
9488
|
"src/tools/get-primitives-ts.ts"() {
|
|
9225
|
-
PRIMITIVES_DOCUMENTATION_TS = "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// GGUI DESIGN SYSTEM \u2014 COMPONENT REFERENCE\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n//\n// CRITICAL \u2014 Props take enum STRING LITERALS, not CSS variables:\n// <Text size=\"sm\" weight=\"bold\" /> \u2713\n// <Text size=\"var(--ggui-font-size-sm)\" /> \u2717 (breaks)\n// The enum strings map to CSS variables internally.\n//\n// CRITICAL \u2014 onChange receives the VALUE directly, not an event:\n// <Input value={x} onChange={setX} /> \u2713\n// <Input onChange={(e) => setX(e.target.value)} /> \u2717\n//\n// Imports allowed: 'react', '@ggui-ai/design', '@ggui-ai/wire'.\n// The WHOLE design system \u2014 primitives, components, compositions,\n// traits \u2014 is one import: import { Card, Grid, Modal, Clickable }\n// from '@ggui-ai/design'. No subpaths. No external libs, no\n// fetch(), no eval().\n//\n// All components accept 'style' + 'className' (omitted below).\n// Structural primitives (Box, Stack, Row, Card) also take a trait via\n// 'as': 'as={Clickable}' adds onClick + keyboard a11y; likewise\n// 'as={Hoverable}' and 'as={Pressable}'. Semantic components\n// (Button/Link/Input) are already interactive and take no 'as'.\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Primitives (import from '@ggui-ai/design')\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// Container \u2014 Width-constrained wrapper that centers content horizontally\ninterface ContainerProps {\n children?: ReactNode;\n maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' | string; // default 'lg'; Maximum width constraint\n center?: boolean; // default true; Whether to center the container horizontally via `margin: 0 auto`\n padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default undefined (no padding); Padding applied to all sides\n}\n\n// Example:\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// Card \u2014 Container with background, shadow, and optional border\ninterface CardProps {\n children?: ReactNode;\n padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default 'lg'; Padding applied to all sides\n shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl'; // default 'sm'; Shadow elevation level\n border?: boolean; // default true; Whether to render a 1px border using `var(--ggui-color-outlineVariant)`\n radius?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | number | string; // default 'lg'; Corner radius\n surface?: 'default' | 'elevated' | 'sunken' | 'accent' | 'inverted' | 'transparent'; // default 'default'; Semantic surface slot\n}\n\n// Example:\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// Stack \u2014 Flexbox layout primitive for arranging children along a single axis\ninterface StackProps {\n children?: ReactNode;\n direction?: 'vertical' | 'horizontal'; // default 'vertical'; Main axis direction\n gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default 'sm'; Gap between children\n align?: 'start' | 'center' | 'end' | 'stretch'; // default 'stretch'; Cross-axis alignment (maps to `align-items`)\n justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'; // default 'start'; Main-axis content distribution (maps to `justify-content`)\n wrap?: boolean; // default false; Whether children wrap to the next line when they overflow\n}\n\n// Example:\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// Grid \u2014 2-D layout primitive\ninterface GridProps {\n children?: ReactNode;\n columns?: number | ResponsiveColumns; // default 2; Column count\n gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default 'md'; Gap between cells\n minColumnWidth?: number | string; // default undefined (use `columns`); When set, the grid becomes responsive \u2014 it fits as many equal\n}\n\n// Example:\n<Grid columns={3} gap=\"md\">\n {items.map((it) => <Card key={it.id}>{it.name}</Card>)}\n</Grid>\n\n// Skeleton \u2014 a pulsing placeholder for content that has not loaded\ninterface SkeletonProps {\n variant?: 'rect' | 'text' | 'circle'; // default 'rect'; Shape preset\n width?: number | string;\n height?: number | string;\n radius?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | number | string; // default 'sm'; Corner radius\n}\n\n// Example:\n{user === undefined\n ? <Skeleton variant=\"text\" width=\"40%\" />\n : <Text>{user.name}</Text>}\n\n// Box \u2014 Generic container with padding, margin, background, and border-radius\ninterface BoxProps {\n children?: ReactNode;\n padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default undefined (no padding); Padding applied to all four sides\n paddingX?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default undefined; Horizontal (left + right) padding\n paddingY?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default undefined; Vertical (top + bottom) padding\n margin?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default undefined (no margin); Margin applied to all four sides\n surface?: 'default' | 'elevated' | 'sunken' | 'accent' | 'inverted' | 'transparent'; // default undefined (transparent); Semantic surface slot\n assetColor?: string; // default undefined; Asset color escape \u2014 the typed valve for legitimate non-theme\n assetSemantic?: string; // default undefined; Human-readable semantic label that documents why\n radius?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | number | string; // default undefined (no rounding); Corner radius\n}\n\n// Example:\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// Divider \u2014 A 1px line to visually separate content sections\ninterface DividerProps {\n orientation?: 'horizontal' | 'vertical'; // default 'horizontal'; Line direction\n margin?: number | string; // default 16; Spacing around the divider\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default undefined (uses `var(--ggui-color-outlineVariant)`); Semantic color slot\n}\n\n// Example:\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// Spacer \u2014 Invisible spacing element, either fixed-size or flexible\ninterface SpacerProps {\n size?: number | 'flex'; // default 16; Spacing amount\n}\n\n// Example:\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// Text \u2014 Versatile typography primitive for body copy, captions, and labels\ninterface TextProps {\n children?: ReactNode;\n size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; // default 'base'; Font size\n weight?: 'normal' | 'medium' | 'semibold' | 'bold'; // default 'normal'; Font weight\n caps?: boolean; // default false; When true, uppercases the text and adds 0\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default 'default' (var(--ggui-color-onSurface)); Semantic color slot\n align?: 'left' | 'center' | 'right'; // default undefined (inherits from parent); Horizontal text alignment\n truncate?: boolean; // default false; When true, clips overflowing text with an ellipsis\n is?: 'p' | 'span' | 'div' | 'label'; // default 'p'; HTML element to render\n id?: string; // `id` for the rendered element \u2014 anchor an in-page link, or pair\n htmlFor?: string; // Associates an `is=\"label\"` element with a form control by the\n}\n\n// Example:\n<Stack gap=\"var(--ggui-spacing-1)\">\n <Text size=\"xs\" weight=\"semibold\" caps tone=\"muted\">Account</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// Heading \u2014 Semantic heading element (h1-h6) with preset typography styles\ninterface HeadingProps {\n children?: ReactNode;\n level?: 1 | 2 | 3 | 4 | 5 | 6; // default 2; Semantic heading level\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default 'default' (var(--ggui-color-onSurface)); Semantic color slot\n align?: 'left' | 'center' | 'right'; // default undefined (inherits from parent); Horizontal text alignment\n}\n\n// Example:\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// Button \u2014 A clickable button primitive with multiple visual variants and sizes\ninterface ButtonProps {\n children?: ReactNode;\n variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'; // default 'primary'; Visual style\n size?: 'xs' | 'sm' | 'md' | 'lg'; // default 'md'; Controls padding, font size, and minimum height:\n fullWidth?: boolean; // default false; When true, sets `width: 100%` so the button fills its container\n loading?: boolean; // default false; When true, replaces children with a 16px `Spinner` (color: `currentColor`)\n leftIcon?: ReactNode; // ReactNode rendered before children, inside the flex layout with `var(--ggui-s...\n rightIcon?: ReactNode; // ReactNode rendered after children, inside the flex layout with `var(--ggui-sp...\n onPress?: () => void; // Alias for `onClick` for cross-platform compatibility (React Native convention)\n}\n\n// Example:\n<Button variant=\"primary\" size=\"md\" leftIcon={<Icon name=\"save\" />} onClick={handleSave}>\n Save Changes\n</Button>\n\n// Input \u2014 A single-line text input with label, validation, and helper text\ninterface InputProps {\n label?: string; // Label rendered above the input\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\n type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search' | 'date' | 'time' | 'datetime-local'; // default 'text'; HTML input type\n error?: string; // Error message displayed below the input in `var(--ggui-color-error-500)`\n helperText?: string; // Helper text displayed below the input in `var(--ggui-color-onSurfaceVariant)`\n required?: boolean; // default false; When true, appends a red asterisk (`*`) to the label and sets the native\n disabled?: boolean; // default false; When true, sets the native `disabled` attribute\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls padding and font size:\n}\n\n// Example:\n<Input label=\"Email\" type=\"email\" value={email} onChange={setEmail} error={emailError} />\n\n// TextArea \u2014 A multiline text input with label, validation, character count, a...\ninterface TextAreaProps {\n label?: string; // Label rendered above the textarea\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\n rows?: number; // default 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)`\n helperText?: string; // Helper text displayed below the textarea in `var(--ggui-color-onSurfaceVariant)`\n required?: boolean; // default false; When true, appends a red asterisk (`*`) to the label and sets the native\n disabled?: boolean; // default false; When true, sets the native `disabled` attribute\n maxLength?: number; // Maximum character length (native `maxLength` attribute)\n showCount?: boolean; // default false; When true AND `maxLength` is set, displays a `{current}/{max}` character\n autoResize?: boolean; // default false; When true, sets CSS `resize: none` on the textarea\n}\n\n// Example:\n<TextArea label=\"Bio\" value={bio} onChange={setBio} rows={6} maxLength={500} showCount />\n\n// Select \u2014 A native dropdown selection primitive with label and validation\ninterface SelectProps {\n label?: string; // Label rendered above the select\n value?: string; // Controlled value\n onChange?: (value: string) => void; // Change handler\n options: SelectOption[]; // Array of selectable options\n placeholder?: string; // Placeholder text rendered as a disabled `<option value=\"\">` at the top of the\n error?: string; // Error message displayed below the select in `var(--ggui-color-error-500)`\n helperText?: string; // Helper text displayed below the select in `var(--ggui-color-onSurfaceVariant)`\n required?: boolean; // default false; When true, appends a red asterisk (`*`) to the label and sets the native\n disabled?: boolean; // default false; When true, sets the native `disabled` attribute\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls padding and font size:\n}\n\n// Example:\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// Checkbox \u2014 A custom-styled checkbox with label and description\ninterface CheckboxProps {\n label?: string; // Primary label text rendered beside the checkbox box\n checked?: boolean; // Controlled checked state\n onChange?: (checked: boolean) => void; // Change handler\n disabled?: boolean; // default false; When true, sets `opacity: 0\n description?: string; // Secondary description text rendered below the label in smaller, muted type\n indeterminate?: boolean; // default false; When true, displays a horizontal dash instead of a checkmark\n}\n\n// Example:\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// Toggle \u2014 A switch/toggle input rendered as a pill-shaped track with a slidin...\ninterface ToggleProps {\n label?: string; // Label text rendered to the right of the toggle track\n checked?: boolean; // Controlled checked (on/off) state\n onChange?: (checked: boolean) => void; // Change handler\n disabled?: boolean; // default false; When true, sets `opacity: 0\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls track and knob dimensions:\n}\n\n// Example:\n<Toggle label=\"Enable notifications\" checked={enabled} onChange={setEnabled} size=\"md\" />\n\n// RadioGroup \u2014 A group of mutually exclusive radio options with optional label...\ninterface RadioGroupProps {\n label?: string; // Group label rendered above the options\n value?: string; // Controlled value\n onChange?: (value: string) => void; // Change handler\n options: RadioOption[]; // Array of radio options\n direction?: 'vertical' | 'horizontal'; // default 'vertical'; Layout direction for the options container:\n disabled?: boolean; // default false; When true, disables ALL options (individual `RadioOption\n error?: string; // Error message displayed below all options in `var(--ggui-color-error-500)`\n}\n\n// Example:\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// Slider \u2014 A range input with a custom-styled track, fill, and thumb\ninterface SliderProps {\n label?: string; // Label rendered above the slider track (left-aligned)\n value?: number; // default 0; Controlled numeric value\n onChange?: (value: number) => void; // Change handler\n min?: number; // default 0; Minimum allowed value\n max?: number; // default 100; Maximum allowed value\n step?: number; // default 1; Step increment for the slider\n disabled?: boolean; // default false; When true, sets `cursor: not-allowed` on the native input\n showValue?: boolean; // default false; When true, displays the current numeric value right-aligned in the header row\n}\n\n// Example:\n<Slider label=\"Volume\" value={volume} onChange={setVolume} min={0} max={100} step={5} showValue />\n\n// Badge \u2014 Inline label for status indicators, counts, or categories\ninterface BadgeProps {\n children?: ReactNode;\n variant?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info'; // default 'default'; Visual style\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls padding and font size:\n pill?: boolean; // default true; When true, uses fully rounded corners (`border-radius: 9999px`)\n}\n\n// Example:\n<Badge variant=\"success\" size=\"sm\">Active</Badge>\n\n// Spinner \u2014 Animated SVG loading indicator\ninterface SpinnerProps {\n size?: number; // default 24; Width and height of the SVG element in pixels\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default undefined (uses `var(--ggui-color-primary-600)`); Semantic color slot for the animated foreground arc\n}\n\n// Example:\n<Spinner size={32} tone=\"success\" />\n\n// Avatar \u2014 User or entity representation with image or auto-generated initials\ninterface AvatarProps {\n src?: string; // Image URL\n name?: string; // Name used for two purposes:\n size?: number | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; // default 'md'; Avatar dimensions\n shape?: 'circle' | 'square'; // default 'circle'; Container shape\n}\n\n// Example:\n<Avatar src=\"/photos/jane.jpg\" name=\"Jane Doe\" size=\"lg\" shape=\"circle\" />\n\n// Alert \u2014 Contextual message box for important information with icon and optio...\ninterface AlertProps {\n children?: ReactNode;\n variant?: 'info' | 'success' | 'warning' | 'error'; // default 'info'; Visual style\n title?: string; // Optional title rendered above the body in semibold (`var(--ggui-font-weight-s...\n closable?: boolean; // default false; When true, renders a close button (X icon) in the top-right area\n onClose?: () => void; // Callback fired when the close button is clicked\n icon?: ReactNode; // Custom icon ReactNode to replace the default variant icon\n}\n\n// Example:\n<Alert variant=\"warning\" title=\"Rate limit\" closable onClose={() => setShow(false)}>\n You have 3 requests remaining this minute.\n</Alert>\n\n// Progress \u2014 Horizontal progress bar with determinate and indeterminate modes\ninterface ProgressProps {\n value: number; // Current progress value\n max?: number; // default 100; Maximum value representing 100% progress\n variant?: 'default' | 'success' | 'warning' | 'error'; // default 'default'; Fill bar color\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls track height in pixels:\n label?: string; // Accessible name describing what this bar measures, e\n showLabel?: boolean; // default false; When true, displays a header row above the track with the `label` text\n indeterminate?: boolean; // default false; When true, ignores `value` for visual width and plays a looping animation\n}\n\n// Example:\n<Progress value={65} variant=\"success\" size=\"md\" showLabel />\n\n// Image \u2014 An `<img>` element with built-in error handling and fallback support\ninterface ImageProps {\n src: string; // Image source URL\n alt: string; // Alt text for the image\n width?: number | string; // default '100%' (applied at render time, not on the type); Image width\n height?: number | string; // default 'auto' (applied at render time, not on the type); Image height\n objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; // default '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\n fallback?: ReactNode; // Custom ReactNode rendered when the image fails to load\n}\n\n// Example:\n<Image src=\"/hero.jpg\" alt=\"Hero banner\" width=\"100%\" height={400} objectFit=\"cover\" radius=\"md\" />\n\n// Icon \u2014 185 Lucide icons + emoji passthrough\ninterface IconProps {\n name?: string; // Lucide icon name (kebab-case, camelCase, or PascalCase all work)\n size?: number; // default 24; Icon dimensions in pixels (applied to both width and height of the wrapper\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default undefined (icon uses `currentColor`); Semantic color slot\n children?: ReactNode; // Custom SVG children\n 'aria-label'?: string; // Accessible name for a standalone, meaning-bearing icon\n}\n\n// Example:\n<Icon name=\"search\" size={20} tone=\"muted\" />\n<Icon name=\"cloud-rain\" size={32} />\n<Icon name=\"\u2600\uFE0F\" size={24} />\n\n// Link \u2014 Styled anchor element with external link support\ninterface LinkProps {\n children?: ReactNode;\n href: string; // Destination URL\n external?: boolean; // default false; When true, opens link in a new tab (`target=\"_blank\"`, `rel=\"noopener norefer...\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default undefined (uses `var(--ggui-color-primary-600)`); Semantic color slot for the link text\n underline?: 'always' | 'hover' | 'none'; // default 'hover'; Underline behavior:\n}\n\n// Example:\n<Link href=\"https://docs.ggui.ai\" external>Documentation</Link>\n\n// Tooltip \u2014 Hoverable information popup positioned relative to a trigger element\ninterface TooltipProps {\n children: ReactNode; // Trigger element\n content: ReactNode; // Tooltip content\n position?: 'top' | 'bottom' | 'left' | 'right'; // default 'top'; Tooltip placement relative to the trigger element:\n delay?: number; // default 200; Delay in milliseconds before the tooltip becomes visible after hover/focus\n}\n\n// Example:\n<Tooltip content=\"Copy to clipboard\" position=\"top\">\n <Button variant=\"ghost\"><Icon name=\"copy\" /></Button>\n</Tooltip>\n\n// Table \u2014 Data table with sortable columns, striped rows, and hover highlights\ninterface TableProps {\n columns: TableColumn<T>[]; // Array of column definitions controlling header labels, data keys, and rendering\n data: T[]; // Array of row data objects\n sortKey?: string; // The `key` of the currently sorted column\n sortDirection?: SortDirection; // default 'asc'; Current sort direction for the column identified by `sortKey`\n onSort?: (key: string, direction: SortDirection) => void; // Sort change handler\n striped?: boolean; // default false; When true, alternating rows (odd index) get a\n hoverable?: boolean; // default true; When true, rows highlight with `var(--ggui-color-surface)`\n compact?: boolean; // default false; When true, reduces cell padding:\n bordered?: boolean; // default false; When true, adds a 1px border around the table wrapper and between cells\n caption?: string; // Accessible table caption\n}\n\n// Example:\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// Tabs \u2014 Accessible tab navigation with panels and keyboard support\ninterface TabsProps {\n items: TabItem[]; // Array of tab definitions\n activeKey?: string; // Controlled active tab key\n onChange?: (key: string) => void; // Tab change handler\n variant?: 'line' | 'pills' | 'enclosed'; // default 'line'; Visual style of the tab bar:\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls tab button padding and font size:\n fullWidth?: boolean; // default false; When true, tab buttons expand equally to fill the container width\n}\n\n// Example:\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// Toast \u2014 Notification banner with auto-dismiss and slide-in animation\ninterface ToastProps {\n message: ReactNode; // Message body content\n variant?: 'info' | 'success' | 'warning' | 'error'; // default 'info'; Visual style\n title?: string; // Optional title rendered above the message in semibold\n duration?: number; // default 5000; Auto-dismiss delay in milliseconds\n onClose?: () => void; // Callback fired on auto-dismiss timeout or when the close button is clicked\n visible?: boolean; // default true; Controls rendering\n position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center'; // Intended screen position\n}\n\n// Example:\n<Toast variant=\"success\" title=\"Saved\" message=\"Your changes have been saved.\" onClose={() => setShow(false)} />\n\n// Accordion \u2014 Collapsible content sections with chevron rotation animation\ninterface AccordionProps {\n items: AccordionItem[]; // Array of collapsible section definitions\n expandedKeys?: string[]; // Controlled expanded state\n onChange?: (expandedKeys: string[]) => void; // Expand/collapse handler\n multiple?: boolean; // default false; When true, multiple items can be open simultaneously\n variant?: 'default' | 'bordered' | 'separated'; // default 'default'; Visual style controlling borders and spacing:\n}\n\n// Example:\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// Row \u2014 Convenience alias for a horizontal Stack\ninterface RowProps {\n children?: ReactNode;\n gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default 'sm'; Gap between children\n align?: 'start' | 'center' | 'end' | 'stretch'; // default 'stretch'; Cross-axis alignment (maps to `align-items`)\n justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'; // default 'start'; Main-axis content distribution (maps to `justify-content`)\n wrap?: boolean; // default false; Whether children wrap to the next line when they overflow\n}\n\n// Example:\n<Row gap=\"md\" align=\"center\" justify=\"between\">\n <Text size=\"sm\" weight=\"medium\">Total</Text>\n <Text weight=\"bold\">$42.00</Text>\n</Row>\n\n// Support types:\n// ResponsiveColumns \u2014 Explicit column count per viewport breakpoint, mobile-first\ninterface ResponsiveColumns {\n base?: number; // default 1; 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\n// SelectOption \u2014 An individual option within a `Select` dropdown\ninterface SelectOption {\n value: string; // The value submitted when this option is selected\n label: string; // The display text shown in the dropdown\n disabled?: boolean; // default false; When true, the option is visible but cannot be selected (grayed out by the br...\n}\n\n// RadioOption \u2014 An individual option within a `RadioGroup`\ninterface RadioOption {\n value: string; // The value emitted via `RadioGroupProps\n label: string; // Display text for this option\n description?: string; // Optional secondary description rendered below the label in smaller, muted type\n disabled?: boolean; // default false; When true, this individual option is grayed out (`opacity: 0\n}\n\n// TableColumn \u2014 Column definition for the Table component\ninterface TableColumn {\n key: string; // Property key in the row data object\n header: string; // Column header text\n width?: number | string; // Fixed column width\n align?: 'left' | 'center' | 'right'; // default 'left'; Horizontal text alignment for both the header and data cells\n sortable?: boolean; // default false; When true, the header cell becomes clickable and shows sort direction\n render?: (value: unknown, row: T, index: number) => ReactNode; // Custom cell renderer\n}\n\n// TabItem \u2014 Definition of a single tab within a Tabs component\ninterface TabItem {\n key: string; // Unique identifier for this tab\n label: ReactNode; // Tab button label\n content: ReactNode; // Panel content rendered below the tab bar when this tab is active\n disabled?: boolean; // default false; When true, the tab button shows `opacity: 0\n icon?: ReactNode; // Optional icon rendered before the label inside the tab button, with\n}\n\n// AccordionItem \u2014 Definition of a single collapsible section within an Accordion\ninterface AccordionItem {\n key: string; // Unique identifier for this item\n title: ReactNode; // Header label rendered inside the toggle button\n content: ReactNode; // Panel content rendered below the header when expanded\n disabled?: boolean; // default false; When true, the header button shows `opacity: 0\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Components (import from '@ggui-ai/design')\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// SearchField \u2014 A text input with a leading search icon and optional submit button\ninterface SearchFieldProps {\n value?: string; // Current search value\n onChange?: (value: string) => void; // Called on every keystroke with the new input string (value directly, not\n onSearch?: (value: string) => void; // Called when the user presses **Enter** or clicks the search button\n placeholder?: string; // default 'Search...'; Placeholder text shown when the input is empty\n showButton?: boolean; // default false; When `true`, renders a `Button` primitive to the right of the input\n buttonText?: string; // default 'Search'; Label rendered inside the submit button\n loading?: boolean; // When `true`, replaces the search icon with a `Spinner`, disables the\n disabled?: boolean; // When `true`, the input and button are visually disabled and do not\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls input height and font size\n}\n\n// Example:\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// FormField \u2014 A wrapper that adds a label, optional description, error message,\ninterface FormFieldProps {\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) re...\n error?: string; // Error message displayed below `children` in `colors\n helperText?: string; // Neutral guidance text displayed below `children` in `colors\n required?: boolean; // When `true`, appends a red asterisk (`*`) after the label text\n description?: string; // Secondary description rendered between the label and the child control\n}\n\n// Example:\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// MenuItem \u2014 A full-width clickable row for menus, sidebars, and action lists\ninterface MenuItemProps {\n label: string; // Primary text content of the menu item\n icon?: ReactNode; // Icon or element rendered to the left of the label\n rightElement?: ReactNode; // Element rendered to the right of the label (e\n onClick?: () => void; // Called when the item is clicked\n disabled?: boolean; // When `true`, the item is non-interactive: `colors\n active?: boolean; // Marks this item as the current selection\n danger?: boolean; // Switches the item to destructive styling: `colors\n}\n\n// Example:\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// Tag \u2014 An inline label for categories, filters, statuses, or selections\ninterface TagProps {\n children: ReactNode; // Tag content -- typically a short text string\n variant?: 'default' | 'primary' | 'success' | 'warning' | 'error' | 'info'; // default 'default'; Semantic color variant applied to background, text, and border\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls padding, font size, and internal gap\n closable?: boolean; // When `true`, renders a small close (\"x\") button after the content\n onClose?: () => void; // Called when the close button is clicked\n icon?: ReactNode; // Icon or element rendered before the text content\n}\n\n// Example:\n```tsx\n<Tag variant=\"success\" size=\"md\" closable onClose={() => removeFilter(id)}>\n Active\n</Tag>\n```\n\n// Dropdown \u2014 A click-triggered menu anchored to a trigger element\ninterface DropdownProps {\n trigger: ReactNode; // The element the user clicks to open the menu\n options: DropdownOption[]; // Array of selectable options rendered as `MenuItem` rows\n value?: string; // The `value` of the currently selected option\n onChange?: (value: string) => void; // Called with the `value` string of the selected option (not the full\n placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'; // default 'bottom-start'; Where to anchor the menu panel relative to the trigger\n disabled?: boolean; // When `true`, the trigger shows `cursor: not-allowed` and clicking\n}\n\n// Example:\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// Autocomplete \u2014 A text input with a filterable suggestion dropdown, keyboard navigation,\ninterface AutocompleteProps {\n value?: string; // default ''; Current text in the input field\n onChange?: (value: string) => void; // Called on every keystroke with the new input string (value directly,\n onSelect?: (option: AutocompleteOption) => void; // Called when the user selects an option (click or Enter on highlighted\n options: AutocompleteOption[]; // The full list of available options\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\n disabled?: boolean; // When `true`, the inner `Input` is disabled and the dropdown\n error?: string; // Error message forwarded to the inner `Input` primitive\n noResultsText?: string; // default 'No results found'; Text shown in the dropdown when filtering produces zero matches\n}\n\n// Example:\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// Breadcrumb \u2014 A horizontal navigation trail showing the user's location within a\ninterface BreadcrumbProps {\n items: BreadcrumbItem[]; // Ordered array of breadcrumb segments from root to current page\n separator?: ReactNode; // default '/'; Separator rendered between each pair of items\n onItemClick?: (item: BreadcrumbItem, index: number) => void; // Called when a non-last item is clicked\n}\n\n// Example:\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// Pagination \u2014 Page navigation controls with previous/next arrows, numbered page buttons,\ninterface PaginationProps {\n currentPage: number; // Current active page\n totalPages: number; // Total number of pages\n onPageChange?: (page: number) => void; // Called when the user clicks a page number, arrow, or first/last button\n showFirstLast?: boolean; // default true; When `true`, renders double-chevron buttons for jumping to the first\n maxVisible?: number; // default 5; Maximum number of page buttons visible at once (including the first\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls button dimensions and icon sizes\n disabled?: boolean; // When `true`, all page buttons and arrows are visually dimmed\n}\n\n// Example:\n```tsx\n<Pagination\n currentPage={page}\n totalPages={20}\n onPageChange={setPage}\n maxVisible={7}\n size=\"md\"\n/>\n```\n\n// EmptyState \u2014 placeholder for a region with no data: empty lists,\ninterface EmptyStateProps {\n icon?: string | ReactNode; // A Lucide icon name (kebab-case), rendered large and subtle above\n title: string; // The headline, e\n description?: string; // Optional supporting line below the title\n action?: ReactNode; // Optional call-to-action, typically a `<Button>`\n}\n\n// Example:\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// Stat \u2014 a single KPI / metric: a label, a large value, an optional\ninterface StatProps {\n label: string; // The metric name, e\n value: string | number; // The headline value \u2014 the big number\n delta?: string; // Optional change indicator, pre-formatted, e\n trend?: 'up' | 'down' | 'neutral'; // default 'neutral'; Direction of `delta` \u2014 colours it: `'up'` success, `'down'` error,\n icon?: string | ReactNode; // Optional Lucide icon name (kebab-case) or custom node, shown next\n}\n\n// Example:\n<Stat label=\"Revenue\" value=\"$48.2k\" delta=\"+12.5%\" trend=\"up\" icon=\"trending-up\" />\n\n// Stepper \u2014 a display-only step indicator for multi-step flows\ninterface StepperProps {\n steps: string[]; // Ordered step labels, one per step\n current: number; // Zero-based index of the active step\n orientation?: 'horizontal' | 'vertical'; // default 'horizontal'; Flow direction\n onStepClick?: (index: number) => void; // When provided, each step becomes a real button and clicks receive\n}\n\n// Example:\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// Markdown \u2014 renders markdown-formatted text as styled content:\ninterface MarkdownProps {\n markdown: string; // Markdown source text, passed through verbatim\n}\n\n// Example:\n<Stack gap=\"md\">\n <Markdown markdown={recipe.instructions} />\n</Stack>\n\n// MarkdownInline \u2014 renders ONE line of inline-level markdown (bold,\ninterface MarkdownInlineProps {\n markdown: string; // Inline markdown source -- bold, italic, inline code, and\n}\n\n// Example:\n<Row gap=\"sm\">\n <Badge variant=\"success\">stable</Badge>\n <MarkdownInline markdown={release.summary} />\n</Row>\n\n// Support types:\n// DropdownOption \u2014 A single option inside a `Dropdown`\ninterface DropdownOption {\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\n disabled?: boolean; // When `true`, the option is visible but non-interactive\n danger?: boolean; // When `true`, the option uses destructive (red) styling via\n}\n\n// AutocompleteOption \u2014 A single option in the `Autocomplete` suggestion list\ninterface AutocompleteOption {\n value: string; // Unique identifier for this option\n label: string; // Primary display text\n description?: string; // Secondary description rendered below the label in `fontSize\n icon?: ReactNode; // Icon rendered to the left of the label/description block\n disabled?: boolean; // When `true`, the option is visible but non-interactive (`cursor: not-allowed`...\n}\n\n// BreadcrumbItem \u2014 A single segment in a `Breadcrumb` trail\ninterface BreadcrumbItem {\n label: string; // Display text for this breadcrumb segment\n href?: string; // URL for this segment\n icon?: ReactNode; // Icon rendered immediately before the label\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Compositions (import from '@ggui-ai/design')\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// Header \u2014 Props for the `Header` composition\ninterface HeaderProps {\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 l...\n actions?: ReactNode; // Right-side action elements (buttons, avatar, etc\n sticky?: boolean; // default false; When true, the header becomes `position: sticky` at the top of its scroll con...\n background?: string; // default colors.white; Background color of the header\n bordered?: boolean; // default true; When true, renders a 1 px bottom border in `colors\n}\n\n// Example:\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// Sidebar \u2014 Props for the `Sidebar` composition\ninterface SidebarProps {\n items: SidebarItem[]; // Array of navigation items to render\n activeId?: string; // ID of the currently active item\n onItemClick?: (item: SidebarItem) => void; // Called when any item (including parent items with children) is clicked\n collapsed?: boolean; // default false; When true, hides labels and badges; only icons remain visible, centered in th...\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; // default 256; Width in pixels when expanded\n collapsedWidth?: number; // default 64; Width in pixels when collapsed\n}\n\n// Example:\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// CardGrid \u2014 Props for the `CardGrid` composition\ninterface CardGridProps {\n children: ReactNode; // Card elements to arrange in the grid\n columns?: number | { sm?: number; md?: number; lg?: number }; // default 3; Number of columns, or a responsive breakpoint map\n gap?: number; // default 16; Gap between grid items in pixels\n}\n\n// Example:\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// CommentThread \u2014 Props for the `CommentThread` composition\ninterface CommentThreadProps {\n comments: Comment[]; // Array of top-level comments to render\n currentUser?: { /** Display name for the current user's avatar. */ name: string; /** Avatar image URL for the current user. */ avatar?: string; }; // Current user info\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\n// Example:\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// DataTable \u2014 Props for the `DataTable` composition\ninterface DataTableProps {\n columns: DataTableColumn<T>[]; // Column definitions that control header, alignment, sorting, and rendering\n data: T[]; // Row data array\n rowKey?: string | ((row: T) => string); // default '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; // default '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\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\n selectable?: boolean; // When true, adds a checkbox column at the start of each row\n selectedKeys?: string[]; // default []; 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-...\n}\n\n// Example:\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// ChatWindow \u2014 Props for the `ChatWindow` composition\ninterface ChatWindowProps {\n messages: ChatMessage[]; // Array of chat messages to display in chronological order\n currentUserId: string; // ID of the current user\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\n placeholder?: string; // default 'Type a message...'; Placeholder text for the message input field\n header?: ReactNode; // Optional header content rendered above the message area, separated by a botto...\n}\n\n// Example:\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// NavigationBar \u2014 Props for the `NavigationBar` composition\ninterface NavigationBarProps {\n items: NavItem[]; // Array of navigation items to render\n activeId?: string; // ID of the currently active item\n onItemClick?: (item: NavItem) => void; // Called when a non-disabled item is clicked\n orientation?: 'horizontal' | 'vertical'; // default 'horizontal'; Layout direction\n variant?: 'default' | 'pills' | 'underline'; // default 'default'; Visual style for active/inactive items\n}\n\n// Example:\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// FileUploader \u2014 Props for the `FileUploader` composition\ninterface FileUploaderProps {\n files?: UploadedFile[]; // default []; 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\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=\"\n multiple?: boolean; // default true; When true, allows selecting multiple files at once\n maxSize?: number; // Maximum file size in bytes\n maxFiles?: number; // Maximum number of files\n disabled?: boolean; // When true, the drop zone is visually dimmed (opacity 0\n dragDrop?: boolean; // default true; When true, enables drag-and-drop on the drop zone\n}\n\n// Example:\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// NotificationCenter \u2014 Props for the `NotificationCenter` composition\ninterface NotificationCenterProps {\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\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\n emptyText?: string; // default '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\n// Example:\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// Modal \u2014 Props for the `Modal` composition\ninterface ModalProps {\n open: boolean; // Controls modal visibility\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 (leve...\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, separ...\n size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; // default 'md'; Controls the width of the modal panel\n closeOnOverlayClick?: boolean; // default true; When true, clicking the semi-transparent overlay behind the modal calls `onCl...\n closeOnEscape?: boolean; // default true; When true, pressing the Escape key calls `onClose`\n showCloseButton?: boolean; // default true; When true, renders a ghost close button (X icon) in the modal header\n}\n\n// Example:\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// CommandPalette \u2014 Props for the `CommandPalette` composition\ninterface CommandPaletteProps {\n open: boolean; // Controls palette visibility\n onClose: () => void; // Called to close the palette (overlay click, Escape key, or after command sele...\n commands: Command[]; // Full array of available commands\n onSelect: (command: Command) => void; // Called when a non-disabled command is selected (Enter key or click)\n placeholder?: string; // default 'Search commands...'; Placeholder text for the search input\n recentIds?: string[]; // default []; IDs of recently used commands\n loading?: boolean; // When true, shows a centered `Spinner` instead of the command list\n}\n\n// Example:\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// Footer \u2014 Props for the `Footer` composition\ninterface FooterProps {\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\n bottomLinks?: FooterLink[]; // Links displayed in the bottom bar after social icons (e\n background?: string; // default colors.gray[50]; Background color of the footer\n bordered?: boolean; // default true; When true, renders a 1 px top border in `colors\n}\n\n// Example:\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// Hero \u2014 Props for the `Hero` composition\ninterface HeroProps {\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\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 ...\n align?: 'center' | 'left'; // default 'center'; Text and layout alignment\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls vertical padding and heading/description font sizes\n background?: string; // default 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:...\n overlay?: boolean; // default false; When true and `backgroundImage` is set, renders a semi-transparent black overlay\n}\n\n// Example:\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// Support types:\n// SidebarItem \u2014 A single navigation entry in a `Sidebar`\ninterface SidebarItem {\n id: string; // Unique identifier used for active-state matching and React keys\n label: string; // Display label for the item\n icon?: ReactNode; // Leading icon rendered before the label\n href?: string; // Optional URL associated with this item (not rendered as a link by default)\n badge?: ReactNode; // Trailing badge element (e\n children?: SidebarItem[]; // Nested child items\n disabled?: boolean; // When true, the item is visually dimmed and non-interactive (`cursor: not-allo...\n}\n\n// Comment \u2014 A single comment entry in a `CommentThread`\ninterface Comment {\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\n replies?: Comment[]; // Nested reply comments\n reactions?: { emoji: string; count: number }[]; // Emoji reactions with their aggregated counts (e\n}\n\n// DataTableColumn \u2014 Column definition for a `DataTable`\ninterface DataTableColumn {\n key: string; // Property key on the row object used to extract cell values\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`\n render?: (value: unknown, row: T, index: number) => ReactNode; // Custom cell renderer\n align?: 'left' | 'center' | 'right'; // default 'left'; Text alignment for both header and body cells\n}\n\n// ChatMessage \u2014 A single message in a `ChatWindow`\ninterface ChatMessage {\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\n status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error'; // Delivery status indicator shown on the current user's messages\n}\n\n// NavItem \u2014 A single navigation entry in a `NavigationBar`\ninterface NavItem {\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\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\n}\n\n// UploadedFile \u2014 A file entry in the `FileUploader` composition\ninterface UploadedFile {\n id: string; // Unique identifier for this file entry\n name: string; // Original file name\n size: number; // File size in bytes\n type: string; // MIME type of the file (e\n progress?: number; // Upload progress as a percentage (0-100)\n status: 'pending' | 'uploading' | 'success' | 'error'; // Current upload lifecycle status\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\n// Notification \u2014 A single notification entry in the `NotificationCenter`\ninterface Notification {\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\n read?: boolean; // default false; Read state\n type?: 'info' | 'success' | 'warning' | 'error'; // default 'info'; Semantic type that controls the status dot color on unread notifications\n icon?: ReactNode; // Optional icon rendered alongside the notification (not used by the default im...\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\n// Command \u2014 A single command entry in a `CommandPalette`\ninterface Command {\n id: string; // Unique identifier for this command\n label: string; // Display label for the command\n description?: string; // Optional description text shown below the label\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\n disabled?: boolean; // When true, the command is visually dimmed and cannot be selected\n}\n\n// FooterLink \u2014 A single link entry in a footer column or the bottom bar\ninterface FooterLink {\n label: string; // Display text for the link\n href?: string; // URL for the link\n onClick?: () => void; // Click handler\n}\n\n// FooterColumn \u2014 A named column of links in the `Footer` layout\ninterface FooterColumn {\n title?: string; // Column heading rendered as an `<h4>` with semibold weight\n links: FooterLink[]; // Links displayed in this column\n}\n\n// FooterSocialLink \u2014 A social media link in the `Footer` bottom bar\ninterface FooterSocialLink {\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\n// HeroAction \u2014 A call-to-action button definition for the `Hero` composition\ninterface HeroAction {\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 ...\n}\n\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// System Conventions\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n//\n// Motion: render <MotionKeyframes /> once to enable keyframes.\n// Entrance/exit: fadeIn, fadeOut, slideInUp, slideInDown, scaleIn, scaleOut\n// State feedback: flash, pulse, bounce\n// Easing: linear, easeIn, easeOut, easeInOut, spring\n// Durations: instant(0ms), fast(100ms), normal(200ms), slow(300ms), slower(500ms)\n//\n// Elevation levels (shadow \u2192 z-index):\n// 0: flat 1: sm (cards) 2: md z=1000 (dropdowns, popovers)\n// 3: lg z=1200 4: xl z=1400 5: 2xl z=1800 (tooltips, toasts)\n//\n// useAnimationKey(dep) returns a key that bumps when dep changes \u2014\n// apply to <div key={...}> to replay CSS animations on data update.\n//\n// useMotion() returns { motionEnabled } \u2014 respect prefers-reduced-motion.\n";
|
|
9489
|
+
PRIMITIVES_DOCUMENTATION_TS = "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// GGUI DESIGN SYSTEM \u2014 COMPONENT REFERENCE\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n//\n// CRITICAL \u2014 Props take enum STRING LITERALS, not CSS variables:\n// <Text size=\"sm\" weight=\"bold\" /> \u2713\n// <Text size=\"var(--ggui-font-size-sm)\" /> \u2717 (breaks)\n// The enum strings map to CSS variables internally.\n//\n// CRITICAL \u2014 onChange receives the VALUE directly, not an event:\n// <Input value={x} onChange={setX} /> \u2713\n// <Input onChange={(e) => setX(e.target.value)} /> \u2717\n//\n// Imports allowed: 'react', '@ggui-ai/design', '@ggui-ai/wire'.\n// The WHOLE design system \u2014 primitives, components, compositions,\n// traits \u2014 is one import: import { Card, Grid, Modal, Clickable }\n// from '@ggui-ai/design'. No subpaths. No external libs, no\n// fetch(), no eval().\n//\n// All components accept 'style' + 'className' (omitted below).\n// Structural primitives (Box, Stack, Row, Card) also take a trait via\n// 'as': 'as={Clickable}' adds onClick + keyboard a11y; likewise\n// 'as={Hoverable}' and 'as={Pressable}'. Semantic components\n// (Button/Link/Input) are already interactive and take no 'as'.\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Primitives (import from '@ggui-ai/design')\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// Container \u2014 Width-constrained wrapper that centers content horizontally\ninterface ContainerProps {\n children?: ReactNode;\n maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' | string; // default 'lg'; Maximum width constraint\n center?: boolean; // default true; Whether to center the container horizontally via `margin: 0 auto`\n padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default undefined (no padding); Padding applied to all sides\n}\n\n// Example:\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// Card \u2014 Container with background, shadow, and optional border\ninterface CardProps {\n children?: ReactNode;\n padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default 'lg'; Padding applied to all sides\n shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl'; // default 'sm'; Shadow elevation level\n border?: boolean; // default true; Whether to render a 1px border using `var(--ggui-color-outlineVariant)`\n radius?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | number | string; // default 'lg'; Corner radius\n surface?: 'default' | 'elevated' | 'sunken' | 'accent' | 'inverted' | 'transparent'; // default 'default'; Semantic surface slot\n}\n\n// Example:\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// Stack \u2014 Flexbox layout primitive for arranging children along a single axis\ninterface StackProps {\n children?: ReactNode;\n direction?: 'vertical' | 'horizontal'; // default 'vertical'; Main axis direction\n gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default 'sm'; Gap between children\n align?: 'start' | 'center' | 'end' | 'stretch'; // default 'stretch'; Cross-axis alignment (maps to `align-items`)\n justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'; // default 'start'; Main-axis content distribution (maps to `justify-content`)\n wrap?: boolean; // default false; Whether children wrap to the next line when they overflow\n}\n\n// Example:\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// Grid \u2014 2-D layout primitive\ninterface GridProps {\n children?: ReactNode;\n columns?: number | ResponsiveColumns; // default 2; Column count\n gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default 'md'; Gap between cells\n minColumnWidth?: number | string; // default undefined (use `columns`); When set, the grid becomes responsive \u2014 it fits as many equal\n}\n\n// Example:\n<Grid columns={3} gap=\"md\">\n {items.map((it) => <Card key={it.id}>{it.name}</Card>)}\n</Grid>\n\n// Skeleton \u2014 a pulsing placeholder for content that has not loaded\ninterface SkeletonProps {\n variant?: 'rect' | 'text' | 'circle'; // default 'rect'; Shape preset\n width?: number | string;\n height?: number | string;\n radius?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | number | string; // default 'sm'; Corner radius\n}\n\n// Example:\n{user === undefined\n ? <Skeleton variant=\"text\" width=\"40%\" />\n : <Text>{user.name}</Text>}\n\n// Box \u2014 Generic container with padding, margin, background, and border-radius\ninterface BoxProps {\n children?: ReactNode;\n padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default undefined (no padding); Padding applied to all four sides\n paddingX?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default undefined; Horizontal (left + right) padding\n paddingY?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default undefined; Vertical (top + bottom) padding\n margin?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default undefined (no margin); Margin applied to all four sides\n surface?: 'default' | 'elevated' | 'sunken' | 'accent' | 'inverted' | 'transparent'; // default undefined (transparent); Semantic surface slot\n assetColor?: string; // default undefined; Asset color escape \u2014 the typed valve for legitimate non-theme\n assetSemantic?: string; // default undefined; Human-readable semantic label that documents why\n radius?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | number | string; // default undefined (no rounding); Corner radius\n}\n\n// Example:\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// Divider \u2014 A 1px line to visually separate content sections\ninterface DividerProps {\n orientation?: 'horizontal' | 'vertical'; // default 'horizontal'; Line direction\n margin?: number | string; // default 16; Spacing around the divider\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default undefined (uses `var(--ggui-color-outlineVariant)`); Semantic color slot\n}\n\n// Example:\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// Spacer \u2014 Invisible spacing element, either fixed-size or flexible\ninterface SpacerProps {\n size?: number | 'flex'; // default 16; Spacing amount\n}\n\n// Example:\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// Text \u2014 Versatile typography primitive for body copy, captions, and labels\ninterface TextProps {\n children?: ReactNode;\n size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; // default 'base'; Font size\n weight?: 'normal' | 'medium' | 'semibold' | 'bold'; // default 'normal'; Font weight\n caps?: boolean; // default false; When true, uppercases the text and adds 0\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default 'default' (var(--ggui-color-onContainer)); Semantic color slot\n align?: 'left' | 'center' | 'right'; // default undefined (inherits from parent); Horizontal text alignment\n truncate?: boolean; // default false; When true, clips overflowing text with an ellipsis\n is?: 'p' | 'span' | 'div' | 'label'; // default 'p'; HTML element to render\n id?: string; // `id` for the rendered element \u2014 anchor an in-page link, or pair\n htmlFor?: string; // Associates an `is=\"label\"` element with a form control by the\n}\n\n// Example:\n<Stack gap=\"var(--ggui-spacing-1)\">\n <Text size=\"xs\" weight=\"semibold\" caps tone=\"muted\">Account</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// Heading \u2014 Semantic heading element (h1-h6) with preset typography styles\ninterface HeadingProps {\n children?: ReactNode;\n level?: 1 | 2 | 3 | 4 | 5 | 6; // default 2; Semantic heading level\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default 'default' (var(--ggui-color-onContainer)); Semantic color slot\n align?: 'left' | 'center' | 'right'; // default undefined (inherits from parent); Horizontal text alignment\n}\n\n// Example:\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// Button \u2014 A clickable button primitive with multiple visual variants and sizes\ninterface ButtonProps {\n children?: ReactNode;\n variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'; // default 'primary'; Visual style\n size?: 'xs' | 'sm' | 'md' | 'lg'; // default 'md'; Controls padding, font size, and minimum height:\n fullWidth?: boolean; // default false; When true, sets `width: 100%` so the button fills its container\n loading?: boolean; // default false; When true, replaces children with a 16px `Spinner` (color: `currentColor`)\n leftIcon?: ReactNode; // ReactNode rendered before children, inside the flex layout with `var(--ggui-s...\n rightIcon?: ReactNode; // ReactNode rendered after children, inside the flex layout with `var(--ggui-sp...\n onPress?: () => void; // Alias for `onClick` for cross-platform compatibility (React Native convention)\n}\n\n// Example:\n<Button variant=\"primary\" size=\"md\" leftIcon={<Icon name=\"save\" />} onClick={handleSave}>\n Save Changes\n</Button>\n\n// Input \u2014 A single-line text input with label, validation, and helper text\ninterface InputProps {\n label?: string; // Label rendered above the input\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\n type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search' | 'date' | 'time' | 'datetime-local'; // default 'text'; HTML input type\n error?: string; // Error message displayed below the input in `var(--ggui-color-error-500)`\n helperText?: string; // Helper text displayed below the input in `var(--ggui-color-onSunken)`\n required?: boolean; // default false; When true, appends a red asterisk (`*`) to the label and sets the native\n disabled?: boolean; // default false; When true, sets the native `disabled` attribute\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls padding and font size:\n}\n\n// Example:\n<Input label=\"Email\" type=\"email\" value={email} onChange={setEmail} error={emailError} />\n\n// TextArea \u2014 A multiline text input with label, validation, character count, a...\ninterface TextAreaProps {\n label?: string; // Label rendered above the textarea\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\n rows?: number; // default 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)`\n helperText?: string; // Helper text displayed below the textarea in `var(--ggui-color-onSunken)`\n required?: boolean; // default false; When true, appends a red asterisk (`*`) to the label and sets the native\n disabled?: boolean; // default false; When true, sets the native `disabled` attribute\n maxLength?: number; // Maximum character length (native `maxLength` attribute)\n showCount?: boolean; // default false; When true AND `maxLength` is set, displays a `{current}/{max}` character\n autoResize?: boolean; // default false; When true, sets CSS `resize: none` on the textarea\n}\n\n// Example:\n<TextArea label=\"Bio\" value={bio} onChange={setBio} rows={6} maxLength={500} showCount />\n\n// Select \u2014 A native dropdown selection primitive with label and validation\ninterface SelectProps {\n label?: string; // Label rendered above the select\n value?: string; // Controlled value\n onChange?: (value: string) => void; // Change handler\n options: SelectOption[]; // Array of selectable options\n placeholder?: string; // Placeholder text rendered as a disabled `<option value=\"\">` at the top of the\n error?: string; // Error message displayed below the select in `var(--ggui-color-error-500)`\n helperText?: string; // Helper text displayed below the select in `var(--ggui-color-onSunken)`\n required?: boolean; // default false; When true, appends a red asterisk (`*`) to the label and sets the native\n disabled?: boolean; // default false; When true, sets the native `disabled` attribute\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls padding and font size:\n}\n\n// Example:\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// Checkbox \u2014 A custom-styled checkbox with label and description\ninterface CheckboxProps {\n label?: string; // Primary label text rendered beside the checkbox box\n checked?: boolean; // Controlled checked state\n onChange?: (checked: boolean) => void; // Change handler\n disabled?: boolean; // default false; When true, sets `opacity: 0\n description?: string; // Secondary description text rendered below the label in smaller, muted type\n indeterminate?: boolean; // default false; When true, displays a horizontal dash instead of a checkmark\n}\n\n// Example:\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// Toggle \u2014 A switch/toggle input rendered as a pill-shaped track with a slidin...\ninterface ToggleProps {\n label?: string; // Label text rendered to the right of the toggle track\n checked?: boolean; // Controlled checked (on/off) state\n onChange?: (checked: boolean) => void; // Change handler\n disabled?: boolean; // default false; When true, sets `opacity: 0\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls track and knob dimensions:\n}\n\n// Example:\n<Toggle label=\"Enable notifications\" checked={enabled} onChange={setEnabled} size=\"md\" />\n\n// RadioGroup \u2014 A group of mutually exclusive radio options with optional label...\ninterface RadioGroupProps {\n label?: string; // Group label rendered above the options\n value?: string; // Controlled value\n onChange?: (value: string) => void; // Change handler\n options: RadioOption[]; // Array of radio options\n direction?: 'vertical' | 'horizontal'; // default 'vertical'; Layout direction for the options container:\n disabled?: boolean; // default false; When true, disables ALL options (individual `RadioOption\n error?: string; // Error message displayed below all options in `var(--ggui-color-error-500)`\n}\n\n// Example:\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// Slider \u2014 A range input with a custom-styled track, fill, and thumb\ninterface SliderProps {\n label?: string; // Label rendered above the slider track (left-aligned)\n value?: number; // default 0; Controlled numeric value\n onChange?: (value: number) => void; // Change handler\n min?: number; // default 0; Minimum allowed value\n max?: number; // default 100; Maximum allowed value\n step?: number; // default 1; Step increment for the slider\n disabled?: boolean; // default false; When true, sets `cursor: not-allowed` on the native input\n showValue?: boolean; // default false; When true, displays the current numeric value right-aligned in the header row\n}\n\n// Example:\n<Slider label=\"Volume\" value={volume} onChange={setVolume} min={0} max={100} step={5} showValue />\n\n// Badge \u2014 Inline label for status indicators, counts, or categories\ninterface BadgeProps {\n children?: ReactNode;\n variant?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'tertiary'; // default 'default'; Visual style\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls padding and font size:\n pill?: boolean; // default true; When true, uses fully rounded corners (`border-radius: 9999px`)\n}\n\n// Example:\n<Badge variant=\"success\" size=\"sm\">Active</Badge>\n\n// Spinner \u2014 Animated SVG loading indicator\ninterface SpinnerProps {\n size?: number; // default 24; Width and height of the SVG element in pixels\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default undefined (uses `var(--ggui-color-primary-600)`); Semantic color slot for the animated foreground arc\n}\n\n// Example:\n<Spinner size={32} tone=\"success\" />\n\n// Avatar \u2014 User or entity representation with image or auto-generated initials\ninterface AvatarProps {\n src?: string; // Image URL\n name?: string; // Name used for two purposes:\n size?: number | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; // default 'md'; Avatar dimensions\n shape?: 'circle' | 'square'; // default 'circle'; Container shape\n}\n\n// Example:\n<Avatar src=\"/photos/jane.jpg\" name=\"Jane Doe\" size=\"lg\" shape=\"circle\" />\n\n// Alert \u2014 Contextual message box for important information with icon and optio...\ninterface AlertProps {\n children?: ReactNode;\n variant?: 'info' | 'success' | 'warning' | 'error'; // default 'info'; Visual style\n title?: string; // Optional title rendered above the body in semibold (`var(--ggui-font-weight-s...\n closable?: boolean; // default false; When true, renders a close button (X icon) in the top-right area\n onClose?: () => void; // Callback fired when the close button is clicked\n icon?: ReactNode; // Custom icon ReactNode to replace the default variant icon\n}\n\n// Example:\n<Alert variant=\"warning\" title=\"Rate limit\" closable onClose={() => setShow(false)}>\n You have 3 requests remaining this minute.\n</Alert>\n\n// Progress \u2014 Horizontal progress bar with determinate and indeterminate modes\ninterface ProgressProps {\n value: number; // Current progress value\n max?: number; // default 100; Maximum value representing 100% progress\n variant?: 'default' | 'success' | 'warning' | 'error'; // default 'default'; Fill bar color\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls track height in pixels:\n label?: string; // Accessible name describing what this bar measures, e\n showLabel?: boolean; // default false; When true, displays a header row above the track with the `label` text\n indeterminate?: boolean; // default false; When true, ignores `value` for visual width and plays a looping animation\n}\n\n// Example:\n<Progress value={65} variant=\"success\" size=\"md\" showLabel />\n\n// Image \u2014 An `<img>` element with built-in error handling and fallback support\ninterface ImageProps {\n src: string; // Image source URL\n alt: string; // Alt text for the image\n width?: number | string; // default '100%' (applied at render time, not on the type); Image width\n height?: number | string; // default 'auto' (applied at render time, not on the type); Image height\n objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; // default '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\n fallback?: ReactNode; // Custom ReactNode rendered when the image fails to load\n}\n\n// Example:\n<Image src=\"/hero.jpg\" alt=\"Hero banner\" width=\"100%\" height={400} objectFit=\"cover\" radius=\"md\" />\n\n// Icon \u2014 185 Lucide icons + emoji passthrough\ninterface IconProps {\n name?: string; // Lucide icon name (kebab-case, camelCase, or PascalCase all work)\n size?: number; // default 24; Icon dimensions in pixels (applied to both width and height of the wrapper\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default undefined (icon uses `currentColor`); Semantic color slot\n children?: ReactNode; // Custom SVG children\n 'aria-label'?: string; // Accessible name for a standalone, meaning-bearing icon\n}\n\n// Example:\n<Icon name=\"search\" size={20} tone=\"muted\" />\n<Icon name=\"cloud-rain\" size={32} />\n<Icon name=\"\u2600\uFE0F\" size={24} />\n\n// Link \u2014 Styled anchor element with external link support\ninterface LinkProps {\n children?: ReactNode;\n href: string; // Destination URL\n external?: boolean; // default false; When true, opens link in a new tab (`target=\"_blank\"`, `rel=\"noopener norefer...\n tone?: | 'default' | 'muted' | 'subtle' | 'emphasized' | 'loud' | 'success' | 'warning' | 'error' | 'info' | 'inverse' | 'inherit'; // default undefined (uses `var(--ggui-color-primary-600)`); Semantic color slot for the link text\n underline?: 'always' | 'hover' | 'none'; // default 'hover'; Underline behavior:\n}\n\n// Example:\n<Link href=\"https://docs.ggui.ai\" external>Documentation</Link>\n\n// Tooltip \u2014 Hoverable information popup positioned relative to a trigger element\ninterface TooltipProps {\n children: ReactNode; // Trigger element\n content: ReactNode; // Tooltip content\n position?: 'top' | 'bottom' | 'left' | 'right'; // default 'top'; Tooltip placement relative to the trigger element:\n delay?: number; // default 200; Delay in milliseconds before the tooltip becomes visible after hover/focus\n}\n\n// Example:\n<Tooltip content=\"Copy to clipboard\" position=\"top\">\n <Button variant=\"ghost\"><Icon name=\"copy\" /></Button>\n</Tooltip>\n\n// Table \u2014 Data table with sortable columns, striped rows, and hover highlights\ninterface TableProps {\n columns: TableColumn<T>[]; // Array of column definitions controlling header labels, data keys, and rendering\n data: T[]; // Array of row data objects\n sortKey?: string; // The `key` of the currently sorted column\n sortDirection?: SortDirection; // default 'asc'; Current sort direction for the column identified by `sortKey`\n onSort?: (key: string, direction: SortDirection) => void; // Sort change handler\n striped?: boolean; // default false; When true, alternating rows (odd index) get a\n hoverable?: boolean; // default true; When true, rows highlight with `var(--ggui-color-container)`\n compact?: boolean; // default false; When true, reduces cell padding:\n bordered?: boolean; // default false; When true, adds a 1px border around the table wrapper and between cells\n caption?: string; // Accessible table caption\n}\n\n// Example:\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// Tabs \u2014 Accessible tab navigation with panels and keyboard support\ninterface TabsProps {\n items: TabItem[]; // Array of tab definitions\n activeKey?: string; // Controlled active tab key\n onChange?: (key: string) => void; // Tab change handler\n variant?: 'line' | 'pills' | 'enclosed'; // default 'line'; Visual style of the tab bar:\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls tab button padding and font size:\n fullWidth?: boolean; // default false; When true, tab buttons expand equally to fill the container width\n}\n\n// Example:\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// Toast \u2014 Notification banner with auto-dismiss and slide-in animation\ninterface ToastProps {\n message: ReactNode; // Message body content\n variant?: 'info' | 'success' | 'warning' | 'error'; // default 'info'; Visual style\n title?: string; // Optional title rendered above the message in semibold\n duration?: number; // default 5000; Auto-dismiss delay in milliseconds\n onClose?: () => void; // Callback fired on auto-dismiss timeout or when the close button is clicked\n visible?: boolean; // default true; Controls rendering\n position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center'; // Intended screen position\n}\n\n// Example:\n<Toast variant=\"success\" title=\"Saved\" message=\"Your changes have been saved.\" onClose={() => setShow(false)} />\n\n// Accordion \u2014 Collapsible content sections with chevron rotation animation\ninterface AccordionProps {\n items: AccordionItem[]; // Array of collapsible section definitions\n expandedKeys?: string[]; // Controlled expanded state\n onChange?: (expandedKeys: string[]) => void; // Expand/collapse handler\n multiple?: boolean; // default false; When true, multiple items can be open simultaneously\n variant?: 'default' | 'bordered' | 'separated'; // default 'default'; Visual style controlling borders and spacing:\n}\n\n// Example:\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// Row \u2014 Convenience alias for a horizontal Stack\ninterface RowProps {\n children?: ReactNode;\n gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | number | string; // default 'sm'; Gap between children\n align?: 'start' | 'center' | 'end' | 'stretch'; // default 'stretch'; Cross-axis alignment (maps to `align-items`)\n justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'; // default 'start'; Main-axis content distribution (maps to `justify-content`)\n wrap?: boolean; // default false; Whether children wrap to the next line when they overflow\n}\n\n// Example:\n<Row gap=\"md\" align=\"center\" justify=\"between\">\n <Text size=\"sm\" weight=\"medium\">Total</Text>\n <Text weight=\"bold\">$42.00</Text>\n</Row>\n\n// Support types:\n// ResponsiveColumns \u2014 Explicit column count per viewport breakpoint, mobile-first\ninterface ResponsiveColumns {\n base?: number; // default 1; 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\n// SelectOption \u2014 An individual option within a `Select` dropdown\ninterface SelectOption {\n value: string; // The value submitted when this option is selected\n label: string; // The display text shown in the dropdown\n disabled?: boolean; // default false; When true, the option is visible but cannot be selected (grayed out by the br...\n}\n\n// RadioOption \u2014 An individual option within a `RadioGroup`\ninterface RadioOption {\n value: string; // The value emitted via `RadioGroupProps\n label: string; // Display text for this option\n description?: string; // Optional secondary description rendered below the label in smaller, muted type\n disabled?: boolean; // default false; When true, this individual option is grayed out (`opacity: 0\n}\n\n// TableColumn \u2014 Column definition for the Table component\ninterface TableColumn {\n key: string; // Property key in the row data object\n header: string; // Column header text\n width?: number | string; // Fixed column width\n align?: 'left' | 'center' | 'right'; // default 'left'; Horizontal text alignment for both the header and data cells\n sortable?: boolean; // default false; When true, the header cell becomes clickable and shows sort direction\n render?: (value: unknown, row: T, index: number) => ReactNode; // Custom cell renderer\n}\n\n// TabItem \u2014 Definition of a single tab within a Tabs component\ninterface TabItem {\n key: string; // Unique identifier for this tab\n label: ReactNode; // Tab button label\n content: ReactNode; // Panel content rendered below the tab bar when this tab is active\n disabled?: boolean; // default false; When true, the tab button shows `opacity: 0\n icon?: ReactNode; // Optional icon rendered before the label inside the tab button, with\n}\n\n// AccordionItem \u2014 Definition of a single collapsible section within an Accordion\ninterface AccordionItem {\n key: string; // Unique identifier for this item\n title: ReactNode; // Header label rendered inside the toggle button\n content: ReactNode; // Panel content rendered below the header when expanded\n disabled?: boolean; // default false; When true, the header button shows `opacity: 0\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Components (import from '@ggui-ai/design')\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// SearchField \u2014 A text input with a leading search icon and optional submit button\ninterface SearchFieldProps {\n value?: string; // Current search value\n onChange?: (value: string) => void; // Called on every keystroke with the new input string (value directly, not\n onSearch?: (value: string) => void; // Called when the user presses **Enter** or clicks the search button\n placeholder?: string; // default 'Search...'; Placeholder text shown when the input is empty\n showButton?: boolean; // default false; When `true`, renders a `Button` primitive to the right of the input\n buttonText?: string; // default 'Search'; Label rendered inside the submit button\n loading?: boolean; // When `true`, replaces the search icon with a `Spinner`, disables the\n disabled?: boolean; // When `true`, the input and button are visually disabled and do not\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls input height and font size\n}\n\n// Example:\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// FormField \u2014 A wrapper that adds a label, optional description, error message,\ninterface FormFieldProps {\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) re...\n error?: string; // Error message displayed below `children` in `colors\n helperText?: string; // Neutral guidance text displayed below `children` in `colors\n required?: boolean; // When `true`, appends a red asterisk (`*`) after the label text\n description?: string; // Secondary description rendered between the label and the child control\n}\n\n// Example:\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// MenuItem \u2014 A full-width clickable row for menus, sidebars, and action lists\ninterface MenuItemProps {\n label: string; // Primary text content of the menu item\n icon?: ReactNode; // Icon or element rendered to the left of the label\n rightElement?: ReactNode; // Element rendered to the right of the label (e\n onClick?: () => void; // Called when the item is clicked\n disabled?: boolean; // When `true`, the item is non-interactive: `colors\n active?: boolean; // Marks this item as the current selection\n danger?: boolean; // Switches the item to destructive styling: `colors\n}\n\n// Example:\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// Tag \u2014 An inline label for categories, filters, statuses, or selections\ninterface TagProps {\n children: ReactNode; // Tag content -- typically a short text string\n variant?: 'default' | 'primary' | 'success' | 'warning' | 'error' | 'info'; // default 'default'; Semantic color variant applied to background, text, and border\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls padding, font size, and internal gap\n closable?: boolean; // When `true`, renders a small close (\"x\") button after the content\n onClose?: () => void; // Called when the close button is clicked\n icon?: ReactNode; // Icon or element rendered before the text content\n}\n\n// Example:\n```tsx\n<Tag variant=\"success\" size=\"md\" closable onClose={() => removeFilter(id)}>\n Active\n</Tag>\n```\n\n// Dropdown \u2014 A click-triggered menu anchored to a trigger element\ninterface DropdownProps {\n trigger: ReactNode; // The element the user clicks to open the menu\n options: DropdownOption[]; // Array of selectable options rendered as `MenuItem` rows\n value?: string; // The `value` of the currently selected option\n onChange?: (value: string) => void; // Called with the `value` string of the selected option (not the full\n placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'; // default 'bottom-start'; Where to anchor the menu panel relative to the trigger\n disabled?: boolean; // When `true`, the trigger shows `cursor: not-allowed` and clicking\n}\n\n// Example:\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// Autocomplete \u2014 A text input with a filterable suggestion dropdown, keyboard navigation,\ninterface AutocompleteProps {\n value?: string; // default ''; Current text in the input field\n onChange?: (value: string) => void; // Called on every keystroke with the new input string (value directly,\n onSelect?: (option: AutocompleteOption) => void; // Called when the user selects an option (click or Enter on highlighted\n options: AutocompleteOption[]; // The full list of available options\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\n disabled?: boolean; // When `true`, the inner `Input` is disabled and the dropdown\n error?: string; // Error message forwarded to the inner `Input` primitive\n noResultsText?: string; // default 'No results found'; Text shown in the dropdown when filtering produces zero matches\n}\n\n// Example:\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// Breadcrumb \u2014 A horizontal navigation trail showing the user's location within a\ninterface BreadcrumbProps {\n items: BreadcrumbItem[]; // Ordered array of breadcrumb segments from root to current page\n separator?: ReactNode; // default '/'; Separator rendered between each pair of items\n onItemClick?: (item: BreadcrumbItem, index: number) => void; // Called when a non-last item is clicked\n}\n\n// Example:\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// Pagination \u2014 Page navigation controls with previous/next arrows, numbered page buttons,\ninterface PaginationProps {\n currentPage: number; // Current active page\n totalPages: number; // Total number of pages\n onPageChange?: (page: number) => void; // Called when the user clicks a page number, arrow, or first/last button\n showFirstLast?: boolean; // default true; When `true`, renders double-chevron buttons for jumping to the first\n maxVisible?: number; // default 5; Maximum number of page buttons visible at once (including the first\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls button dimensions and icon sizes\n disabled?: boolean; // When `true`, all page buttons and arrows are visually dimmed\n}\n\n// Example:\n```tsx\n<Pagination\n currentPage={page}\n totalPages={20}\n onPageChange={setPage}\n maxVisible={7}\n size=\"md\"\n/>\n```\n\n// EmptyState \u2014 placeholder for a region with no data: empty lists,\ninterface EmptyStateProps {\n icon?: string | ReactNode; // A Lucide icon name (kebab-case), rendered large and subtle above\n title: string; // The headline, e\n description?: string; // Optional supporting line below the title\n action?: ReactNode; // Optional call-to-action, typically a `<Button>`\n}\n\n// Example:\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// Stat \u2014 a single KPI / metric: a label, a large value, an optional\ninterface StatProps {\n label: string; // The metric name, e\n value: string | number; // The headline value \u2014 the big number\n delta?: string; // Optional change indicator, pre-formatted, e\n trend?: 'up' | 'down' | 'neutral'; // default 'neutral'; Direction of `delta` \u2014 colours it: `'up'` success, `'down'` error,\n icon?: string | ReactNode; // Optional Lucide icon name (kebab-case) or custom node, shown next\n}\n\n// Example:\n<Stat label=\"Revenue\" value=\"$48.2k\" delta=\"+12.5%\" trend=\"up\" icon=\"trending-up\" />\n\n// Stepper \u2014 a display-only step indicator for multi-step flows\ninterface StepperProps {\n steps: string[]; // Ordered step labels, one per step\n current: number; // Zero-based index of the active step\n orientation?: 'horizontal' | 'vertical'; // default 'horizontal'; Flow direction\n onStepClick?: (index: number) => void; // When provided, each step becomes a real button and clicks receive\n}\n\n// Example:\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// Markdown \u2014 renders markdown-formatted text as styled content:\ninterface MarkdownProps {\n markdown: string; // Markdown source text, passed through verbatim\n}\n\n// Example:\n<Stack gap=\"md\">\n <Markdown markdown={recipe.instructions} />\n</Stack>\n\n// MarkdownInline \u2014 renders ONE line of inline-level markdown (bold,\ninterface MarkdownInlineProps {\n markdown: string; // Inline markdown source -- bold, italic, inline code, and\n}\n\n// Example:\n<Row gap=\"sm\">\n <Badge variant=\"success\">stable</Badge>\n <MarkdownInline markdown={release.summary} />\n</Row>\n\n// Support types:\n// DropdownOption \u2014 A single option inside a `Dropdown`\ninterface DropdownOption {\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\n disabled?: boolean; // When `true`, the option is visible but non-interactive\n danger?: boolean; // When `true`, the option uses destructive (red) styling via\n}\n\n// AutocompleteOption \u2014 A single option in the `Autocomplete` suggestion list\ninterface AutocompleteOption {\n value: string; // Unique identifier for this option\n label: string; // Primary display text\n description?: string; // Secondary description rendered below the label in `fontSize\n icon?: ReactNode; // Icon rendered to the left of the label/description block\n disabled?: boolean; // When `true`, the option is visible but non-interactive (`cursor: not-allowed`...\n}\n\n// BreadcrumbItem \u2014 A single segment in a `Breadcrumb` trail\ninterface BreadcrumbItem {\n label: string; // Display text for this breadcrumb segment\n href?: string; // URL for this segment\n icon?: ReactNode; // Icon rendered immediately before the label\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Compositions (import from '@ggui-ai/design')\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// Header \u2014 Props for the `Header` composition\ninterface HeaderProps {\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 l...\n actions?: ReactNode; // Right-side action elements (buttons, avatar, etc\n sticky?: boolean; // default false; When true, the header becomes `position: sticky` at the top of its scroll con...\n background?: string; // default colors.white; Background color of the header\n bordered?: boolean; // default true; When true, renders a 1 px bottom border in `colors\n}\n\n// Example:\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// Sidebar \u2014 Props for the `Sidebar` composition\ninterface SidebarProps {\n items: SidebarItem[]; // Array of navigation items to render\n activeId?: string; // ID of the currently active item\n onItemClick?: (item: SidebarItem) => void; // Called when any item (including parent items with children) is clicked\n collapsed?: boolean; // default false; When true, hides labels and badges; only icons remain visible, centered in th...\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; // default 256; Width in pixels when expanded\n collapsedWidth?: number; // default 64; Width in pixels when collapsed\n}\n\n// Example:\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// CardGrid \u2014 Props for the `CardGrid` composition\ninterface CardGridProps {\n children: ReactNode; // Card elements to arrange in the grid\n columns?: number | { sm?: number; md?: number; lg?: number }; // default 3; Number of columns, or a responsive breakpoint map\n gap?: number; // default 16; Gap between grid items in pixels\n}\n\n// Example:\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// CommentThread \u2014 Props for the `CommentThread` composition\ninterface CommentThreadProps {\n comments: Comment[]; // Array of top-level comments to render\n currentUser?: { /** Display name for the current user's avatar. */ name: string; /** Avatar image URL for the current user. */ avatar?: string; }; // Current user info\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\n// Example:\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// DataTable \u2014 Props for the `DataTable` composition\ninterface DataTableProps {\n columns: DataTableColumn<T>[]; // Column definitions that control header, alignment, sorting, and rendering\n data: T[]; // Row data array\n rowKey?: string | ((row: T) => string); // default '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; // default '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\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\n selectable?: boolean; // When true, adds a checkbox column at the start of each row\n selectedKeys?: string[]; // default []; 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-...\n}\n\n// Example:\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// ChatWindow \u2014 Props for the `ChatWindow` composition\ninterface ChatWindowProps {\n messages: ChatMessage[]; // Array of chat messages to display in chronological order\n currentUserId: string; // ID of the current user\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\n placeholder?: string; // default 'Type a message...'; Placeholder text for the message input field\n header?: ReactNode; // Optional header content rendered above the message area, separated by a botto...\n}\n\n// Example:\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// NavigationBar \u2014 Props for the `NavigationBar` composition\ninterface NavigationBarProps {\n items: NavItem[]; // Array of navigation items to render\n activeId?: string; // ID of the currently active item\n onItemClick?: (item: NavItem) => void; // Called when a non-disabled item is clicked\n orientation?: 'horizontal' | 'vertical'; // default 'horizontal'; Layout direction\n variant?: 'default' | 'pills' | 'underline'; // default 'default'; Visual style for active/inactive items\n}\n\n// Example:\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// FileUploader \u2014 Props for the `FileUploader` composition\ninterface FileUploaderProps {\n files?: UploadedFile[]; // default []; 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\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=\"\n multiple?: boolean; // default true; When true, allows selecting multiple files at once\n maxSize?: number; // Maximum file size in bytes\n maxFiles?: number; // Maximum number of files\n disabled?: boolean; // When true, the drop zone is visually dimmed (opacity 0\n dragDrop?: boolean; // default true; When true, enables drag-and-drop on the drop zone\n}\n\n// Example:\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// NotificationCenter \u2014 Props for the `NotificationCenter` composition\ninterface NotificationCenterProps {\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\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\n emptyText?: string; // default '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\n// Example:\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// Modal \u2014 Props for the `Modal` composition\ninterface ModalProps {\n open: boolean; // Controls modal visibility\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 (leve...\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, separ...\n size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; // default 'md'; Controls the width of the modal panel\n closeOnOverlayClick?: boolean; // default true; When true, clicking the semi-transparent overlay behind the modal calls `onCl...\n closeOnEscape?: boolean; // default true; When true, pressing the Escape key calls `onClose`\n showCloseButton?: boolean; // default true; When true, renders a ghost close button (X icon) in the modal header\n}\n\n// Example:\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// CommandPalette \u2014 Props for the `CommandPalette` composition\ninterface CommandPaletteProps {\n open: boolean; // Controls palette visibility\n onClose: () => void; // Called to close the palette (overlay click, Escape key, or after command sele...\n commands: Command[]; // Full array of available commands\n onSelect: (command: Command) => void; // Called when a non-disabled command is selected (Enter key or click)\n placeholder?: string; // default 'Search commands...'; Placeholder text for the search input\n recentIds?: string[]; // default []; IDs of recently used commands\n loading?: boolean; // When true, shows a centered `Spinner` instead of the command list\n}\n\n// Example:\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// Footer \u2014 Props for the `Footer` composition\ninterface FooterProps {\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\n bottomLinks?: FooterLink[]; // Links displayed in the bottom bar after social icons (e\n background?: string; // default colors.gray[50]; Background color of the footer\n bordered?: boolean; // default true; When true, renders a 1 px top border in `colors\n}\n\n// Example:\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// Hero \u2014 Props for the `Hero` composition\ninterface HeroProps {\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\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 ...\n align?: 'center' | 'left'; // default 'center'; Text and layout alignment\n size?: 'sm' | 'md' | 'lg'; // default 'md'; Controls vertical padding and heading/description font sizes\n background?: string; // default 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:...\n overlay?: boolean; // default false; When true and `backgroundImage` is set, renders a semi-transparent black overlay\n}\n\n// Example:\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// Support types:\n// SidebarItem \u2014 A single navigation entry in a `Sidebar`\ninterface SidebarItem {\n id: string; // Unique identifier used for active-state matching and React keys\n label: string; // Display label for the item\n icon?: ReactNode; // Leading icon rendered before the label\n href?: string; // Optional URL associated with this item (not rendered as a link by default)\n badge?: ReactNode; // Trailing badge element (e\n children?: SidebarItem[]; // Nested child items\n disabled?: boolean; // When true, the item is visually dimmed and non-interactive (`cursor: not-allo...\n}\n\n// Comment \u2014 A single comment entry in a `CommentThread`\ninterface Comment {\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\n replies?: Comment[]; // Nested reply comments\n reactions?: { emoji: string; count: number }[]; // Emoji reactions with their aggregated counts (e\n}\n\n// DataTableColumn \u2014 Column definition for a `DataTable`\ninterface DataTableColumn {\n key: string; // Property key on the row object used to extract cell values\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`\n render?: (value: unknown, row: T, index: number) => ReactNode; // Custom cell renderer\n align?: 'left' | 'center' | 'right'; // default 'left'; Text alignment for both header and body cells\n}\n\n// ChatMessage \u2014 A single message in a `ChatWindow`\ninterface ChatMessage {\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\n status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error'; // Delivery status indicator shown on the current user's messages\n}\n\n// NavItem \u2014 A single navigation entry in a `NavigationBar`\ninterface NavItem {\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\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\n}\n\n// UploadedFile \u2014 A file entry in the `FileUploader` composition\ninterface UploadedFile {\n id: string; // Unique identifier for this file entry\n name: string; // Original file name\n size: number; // File size in bytes\n type: string; // MIME type of the file (e\n progress?: number; // Upload progress as a percentage (0-100)\n status: 'pending' | 'uploading' | 'success' | 'error'; // Current upload lifecycle status\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\n// Notification \u2014 A single notification entry in the `NotificationCenter`\ninterface Notification {\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\n read?: boolean; // default false; Read state\n type?: 'info' | 'success' | 'warning' | 'error'; // default 'info'; Semantic type that controls the status dot color on unread notifications\n icon?: ReactNode; // Optional icon rendered alongside the notification (not used by the default im...\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\n// Command \u2014 A single command entry in a `CommandPalette`\ninterface Command {\n id: string; // Unique identifier for this command\n label: string; // Display label for the command\n description?: string; // Optional description text shown below the label\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\n disabled?: boolean; // When true, the command is visually dimmed and cannot be selected\n}\n\n// FooterLink \u2014 A single link entry in a footer column or the bottom bar\ninterface FooterLink {\n label: string; // Display text for the link\n href?: string; // URL for the link\n onClick?: () => void; // Click handler\n}\n\n// FooterColumn \u2014 A named column of links in the `Footer` layout\ninterface FooterColumn {\n title?: string; // Column heading rendered as an `<h4>` with semibold weight\n links: FooterLink[]; // Links displayed in this column\n}\n\n// FooterSocialLink \u2014 A social media link in the `Footer` bottom bar\ninterface FooterSocialLink {\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\n// HeroAction \u2014 A call-to-action button definition for the `Hero` composition\ninterface HeroAction {\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 ...\n}\n\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// System Conventions\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n//\n// Motion: render <MotionKeyframes /> once to enable keyframes.\n// Entrance/exit: fadeIn, fadeOut, slideInUp, slideInDown, scaleIn, scaleOut\n// State feedback: flash, pulse, bounce\n// Easing: linear, easeIn, easeOut, easeInOut, spring\n// Durations: instant(0ms), fast(100ms), normal(200ms), slow(300ms), slower(500ms)\n//\n// Elevation levels (shadow \u2192 z-index):\n// 0: flat 1: sm (cards) 2: md z=1000 (dropdowns, popovers)\n// 3: lg z=1200 4: xl z=1400 5: 2xl z=1800 (tooltips, toasts)\n//\n// useAnimationKey(dep) returns a key that bumps when dep changes \u2014\n// apply to <div key={...}> to replay CSS animations on data update.\n//\n// useMotion() returns { motionEnabled } \u2014 respect prefers-reduced-motion.\n";
|
|
9226
9490
|
}
|
|
9227
9491
|
});
|
|
9228
9492
|
|
|
@@ -9840,7 +10104,8 @@ async function runCheck(input) {
|
|
|
9840
10104
|
compiledCode,
|
|
9841
10105
|
contract,
|
|
9842
10106
|
originalPrompt: prompt,
|
|
9843
|
-
classification: harness.classification
|
|
10107
|
+
classification: harness.classification,
|
|
10108
|
+
designMode: harness.designMode
|
|
9844
10109
|
};
|
|
9845
10110
|
let axisIssueCount = 0;
|
|
9846
10111
|
const seenAxisIds = /* @__PURE__ */ new Set();
|
|
@@ -10391,6 +10656,9 @@ function compose(classification) {
|
|
|
10391
10656
|
fragments: matched
|
|
10392
10657
|
};
|
|
10393
10658
|
}
|
|
10659
|
+
|
|
10660
|
+
// src/boilerplate/render.ts
|
|
10661
|
+
init_design_mode();
|
|
10394
10662
|
var __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
10395
10663
|
var TEMPLATE_DIRS = [
|
|
10396
10664
|
resolve(__dirname$1, "templates"),
|
|
@@ -10399,19 +10667,31 @@ var TEMPLATE_DIRS = [
|
|
|
10399
10667
|
resolve(__dirname$1, "..", "..", "..", "src", "boilerplate", "templates"),
|
|
10400
10668
|
resolve(__dirname$1, "..", "..", "..", "..", "src", "boilerplate", "templates")
|
|
10401
10669
|
];
|
|
10402
|
-
var
|
|
10670
|
+
var BASE_TEMPLATE_FILE = {
|
|
10671
|
+
constrained: "base.tsx.tmpl",
|
|
10672
|
+
free: "base-free.tsx.tmpl"
|
|
10673
|
+
};
|
|
10674
|
+
var FREE_LAYOUT = [
|
|
10675
|
+
" <div style={{ width: '100%' }}>",
|
|
10676
|
+
" {/* implement your UI here */}",
|
|
10677
|
+
" </div>"
|
|
10678
|
+
].join("\n");
|
|
10679
|
+
var baseCache = /* @__PURE__ */ new Map();
|
|
10403
10680
|
var layoutCache = /* @__PURE__ */ new Map();
|
|
10404
|
-
function loadBase() {
|
|
10405
|
-
|
|
10681
|
+
function loadBase(designMode) {
|
|
10682
|
+
const cached = baseCache.get(designMode);
|
|
10683
|
+
if (cached !== void 0) return cached;
|
|
10684
|
+
const file = BASE_TEMPLATE_FILE[designMode];
|
|
10406
10685
|
for (const dir of TEMPLATE_DIRS) {
|
|
10407
10686
|
try {
|
|
10408
|
-
|
|
10409
|
-
|
|
10687
|
+
const content = readFileSync(resolve(dir, file), "utf-8");
|
|
10688
|
+
baseCache.set(designMode, content);
|
|
10689
|
+
return content;
|
|
10410
10690
|
} catch {
|
|
10411
10691
|
continue;
|
|
10412
10692
|
}
|
|
10413
10693
|
}
|
|
10414
|
-
throw new Error(
|
|
10694
|
+
throw new Error(`No ${file} found`);
|
|
10415
10695
|
}
|
|
10416
10696
|
function loadLayout(shellType, screen) {
|
|
10417
10697
|
const key = `${shellType}-${screen}`;
|
|
@@ -10435,9 +10715,9 @@ function loadLayout(shellType, screen) {
|
|
|
10435
10715
|
}
|
|
10436
10716
|
throw new Error(`No layout found for ${key}`);
|
|
10437
10717
|
}
|
|
10438
|
-
function renderBoilerplate(shellType, screen, markers) {
|
|
10439
|
-
let template = loadBase();
|
|
10440
|
-
const layout = loadLayout(shellType, screen);
|
|
10718
|
+
function renderBoilerplate(shellType, screen, markers, designMode = DEFAULT_DESIGN_MODE) {
|
|
10719
|
+
let template = loadBase(designMode);
|
|
10720
|
+
const layout = designMode === "free" ? FREE_LAYOUT : loadLayout(shellType, screen);
|
|
10441
10721
|
template = template.replace("{{LAYOUT}}", layout);
|
|
10442
10722
|
for (const [key, value] of Object.entries(markers)) {
|
|
10443
10723
|
template = template.replaceAll(`{{${key}}}`, value);
|
|
@@ -10447,6 +10727,7 @@ function renderBoilerplate(shellType, screen, markers) {
|
|
|
10447
10727
|
|
|
10448
10728
|
// src/boilerplate/generate.ts
|
|
10449
10729
|
init_json_schema_ts();
|
|
10730
|
+
init_design_mode();
|
|
10450
10731
|
var ALL_PRIMITIVES = [
|
|
10451
10732
|
"Container",
|
|
10452
10733
|
"Card",
|
|
@@ -10545,7 +10826,7 @@ function inferTypeFromExample(example) {
|
|
|
10545
10826
|
}
|
|
10546
10827
|
return `{ ${fields.join("; ")} }`;
|
|
10547
10828
|
}
|
|
10548
|
-
function generateBoilerplate(_userPrompt, contract, shellType, screen, composedSections, appGadgets) {
|
|
10829
|
+
function generateBoilerplate(_userPrompt, contract, shellType, screen, composedSections, appGadgets, designMode = DEFAULT_DESIGN_MODE) {
|
|
10549
10830
|
const propsFields = [];
|
|
10550
10831
|
const propsProperties = contract?.propsSpec?.properties ?? {};
|
|
10551
10832
|
for (const [key, entry] of Object.entries(propsProperties)) {
|
|
@@ -10727,7 +11008,7 @@ ${body}
|
|
|
10727
11008
|
CONTEXT_HOOKS: contextHooks,
|
|
10728
11009
|
WIRE_HOOKS: hookBody,
|
|
10729
11010
|
AXIS_SECTIONS: composedSections ?? ""
|
|
10730
|
-
});
|
|
11011
|
+
}, designMode);
|
|
10731
11012
|
}
|
|
10732
11013
|
|
|
10733
11014
|
// src/boilerplate.ts
|
|
@@ -10735,6 +11016,431 @@ init_json_schema_ts();
|
|
|
10735
11016
|
|
|
10736
11017
|
// src/boilerplate/system-prompt.ts
|
|
10737
11018
|
init_types_public();
|
|
11019
|
+
init_design_mode();
|
|
11020
|
+
|
|
11021
|
+
// src/boilerplate/free-design-prompt.ts
|
|
11022
|
+
init_design_mode();
|
|
11023
|
+
|
|
11024
|
+
// src/boilerplate/hard-sections.ts
|
|
11025
|
+
var PROTOCOL_NOTES = `## Protocol Notes
|
|
11026
|
+
The boilerplate pre-declares every wire hook the contract requires (\`useAction\`, \`useStream\`, \`useGguiContext\`, plus capability hooks from \`@ggui-ai/gadgets\` when the contract declares \`clientCapabilities\`). Three rules:
|
|
11027
|
+
1. **Do NOT delete any pre-declared hook.** \`self_check\` fails with \`wire_preservation:<kind>:<name>\` if you remove one.
|
|
11028
|
+
2. **Consume every hook binding** somewhere in the component \u2014 in JSX, a callback, or an effect. Unused bindings fail lint with \`no-unused-vars\`.
|
|
11029
|
+
3. **Do NOT invent new wire calls.** Every \`useAction('X')\`, \`useStream('X')\`, \`useGguiContext('X')\` etc. MUST correspond to a declared entry on the contract. Calling one that isn't declared fails \`self_check\` with \`wire_undeclared:<kind>:<name>\` because the runtime has no Context/registration for it and would throw at first paint. If you need a new wire surface, that's a contract authoring step the agent owns \u2014 your job is to honor what's declared.
|
|
11030
|
+
|
|
11031
|
+
Renaming a binding is fine \u2014 the wiring is the string-literal argument, not the identifier.`;
|
|
11032
|
+
var CONTRACT_SURFACE = `## Contract surface \u2014 four specs + two catalogs
|
|
11033
|
+
|
|
11034
|
+
A \`DataContract\` declares everything a GguiSession exchanges with the outside world. **Four typed specs** for the four data-flow directions, **two reference catalogs** for tool / hook lookups:
|
|
11035
|
+
|
|
11036
|
+
| Surface | Direction | Role |
|
|
11037
|
+
| -------------------- | -------------------------- | -------------------------------------------------------------------- |
|
|
11038
|
+
| \`propsSpec\` | server \u2192 UI (one-shot) | Initial render values delivered once at \`ggui_render\` |
|
|
11039
|
+
| \`streamSpec\` | agent \u2192 UI (many) | Typed channels for live updates via \`ggui_emit\` |
|
|
11040
|
+
| \`actionSpec\` | UI \u2192 agent (events) | Discrete events driving the agent's next turn (consumed via \`ggui_consume\`) |
|
|
11041
|
+
| \`contextSpec\` | UI \u2192 server (state mirror) | UI state the agent observes between turns |
|
|
11042
|
+
| \`agentCapabilities.tools\` | catalog | Tools the contract references via \`actionSpec[*].nextStep\` and \`streamSpec[*].source.tool\` |
|
|
11043
|
+
| \`clientCapabilities.gadgets\` | catalog | Browser-capability gadget hooks the component code mounts (e.g., \`useGeolocation\`) |
|
|
11044
|
+
|
|
11045
|
+
**Placement rule for inbound specs**: actions drive turns; context observes state. There is no third category.
|
|
11046
|
+
|
|
11047
|
+
**Data vs behavior**: the contract describes data flow; the component code describes behavior. Scroll, focus, toast, animation, clipboard write \u2014 all component code, never contract fields.`;
|
|
11048
|
+
var DEFENSIVE_CODING = `## Defensive coding for absent / late-arriving data
|
|
11049
|
+
|
|
11050
|
+
Props arrive at mount and may be partial on first render; later prop changes reach the SAME mounted component via \`ggui_amend\` (in-place repaint) on the live channel. Stream channels start empty and fill over time. Context slots start at their declared default (often \`null\`). **Never assume a field exists before you read it.**
|
|
11051
|
+
|
|
11052
|
+
- **Optional contract props**: a \`Props\` field marked \`?\` in the boilerplate may be omitted by the agent ENTIRELY \u2014 the wire gate only enforces \`required: true\` entries, and the harness verifies your component with those props stripped. Guard them (\`?? default\`, \`?.\`, or an empty-state branch) from the FIRST render.
|
|
11053
|
+
- **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.all, stream.latest, etc.
|
|
11054
|
+
- **Object access**: optional-chain through nested fields. \`props.user?.name ?? 'Anonymous'\` not \`props.user.name\`.
|
|
11055
|
+
- **Number ops**: default before arithmetic. \`(props.count ?? 0) + 1\` not \`props.count + 1\`.
|
|
11056
|
+
- **Stream latest**: \`useStream\` returns \`{latest: T | null, all: T[], isComplete: boolean}\`. The default \`all\` is \`[]\` so it's safe to map; \`latest\` is null until the first frame arrives \u2014 guard before reading \`.foo\`.
|
|
11057
|
+
- **Stream reconciliation**: when a stream event carries an \`action\` discriminant (e.g. \`create | move | edit | delete\`), the channel is a CRUD feed \u2014 your handler MUST branch on EVERY value: append on \`create\`, drop on \`delete\`, replace-by-id on \`move\` / \`edit\`. Merging only the "edit" case silently loses created and deleted items. Reconcile into the SAME state that seeds from \`props\` (e.g. \`useState(() => props.tasks ?? [])\`) so the seed data and the live feed render as one list \u2014 and handle an event whose id is not yet present (a \`create\` for an unknown item) by inserting it, not ignoring it.
|
|
11058
|
+
- **Props re-sync on repaint**: props the agent owns are the source of truth for display \u2014 prefer rendering DIRECTLY from \`props\` (controlled, no local copy). When local optimistic state genuinely needs a prop seed (a click bumps a count before the agent confirms), it MUST re-sync on prop change \u2014 \`useEffect(() => setCount(props.count ?? 0), [props.count])\` \u2014 or a \`ggui_amend\` repaint applies the new props while the display keeps the stale local value. BAD: \`const [count, setCount] = useState(props.count ?? 0)\` alone. GOOD: the same seed + the one-line re-sync effect.
|
|
11059
|
+
- **Loading state**: while data is still absent, render \`<Skeleton>\` placeholders \u2014 never a blank screen. \`<Skeleton variant="text" />\` for a text line, \`variant="circle"\` for an avatar slot, default \`rect\` for a block.
|
|
11060
|
+
- **Empty state**: when a list or results array is empty, render \`<EmptyState title="\u2026" description="\u2026" />\` \u2014 a region that renders nothing when empty looks broken to the user.
|
|
11061
|
+
|
|
11062
|
+
Unhandled \`Cannot read properties of undefined\` errors trip the iframe error boundary and the user sees "Something went wrong" \u2014 a regression class the runtime can't recover from.`;
|
|
11063
|
+
var GESTURE_ROUTING = `## Picking the right primitive for user gestures
|
|
11064
|
+
|
|
11065
|
+
Choose by what the user is DOING, not where the result goes \u2014 the runtime handles the routing.
|
|
11066
|
+
|
|
11067
|
+
| Gesture intent | LLM writes | Notes |
|
|
11068
|
+
| -------------- | ---------- | ----- |
|
|
11069
|
+
| Fire a server-side action | \`useAction(name)\` + call \`dispatch(name, payload)\` | Every action is agent-routed. The runtime emits an event on \`ggui_consume\`; the agent reacts on its next turn. If the contract entry declares \`nextStep: 'X'\`, that names the tool the agent SHOULD call next \u2014 advisory hint forwarded as event metadata. |
|
|
11070
|
+
| Surface state to the agent's context | the auto-generated \`setSlotName\` setter (from the boilerplate's \`useGguiContext\` line) | The runtime owns useState + Provider; the boilerplate emits one \`const [slot, setSlot] = useGguiContext<T>('slot')\` line per declared \`contextSpec\` slot. Write plain JSX, no \`useState\`, no Provider wrap. Every value change auto-flows to the host LLM (debounced). One-way client \u2192 agent \u2014 see "Observable state via \`contextSpec\`" below. |
|
|
11071
|
+
| Use a browser capability (camera, mic, geolocation, clipboard, file picker, notifications) | call the hook the contract declared, e.g., \`const loc = useGeolocation();\` and \`await loc.start()\` | The contract's \`clientCapabilities.gadgets\` declares which gadget exports the UI uses. The hook implementations live in \`@ggui-ai/gadgets\` (or a third-party package named in the \`Package\` column). Read \`status\` ("idle" / "prompting" / "active" / "completed" / "denied" / "error") to gate UI, and thread the resolved \`value\` into a contextSpec slot or actionSpec payload if the agent needs to see it. |
|
|
11072
|
+
| Open external link | Plain \`<a href="https://...">\` (or \`target="_blank"\`) | External cross-origin clicks are intercepted and routed through the host (security warnings, app-internal navigation, audit). Same-origin links and \`#fragment\` jumps stay native. |
|
|
11073
|
+
| Toggle fullscreen / chrome | Plain \`el.requestFullscreen()\` / \`document.exitFullscreen()\` | The native browser API is intercepted; the host adjusts iframe chrome accordingly. Returns a resolved promise so \`.then()\` / \`await\` chains don't break. |
|
|
11074
|
+
|
|
11075
|
+
Every gesture fires a uniform server-side audit envelope (\`ggui_runtime_submit_action\`) so operators see all three patterns in RenderInspector with the same shape.
|
|
11076
|
+
|
|
11077
|
+
**Don't import wire hooks for link / display-mode.** \`useAction\` is the only wire hook for user gestures; links and fullscreen use plain HTML / browser APIs.
|
|
11078
|
+
|
|
11079
|
+
**All actions are agent-routed.** Every action emits an event the agent reacts to on its next turn via \`ggui_consume\`. The optional \`nextStep: '<tool>'\` field on an \`actionSpec\` entry is a HINT naming the tool the agent SHOULD call next \u2014 the contract author's recommendation, NOT a binding directive. The agent decides whether to honor it. If you want to declare a tool catalog entry the contract references, add it to \`agentCapabilities.tools[<name>]\` with input/output schemas; the cross-ref linter rejects dangling \`nextStep\` values that don't resolve to a declared catalog entry.`;
|
|
11080
|
+
var INTERACTIVE_TRAITS = `## Making a primitive interactive \u2014 \`as={Trait}\`
|
|
11081
|
+
|
|
11082
|
+
Structural primitives (\`Box\`, \`Stack\`, \`Row\`, \`Card\`) have NO \`onClick\` by default. Add interactivity with the \`as\` prop \u2014 a trait, not a wrapper:
|
|
11083
|
+
|
|
11084
|
+
\`\`\`tsx
|
|
11085
|
+
<Card as={Clickable} onClick={() => dispatch('select', { id })}
|
|
11086
|
+
hoverStyle={{ boxShadow: 'var(--ggui-shape-shadow-lg)' }}>\u2026</Card>
|
|
11087
|
+
\`\`\`
|
|
11088
|
+
|
|
11089
|
+
- \`as={Clickable}\` \u2192 \`onClick\` + keyboard activation (Enter/Space) + \`role="button"\` + \`hoverStyle\`/\`activeStyle\`/\`cursor\`.
|
|
11090
|
+
- \`as={Hoverable}\` \u2192 \`hoverStyle\` only (no click). \`as={Pressable}\` \u2192 \`onPress\` + \`pressStyle\`.
|
|
11091
|
+
|
|
11092
|
+
\`as={Trait}\` is a PROP \u2014 it does NOT re-nest the JSX. Never write \`<Clickable>\u2026</Clickable>\` around a primitive; put \`as={Clickable}\` on the primitive itself. The trait carries the keyboard + ARIA wiring, so don't hand-write \`onKeyDown\` / \`role\`. Trait components (\`Clickable\`, \`Hoverable\`, \`Pressable\`) import from \`@ggui-ai/design\` like everything else \u2014 the boilerplate already imports them.
|
|
11093
|
+
|
|
11094
|
+
**Semantic components are already interactive** \u2014 \`Button\` (\`onClick\`), \`Link\` (\`href\`), \`Input\` / \`Select\` (\`onChange\`). Use their own props; never put \`as\` on them. \`Text\` picks its element with \`is\` (\`<Text is="label">\`), not \`as\`.
|
|
11095
|
+
|
|
11096
|
+
**Never nest two interactive elements.** Interactive content MUST NOT contain other interactive content \u2014 a gesture on the inner control bubbles to the outer one and fires BOTH handlers (one user click \u2192 the action dispatched twice). Do NOT put a \`Button\`, \`Checkbox\`, \`Input\`, \`Select\`, \`Link\`, or another \`as={Clickable}\` primitive inside a \`Card\` / \`Box\` / \`Row\` / \`Stack\` that is itself \`as={Clickable}\`. Wire each \`useAction\` callback to exactly ONE surface: EITHER the whole card is the trigger (interactive container, no interactive children) OR an inner control is the trigger (plain container, no \`as={Clickable}\`) \u2014 never both. A row with a checkbox: put the action on the \`Checkbox onChange\` and leave the row plain.
|
|
11097
|
+
|
|
11098
|
+
**\`Text\` / \`Heading\` accept NO event handlers and NO \`as\` \u2014 only \`style\` / \`className\` plus their own typed props.** \`onClick\`, \`onDoubleClick\`, \`as={Clickable}\`, \`color\` are all type errors on \`Text\`. When the request says a label is "clickable", "editable", "edit on click / double-click", or "tap to \u2026", do ONE of these \u2014 never put the handler on \`Text\`:
|
|
11099
|
+
|
|
11100
|
+
\`\`\`tsx
|
|
11101
|
+
// Click-to-edit a label: wrap the Text in a Clickable structural primitive.
|
|
11102
|
+
<Box as={Clickable} onClick={() => setEditingId(task.id)}
|
|
11103
|
+
style={{ cursor: 'pointer' }}>
|
|
11104
|
+
<Text weight="semibold">{task.title}</Text>
|
|
11105
|
+
</Box>
|
|
11106
|
+
|
|
11107
|
+
// Or pair the label with an explicit edit Button (clearer affordance).
|
|
11108
|
+
<Row gap="xs" align="center">
|
|
11109
|
+
<Text weight="semibold">{task.title}</Text>
|
|
11110
|
+
<Button variant="ghost" size="xs" aria-label="Edit title"
|
|
11111
|
+
onClick={() => setEditingId(task.id)}>Edit</Button>
|
|
11112
|
+
</Row>
|
|
11113
|
+
|
|
11114
|
+
// In edit mode, swap the Text for an Input.
|
|
11115
|
+
{editingId === task.id
|
|
11116
|
+
? <Input value={draftTitle} onChange={setDraftTitle} label="Task title" />
|
|
11117
|
+
: <Text weight="semibold">{task.title}</Text>}
|
|
11118
|
+
\`\`\``;
|
|
11119
|
+
var ANTI_PATTERNS = `## Anti-patterns \u2014 DO NOT WRITE
|
|
11120
|
+
|
|
11121
|
+
The following identifiers / shapes are RETIRED from the contract surface as of 2026-05-11. Pre-2026-05-11 examples in your training data may include them; do not reproduce. The linter / CI grep gate rejects:
|
|
11122
|
+
|
|
11123
|
+
- \`useWiredTool\`, \`useClientTool\` \u2014 retired hooks. Replace with \`useAction\` (events) and the named hook from \`@ggui-ai/gadgets\` (browser capabilities).
|
|
11124
|
+
- \`dispatch: { kind: 'tool', tool: '...' }\` / \`dispatch: { kind: 'agent', intendedTool: '...' }\` \u2014 retired discriminated-union. Use the flat optional \`nextStep?: '<tool>'\` instead.
|
|
11125
|
+
- \`mode: 'host-routed'\` / \`mode: 'tool'\` \u2014 retired \`mode\` field. Same fix: flat \`nextStep?\`.
|
|
11126
|
+
- \`broadcast: {...}\` on the contract \u2014 retired top-level field. Use \`streamSpec[channel].source: {tool, args?}\` to declare a tool-fed channel.
|
|
11127
|
+
- \`wiredTools\` / \`agentTools\` (top-level) \u2014 retired catalog names. Use \`agentCapabilities.tools\`.
|
|
11128
|
+
- \`clientTools\` / \`clientCapabilities.capabilities\` \u2014 retired catalog shapes. Use \`clientCapabilities.gadgets\` (entries declare hooks, not RPC).
|
|
11129
|
+
- \`@ggui-ai/client-tools\` \u2014 retired package name. Import gadget hooks from \`@ggui-ai/gadgets\`.
|
|
11130
|
+
- \`intendedTool\` \u2014 retired. Use \`nextStep\` (flat).
|
|
11131
|
+
- \`props: { properties: {...} }\` as a CONTRACT field \u2014 retired. The contract field is \`propsSpec\` (the wire \`props\` field on render / update still carries VALUES).`;
|
|
11132
|
+
var CROSS_REFERENCE_RULES = `## Cross-reference rules
|
|
11133
|
+
|
|
11134
|
+
When you declare a reference, also declare the catalog entry it points at:
|
|
11135
|
+
|
|
11136
|
+
- \`actionSpec[X].nextStep = 'fetch_inbox'\` \u2192 \`agentCapabilities.tools.fetch_inbox = { toolInfo: { inputSchema, description?, outputSchema? }, usage?, example? }\` MUST exist. Cross-ref code: \`CTR_REF_NEXT_STEP\`.
|
|
11137
|
+
- \`streamSpec[X].source.tool = 'list_messages'\` \u2192 \`agentCapabilities.tools.list_messages\` MUST exist. Cross-ref code: \`CTR_REF_STREAM_SOURCE\`.
|
|
11138
|
+
- The catalog entry's schemas MUST be a superset of the referencing spec's schema. Cross-ref code: \`CTR_SCHEMA_INCOMPAT\`.`;
|
|
11139
|
+
function renderGadgetCatalogSection(gadgetsSection) {
|
|
11140
|
+
return `## clientCapabilities \u2014 registered catalog
|
|
11141
|
+
|
|
11142
|
+
${gadgetsSection}
|
|
11143
|
+
|
|
11144
|
+
Each hook conforms to \`GadgetHook<TOutput, TOptions>\`: call \`start(opts?)\` to fire, read \`{value, status, error, stop?}\`. \`status\` walks through \`idle \u2192 prompting \u2192 active|completed\` or routes to \`denied\` / \`error\` on failure.
|
|
11145
|
+
|
|
11146
|
+
3rd-party plugins (Leaflet maps, Mapbox, Stripe, Chart.js, \u2026) are registered via \`createGguiGadget\` from \`@ggui-ai/gadgets\` and surface in this same table when the operator has added them to \`App.gadgets\`. Reference any registered hook by name \u2014 render validation rejects hooks not in this catalog with \`gadget_not_registered\`.`;
|
|
11147
|
+
}
|
|
11148
|
+
var CONTEXT_SPEC_SECTION = `## Observable state via \`contextSpec\`
|
|
11149
|
+
|
|
11150
|
+
When the contract declares \`contextSpec\`, the boilerplate auto-generates one \`useGguiContext\` call per slot at the top of your component. The runtime owns the underlying \`useState\` and the Provider tree \u2014 **you do NOT write \`useState\` or any \`<Provider>\` wrap yourself**:
|
|
11151
|
+
|
|
11152
|
+
\`\`\`tsx
|
|
11153
|
+
import { useGguiContext } from '@ggui-ai/wire';
|
|
11154
|
+
|
|
11155
|
+
export default function Component(props: Props) {
|
|
11156
|
+
// AUTO-GENERATED \u2014 do not remove or rename:
|
|
11157
|
+
const [currentStep, setCurrentStep] = useGguiContext<number>('currentStep');
|
|
11158
|
+
const [draftText, setDraftText] = useGguiContext<string>('draftText');
|
|
11159
|
+
|
|
11160
|
+
// Plain JSX. No Provider wrap. The runtime already wrapped your
|
|
11161
|
+
// component in nested SingleSlotProviders before this code ran.
|
|
11162
|
+
return (
|
|
11163
|
+
<Container>
|
|
11164
|
+
<Text>Step {currentStep}</Text>
|
|
11165
|
+
<Input value={draftText} onChange={(e) => setDraftText(e.target.value)} />
|
|
11166
|
+
<Button onClick={() => setCurrentStep((s) => s + 1)}>Next</Button>
|
|
11167
|
+
</Container>
|
|
11168
|
+
);
|
|
11169
|
+
}
|
|
11170
|
+
\`\`\`
|
|
11171
|
+
|
|
11172
|
+
For every declared slot you have **\`slotName\` + \`setSlotName\`** in scope:
|
|
11173
|
+
- **Read** the value to render: \`<Text>Step {currentStep}</Text>\`
|
|
11174
|
+
- **Write** via the setter: \`setCurrentStep(s => s + 1)\` (in callbacks, effects, anywhere)
|
|
11175
|
+
|
|
11176
|
+
Every value change is mirrored to the host LLM's context automatically (debounced, default 300ms \u2014 adjustable per-slot via \`entry.debounceMs\` in the contract). The agent sees the user's interaction state \u2014 drafts, current step, hover, selection \u2014 without you calling any API.
|
|
11177
|
+
|
|
11178
|
+
**When to use the auto-generated state.** Any slot the contract declared. If \`contextSpec.draftText\` exists, bind \`<Input value={draftText} onChange={e => setDraftText(e.target.value)}>\` so the agent sees the typing live. If \`contextSpec.currentStep\` exists, render the step indicator from \`currentStep\` and bump it via \`setCurrentStep\` in your "next" callback.
|
|
11179
|
+
|
|
11180
|
+
**When NOT to use it.** Local UI state the contract did NOT declare \u2014 \`isDropdownOpen\`, hover flags, animation phase, ephemeral toggles. For those, use a plain \`useState\` directly. The runtime ignores undeclared state.
|
|
11181
|
+
|
|
11182
|
+
**\`contextSpec\` direction is one-way: client \u2192 agent.** The agent uses \`propsSpec\` (via \`ggui_amend\` for in-place repaints of this mount, or \`ggui_update\` when it mints a new history card) and \`streamSpec\` (via the live channel) to push state TO the client. Don't try to write to the agent via \`contextSpec\` \u2014 there is no return path.
|
|
11183
|
+
|
|
11184
|
+
**Schema mismatches drop silently.** If you set a value that doesn't match the slot's schema (e.g. a string into a \`{type: 'number'}\` slot), the runtime logs a dev \`console.warn\` and skips the post. Make sure your setter calls produce values that match the declared shape.`;
|
|
11185
|
+
var DATA_PARAMETERIZATION = `## Data Parameterization (CRITICAL)
|
|
11186
|
+
|
|
11187
|
+
Generated components are CACHED blueprints reused across requests. NEVER hardcode request-specific data (names, cities, numbers, dates) into the component body. Define data as default prop values so the blueprint works for ANY similar request:
|
|
11188
|
+
|
|
11189
|
+
\`\`\`tsx
|
|
11190
|
+
// BAD \u2014 hardcoded, only works for Tokyo
|
|
11191
|
+
const city = "Tokyo";
|
|
11192
|
+
const temp = 18;
|
|
11193
|
+
|
|
11194
|
+
// GOOD \u2014 parameterized via props with defaults from the request
|
|
11195
|
+
interface Props {
|
|
11196
|
+
city?: string;
|
|
11197
|
+
temperature?: number;
|
|
11198
|
+
}
|
|
11199
|
+
export default function WeatherCard({ city = "Tokyo", temperature = 18 }: Props) {
|
|
11200
|
+
// A controller can override for Seoul, Paris, etc.
|
|
11201
|
+
}
|
|
11202
|
+
\`\`\`
|
|
11203
|
+
|
|
11204
|
+
Rules:
|
|
11205
|
+
1. All request-specific data \u2192 props with defaults. City names, tickers, user names, dates, counts.
|
|
11206
|
+
2. Layout and styling are universal. Colors, spacing, structure \u2014 these are the reusable part.
|
|
11207
|
+
3. Default values come from the current request \u2014 so the component renders correctly standalone.
|
|
11208
|
+
4. Props interface must be typed and exported.`;
|
|
11209
|
+
var COMPONENT_STRUCTURE = `## Component Structure
|
|
11210
|
+
|
|
11211
|
+
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.
|
|
11212
|
+
|
|
11213
|
+
\`\`\`tsx
|
|
11214
|
+
import { useState } from 'react';
|
|
11215
|
+
import { Container, Card, Stack, Text, Button, Input } from '@ggui-ai/design';
|
|
11216
|
+
|
|
11217
|
+
interface Props {
|
|
11218
|
+
onSubmit?: (data: unknown) => void;
|
|
11219
|
+
}
|
|
11220
|
+
|
|
11221
|
+
function ItemCard({ item, onEdit }: { item: Item; onEdit: (id: string) => void }) {
|
|
11222
|
+
return <Card padding="md">\u2026</Card>;
|
|
11223
|
+
}
|
|
11224
|
+
|
|
11225
|
+
export default function GeneratedComponent({ onSubmit }: Props) {
|
|
11226
|
+
return (
|
|
11227
|
+
<Container>
|
|
11228
|
+
{items.map((item) => <ItemCard key={item.id} item={item} onEdit={handleEdit} />)}
|
|
11229
|
+
</Container>
|
|
11230
|
+
);
|
|
11231
|
+
}
|
|
11232
|
+
\`\`\``;
|
|
11233
|
+
|
|
11234
|
+
// src/boilerplate/free-design-prompt.ts
|
|
11235
|
+
var FREE_GESTURE_SURFACE = `## One gesture surface per action binding
|
|
11236
|
+
|
|
11237
|
+
Every \`useAction\` binding is wired to exactly ONE interactive surface. Interactive content MUST NOT contain other interactive content that dispatches the same binding \u2014 a click on the inner control bubbles to the outer handler and the action fires TWICE, so a toggle-style action silently reverts the user's change. Concretely: never put a \`<button>\`, \`<a>\`, \`<input>\`, \`<select>\`, an element with \`role="button"\`, or any element with its own \`onClick\`, inside a \`<div>\` / \`<li>\` / \`<tr>\` / \`<Card>\` whose own \`onClick\` calls the same binding. EITHER the whole row is the trigger (interactive container, no interactive children) OR an inner control is the trigger (plain container) \u2014 never both. Tier-0 fails the nested shape with \`double-wired-action:certain\`. A row with a checkbox: put the action on the checkbox's \`onChange\` and leave the row plain. \`stopPropagation\` is not a fix \u2014 pick one surface.`;
|
|
11238
|
+
var FREE_DESIGN_FREEDOM = `## Design freedom
|
|
11239
|
+
|
|
11240
|
+
You are not required to compose from the \`@ggui-ai/design\` primitives. Compose with raw HTML elements (\`<div>\`, \`<section>\`, \`<button>\`, \`<input>\`, \`<ul>\`, \`<table>\`, \u2026), inline \`style\` props and \`<style>\` blocks \u2014 regular CSS, \`@media\` and \`@container\` queries, \`@keyframes\`, \`:hover\` / \`:focus-visible\` (the host CSP permits \`'unsafe-inline'\` styles). Any layout: CSS grid, flexbox, absolute positioning, sticky headers. The runtime injects NO Tailwind and NO reset stylesheet \u2014 a raw element renders unstyled until your CSS styles it, so bring the CSS you need. Use class selectors in \`<style>\` blocks (an \`#id\` selector that reads as hex digits trips the color check).
|
|
11241
|
+
|
|
11242
|
+
\`@ggui-ai/design\` stays importable \u2014 \`import { Skeleton, EmptyState, Icon } from '@ggui-ai/design'\` \u2014 use a primitive when it saves work, skip it when it constrains you. Its props are documented in the optional catalog at the end of this prompt.
|
|
11243
|
+
|
|
11244
|
+
Spacing, typography, radius, shadow geometry and layout values MAY be literals: \`padding: '20px'\`, \`fontSize: 'clamp(1rem, 2vw, 1.5rem)'\`, \`borderRadius: 12\`, \`gap: '0.75rem'\`, \`boxShadow: '0 8px 24px var(--ggui-color-outlineVariant)'\`. Color is the one exception \u2014 next section.`;
|
|
11245
|
+
var COLOR_PREFIX = "--ggui-color-";
|
|
11246
|
+
var RAMP_FAMILIES = ["primary", "neutral", "success", "warning", "error", "info"];
|
|
11247
|
+
var SEMANTIC_ROLES = [
|
|
11248
|
+
{ names: ["ground", "onGround"], role: "the page / chat canvas behind everything + its text" },
|
|
11249
|
+
{ names: ["container", "onContainer"], role: "a card, a bubble, a panel + body text on it" },
|
|
11250
|
+
{ names: ["elevated", "onElevated"], role: "popover, menu, modal, toast \u2014 floats above a container" },
|
|
11251
|
+
{ names: ["sunken", "onSunken"], role: "wells, inputs at rest, code blocks + secondary text" },
|
|
11252
|
+
{ names: ["ground-subtle", "ground-gradient"], role: "quiet canvas fills" },
|
|
11253
|
+
{ names: ["onPrimary"], role: "text on the brand color" },
|
|
11254
|
+
{ names: ["outline", "outlineVariant"], role: "borders + dividers" },
|
|
11255
|
+
{ names: ["onError"], role: "text on an error fill" }
|
|
11256
|
+
];
|
|
11257
|
+
function colorVocabularyFromManifest(manifest) {
|
|
11258
|
+
const colors = manifest.filter((t) => t.startsWith(COLOR_PREFIX)).map((t) => t.slice(COLOR_PREFIX.length));
|
|
11259
|
+
const ramps = [];
|
|
11260
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
11261
|
+
for (const family of RAMP_FAMILIES) {
|
|
11262
|
+
const steps = [];
|
|
11263
|
+
for (const name of colors) {
|
|
11264
|
+
const m = new RegExp(`^${family}-(\\d+)$`).exec(name);
|
|
11265
|
+
if (m) {
|
|
11266
|
+
steps.push(Number(m[1]));
|
|
11267
|
+
claimed.add(name);
|
|
11268
|
+
}
|
|
11269
|
+
}
|
|
11270
|
+
if (steps.length === 0) continue;
|
|
11271
|
+
steps.sort((a, b) => a - b);
|
|
11272
|
+
const bare = colors.includes(family);
|
|
11273
|
+
if (bare) claimed.add(family);
|
|
11274
|
+
ramps.push({ family, steps, bare });
|
|
11275
|
+
}
|
|
11276
|
+
const roles = colors.filter((n) => !claimed.has(n)).sort();
|
|
11277
|
+
return { ramps, roles };
|
|
11278
|
+
}
|
|
11279
|
+
function renderRamp(r) {
|
|
11280
|
+
return `\`${r.family}-${r.steps.join("/")}\`${r.bare ? ` (plus bare \`${r.family}\`)` : ""}`;
|
|
11281
|
+
}
|
|
11282
|
+
function renderFreeColorRule(manifest = consumedTokenManifest) {
|
|
11283
|
+
const { ramps, roles } = colorVocabularyFromManifest(manifest);
|
|
11284
|
+
const brand = ramps.filter((r) => r.family === "primary" || r.family === "neutral");
|
|
11285
|
+
const state = ramps.filter((r) => r.family !== "primary" && r.family !== "neutral");
|
|
11286
|
+
const roleLines = [];
|
|
11287
|
+
const named = /* @__PURE__ */ new Set();
|
|
11288
|
+
for (const g of SEMANTIC_ROLES) {
|
|
11289
|
+
const present = g.names.filter((n) => roles.includes(n));
|
|
11290
|
+
if (present.length === 0) continue;
|
|
11291
|
+
for (const n of present) named.add(n);
|
|
11292
|
+
roleLines.push(`${present.map((n) => `\`${n}\``).join(" / ")} (${g.role})`);
|
|
11293
|
+
}
|
|
11294
|
+
const other = roles.filter((n) => !named.has(n));
|
|
11295
|
+
if (other.length > 0) roleLines.push(`other roles: ${other.map((n) => `\`${n}\``).join(", ")}`);
|
|
11296
|
+
const vocabulary = [
|
|
11297
|
+
roleLines.length > 0 ? `- Semantic roles cover most needs: ${roleLines.join("; ")}.` : "",
|
|
11298
|
+
brand.length > 0 ? `- Brand + neutral ramps (every step that exists, nothing else): ${brand.map(renderRamp).join(", ")}.` : "",
|
|
11299
|
+
state.length > 0 ? `- State ramps (every step that exists, nothing else): ${state.map(renderRamp).join(", ")}.` : "",
|
|
11300
|
+
"- A step not listed does not exist and renders unset; pick the nearest listed step."
|
|
11301
|
+
].filter((l) => l.length > 0).join("\n");
|
|
11302
|
+
return `## Color stays tokenized (the one hard styling rule)
|
|
11303
|
+
|
|
11304
|
+
Brand-bearing color \u2014 text, backgrounds, borders, fills, gradient stops, focus rings, shadow tints \u2014 comes ONLY from the closed \`--ggui-color-*\` token manifest, referenced BARE: \`color: 'var(--ggui-color-onContainer)'\`, \`background: 'var(--ggui-color-primary-50)'\`, \`border: '1px solid var(--ggui-color-outline)'\`, \`linear-gradient(135deg, var(--ggui-color-primary-500), var(--ggui-color-primary-700))\`.
|
|
11305
|
+
|
|
11306
|
+
- No hex (\`#7c3aed\`), no \`rgb()\` / \`rgba()\` / \`hsl()\` / \`hsla()\`, no CSS named colors (\`red\`, \`slategray\`) \u2014 anywhere in the file, including \`<style>\` blocks and shadows. Tier-0 fails them (\`tokens:hex-color\`, \`tokens:hardcoded-color-fn\`, \`tokens:named-color\`). \`transparent\`, \`currentColor\` and \`inherit\` are fine.
|
|
11307
|
+
- No literal fallback inside ANY token reference: \`var(--ggui-color-primary-600)\` bare, never \`var(--ggui-color-primary-600, #0284c7)\` \u2014 the runtime injects every token on \`:root\`, and a fallback paints the wrong brand exactly when the operator's theme matters (\`tokens:token-fallback\`). This holds for every \`--ggui-*\` token you reference, not only colors.
|
|
11308
|
+
- Only names from the manifest (listed at the end of this prompt) exist \u2014 an invented \`--ggui-color-*\` name renders unset (\`tokens:off-manifest-token\`).
|
|
11309
|
+
- Shadows: \`var(--ggui-shape-shadow-sm|md|lg|xl)\`, or a literal offset/blur whose color is a token \u2014 never an \`rgba()\` tint.
|
|
11310
|
+
- Why: the operator switches the whole app's theme (light, dark, branded) by redefining these variables. A component that reads them is on-brand everywhere; one that hardcodes color is off-brand everywhere but the default.
|
|
11311
|
+
|
|
11312
|
+
The color vocabulary (rendered from the manifest \u2014 these names and steps, exactly):
|
|
11313
|
+
${vocabulary}`;
|
|
11314
|
+
}
|
|
11315
|
+
var FREE_COLOR_RULE = renderFreeColorRule();
|
|
11316
|
+
var FREE_HOST_ENVELOPE = `## Where this renders
|
|
11317
|
+
|
|
11318
|
+
The compiled component is loaded into a sandboxed iframe by an MCP Apps host (chat clients, the ggui console, embedded panels):
|
|
11319
|
+
|
|
11320
|
+
- **Sandbox**: \`allow-scripts allow-forms\`, no same-origin. The only channel to the outside is \`postMessage\` through the wire hooks \u2014 the component never talks to a server itself.
|
|
11321
|
+
- **No network from the component**: \`fetch\`, \`XMLHttpRequest\`, \`WebSocket\`, dynamic \`import()\` fail tier-0 and the CSP (\`connect-src 'none'\` on spec hosts). No \`<link rel="stylesheet">\`, no \`@import url()\`, no CDN scripts, no web fonts from a CDN \u2014 use \`var(--ggui-font-family-sans)\` / \`var(--ggui-font-family-mono)\` (system stacks) or a generic family.
|
|
11322
|
+
- **No host document access**: \`window.*\`, \`document.*\` (other than the intercepted \`requestFullscreen\` / \`exitFullscreen\`), \`localStorage\`, \`sessionStorage\`, \`navigator\`, \`location\`, \`history\`, \`innerHTML\` / \`dangerouslySetInnerHTML\`, \`<script>\` \u2014 all rejected.
|
|
11323
|
+
- **Props arrive through \`_meta["ai.ggui/render"]\`** on the host side and reach you as \`props\`; later repaints (\`ggui_amend\`) update the SAME mounted component \u2014 see "Defensive coding".
|
|
11324
|
+
- **Display modes**: the host advertises \`availableDisplayModes\` (\`inline\` | \`fullscreen\` | \`pip\`); \`el.requestFullscreen()\` asks the host to switch \u2014 never assume it will.
|
|
11325
|
+
- **Size**: the host sets the iframe's \`containerDimensions\` once per render; there is no live resize notification \u2014 a resize is a re-render, so layout must be fluid (\`width: 100%\`, \`max-width\`, no fixed width) and hold at any width inside the canvas range above.`;
|
|
11326
|
+
var FREE_BUDGETS = `## Budgets (deterministic caps)
|
|
11327
|
+
|
|
11328
|
+
- Source \u2264 50 KB and \u2264 500 lines.
|
|
11329
|
+
- Compiled bundle \u2264 2 MB.
|
|
11330
|
+
- One file, one \`export default function Component(props: Props)\`.`;
|
|
11331
|
+
var FREE_IMPORTS_AND_SECURITY = `## Imports & security (what you may import, what you may not call)
|
|
11332
|
+
|
|
11333
|
+
Import ONLY from: \`react\`, \`react-dom\`, \`@ggui-ai/design\` (single barrel \u2014 no \`/primitives\` / \`/components\` subpaths), \`@ggui-ai/wire\`, \`@ggui-ai/gadgets\`, or a gadget package the contract declares \u2014 never \`@ggui-ai/wire/internal\`. The iframe runtime rewrites exactly these specifiers to in-bundle shims; ANY other import fails at module evaluation and blanks the iframe (tier-0 \`imports\` fail). No \`require()\`.
|
|
11334
|
+
|
|
11335
|
+
Never call \`eval()\`, \`Function()\`, \`fetch()\` or the host-document APIs listed under "Where this renders" \u2014 tier-0 \`security\` fails them and the sandbox blocks them anyway.`;
|
|
11336
|
+
var FREE_RESPONSIVE_INVARIANT = `## Responsive invariant
|
|
11337
|
+
|
|
11338
|
+
The component must fit the host-supplied iframe width at every point of the canvas range: \`width: 100%\` (plus \`max-width\` where you want to cap), never a fixed width, no horizontal scroll. Use \`@media\` / \`@container\` queries, \`clamp()\`, \`minmax()\`, \`auto-fit\` grids and \`flex-wrap\` freely. Compact chrome on \`xs-chat-card\` (the host draws the card border and shadow); own the chrome on the fullscreen canvases (\`mobile-fullscreen-small\`, \`md\`, \`lg\`, \`xl\`).`;
|
|
11339
|
+
var FREE_ACCESSIBILITY = `## Accessibility (REQUIRED)
|
|
11340
|
+
|
|
11341
|
+
With raw elements YOU own the semantics \u2014 nothing is accessible by construction unless you import a \`@ggui-ai/design\` primitive (those carry their own roles / labels / keyboard handling; don't re-declare ARIA on them).
|
|
11342
|
+
|
|
11343
|
+
1. **Interactive = a real control.** A click target is a \`<button type="button">\` or an \`<a href>\` \u2014 never a bare \`<div onClick>\`. If a non-button element must be clickable, add \`role="button"\`, \`tabIndex={0}\` and an \`onKeyDown\` handling Enter/Space.
|
|
11344
|
+
2. **Form inputs have labels.** \`<label htmlFor={id}>\` paired with the input's \`id\`, or \`aria-label\`; errors via \`aria-invalid\` + \`aria-describedby\`.
|
|
11345
|
+
3. **Images have \`alt\`** (\`alt=""\` for decorative). Icon-only buttons need \`aria-label\`; decorative icons and emoji are \`aria-hidden="true"\`.
|
|
11346
|
+
4. **Live & streaming data** (a \`useStream\` \`.latest\` value, counters, clocks, flashing prices) sits in an \`aria-live="polite"\` (or \`role="status"\`) region so screen readers announce the change.
|
|
11347
|
+
5. **Headings nest**: one \`<h1>\` per screen, \`<h2>\` for sections, \`<h3>\` below \u2014 never skipped or inverted.
|
|
11348
|
+
6. **Busy state is announced**: \`<button disabled={isLoading} aria-busy={isLoading}>{isLoading ? 'Submitting\u2026' : 'Submit'}</button>\`.
|
|
11349
|
+
7. **Stateful controls announce state**: \`role="checkbox"\` + \`aria-checked\` on a toggleable row, \`aria-pressed\` on a toggle button, \`aria-selected\` on the chosen item, \`aria-expanded\` on an open/close affordance. Styling (strikethrough, color, a check mark) is never the only carrier of state.
|
|
11350
|
+
8. **Focus is visible** \u2014 style \`:focus-visible\` with a token color; never \`outline: none\` without a replacement.`;
|
|
11351
|
+
var FREE_QUALITY_CHECKLIST = `## Quality Checklist (verify before returning)
|
|
11352
|
+
|
|
11353
|
+
- [ ] Imports ONLY from: react, react-dom, @ggui-ai/design, @ggui-ai/wire, @ggui-ai/gadgets (+ contract gadget packages)
|
|
11354
|
+
- [ ] ZERO literal colors \u2014 every color is a bare \`var(--ggui-color-*)\` from the manifest; no hex / rgb() / hsl() / named colors, no fallbacks \u2014 in \`style\` props AND \`<style>\` blocks
|
|
11355
|
+
- [ ] Fluid width (\`width: 100%\`, \`max-width\`), holds across the whole canvas range, no horizontal scroll
|
|
11356
|
+
- [ ] Typed Props interface exported; request-specific data is a prop with default
|
|
11357
|
+
- [ ] Every \`useAction\` binding is wired to exactly ONE gesture surface \u2014 no handler on both a parent and a child
|
|
11358
|
+
- [ ] Real controls (\`<button>\`, \`<a>\`, \`<input>\` + \`<label>\`), \`alt\` on images, \`aria-label\` on icon-only buttons
|
|
11359
|
+
- [ ] State is ARIA-expressed (\`aria-checked\` / \`aria-pressed\` / \`aria-selected\` / \`aria-expanded\`) \u2014 never style-only
|
|
11360
|
+
- [ ] Live/streaming regions wrapped in \`aria-live="polite"\`
|
|
11361
|
+
- [ ] Headings nest \u2014 one \`<h1>\`, then \`<h2>\` / \`<h3>\` \u2014 never skipped or inverted
|
|
11362
|
+
- [ ] No \`fetch\` / \`eval\` / \`window\` / \`document\` / \`localStorage\` / external stylesheets or fonts
|
|
11363
|
+
- [ ] Source \u2264 50 KB / \u2264 500 lines; wire hooks (\`useAction\`, \`useStream\`, \`useGguiContext\`) kept and consumed`;
|
|
11364
|
+
var FREE_SHAPE_GUIDANCE_NOTE = "In this mode the design-package components named above (`<Stepper>`, `<Modal>`, `<Card>`, `<Row>`, `<CardGrid>`) are defaults, not requirements \u2014 any composition that satisfies the behavioural rule (track the step index in state, a close affordance + focus trap, `key={item.id}`, no horizontal scroll) is acceptable. The crash anti-patterns are mandatory.";
|
|
11365
|
+
var TOKEN_GROUPS = [
|
|
11366
|
+
{ label: "Color (mandatory for every color)", prefixes: ["--ggui-color-"], note: "" },
|
|
11367
|
+
{ label: "Spacing (optional \u2014 literals are fine)", prefixes: ["--ggui-spacing-"], note: "" },
|
|
11368
|
+
{ label: "Typography (optional)", prefixes: ["--ggui-font-"], note: "" },
|
|
11369
|
+
{ label: "Radius (optional)", prefixes: ["--ggui-shape-radius-"], note: "" },
|
|
11370
|
+
{ label: "Shadow (optional)", prefixes: ["--ggui-shape-shadow-"], note: "" }
|
|
11371
|
+
];
|
|
11372
|
+
function renderTokenVocabulary(manifest = consumedTokenManifest) {
|
|
11373
|
+
const sorted = [...manifest].sort();
|
|
11374
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
11375
|
+
const lines = [
|
|
11376
|
+
"### Reference (optional): theme token vocabulary",
|
|
11377
|
+
"",
|
|
11378
|
+
"The closed manifest \u2014 the ONLY `--ggui-*` names the runtime defines on `:root`. Color names are mandatory for color; the rest are optional conveniences.",
|
|
11379
|
+
""
|
|
11380
|
+
];
|
|
11381
|
+
for (const group of TOKEN_GROUPS) {
|
|
11382
|
+
const names = sorted.filter((t) => group.prefixes.some((p) => t.startsWith(p)));
|
|
11383
|
+
if (names.length === 0) continue;
|
|
11384
|
+
for (const n of names) claimed.add(n);
|
|
11385
|
+
lines.push(`- **${group.label}**: ${names.map((n) => `\`${n}\``).join(", ")}`);
|
|
11386
|
+
}
|
|
11387
|
+
const other = sorted.filter((t) => !claimed.has(t));
|
|
11388
|
+
if (other.length > 0) {
|
|
11389
|
+
lines.push(`- **Other**: ${other.map((n) => `\`${n}\``).join(", ")}`);
|
|
11390
|
+
}
|
|
11391
|
+
return lines.join("\n");
|
|
11392
|
+
}
|
|
11393
|
+
function buildFreeDesignPrompt(inputs) {
|
|
11394
|
+
const axisSection = inputs.axisSection.length > 0 ? `${inputs.axisSection}
|
|
11395
|
+
${FREE_SHAPE_GUIDANCE_NOTE}
|
|
11396
|
+
` : "";
|
|
11397
|
+
const head = `You are ggui's UI builder. You receive a typed boilerplate and fill it in using apply_changes.
|
|
11398
|
+
|
|
11399
|
+
## Your Task
|
|
11400
|
+
${inputs.userRequest}
|
|
11401
|
+
|
|
11402
|
+
## Rendering canvas
|
|
11403
|
+
${describeCanvas(inputs.canvas)}
|
|
11404
|
+
|
|
11405
|
+
## How It Works
|
|
11406
|
+
1. Read the boilerplate \u2014 typed Props and wire hooks are pre-configured; the JSX body and its styling are yours to compose
|
|
11407
|
+
2. Respond with one apply_changes call \u2014 add state, helpers, styles and JSX
|
|
11408
|
+
3. If compilation or evaluation fails, you'll get errors to fix in the next turn
|
|
11409
|
+
|
|
11410
|
+
${inputs.criteriaBlock}
|
|
11411
|
+
${axisSection}
|
|
11412
|
+
`;
|
|
11413
|
+
const sections = [
|
|
11414
|
+
PROTOCOL_NOTES,
|
|
11415
|
+
CONTRACT_SURFACE,
|
|
11416
|
+
DEFENSIVE_CODING,
|
|
11417
|
+
GESTURE_ROUTING,
|
|
11418
|
+
FREE_GESTURE_SURFACE,
|
|
11419
|
+
ANTI_PATTERNS,
|
|
11420
|
+
CROSS_REFERENCE_RULES,
|
|
11421
|
+
renderGadgetCatalogSection(inputs.gadgetsSection),
|
|
11422
|
+
CONTEXT_SPEC_SECTION,
|
|
11423
|
+
`## Reference: Wire Hooks
|
|
11424
|
+
${inputs.wireDoc}`,
|
|
11425
|
+
FREE_DESIGN_FREEDOM,
|
|
11426
|
+
FREE_COLOR_RULE,
|
|
11427
|
+
FREE_HOST_ENVELOPE,
|
|
11428
|
+
FREE_BUDGETS,
|
|
11429
|
+
FREE_IMPORTS_AND_SECURITY,
|
|
11430
|
+
DATA_PARAMETERIZATION,
|
|
11431
|
+
COMPONENT_STRUCTURE,
|
|
11432
|
+
FREE_RESPONSIVE_INVARIANT,
|
|
11433
|
+
FREE_ACCESSIBILITY,
|
|
11434
|
+
FREE_QUALITY_CHECKLIST,
|
|
11435
|
+
renderTokenVocabulary(),
|
|
11436
|
+
`### Reference (optional): \`@ggui-ai/design\` component catalog
|
|
11437
|
+
|
|
11438
|
+
Optional \u2014 import a primitive only when it helps. Raw HTML + your own CSS is a first-class choice.
|
|
11439
|
+
|
|
11440
|
+
${inputs.primitivesDoc}`
|
|
11441
|
+
];
|
|
11442
|
+
return head + sections.join("\n\n") + "\n";
|
|
11443
|
+
}
|
|
10738
11444
|
var VIRTUAL_DTS_PATH = "/__gadget__.d.ts";
|
|
10739
11445
|
var EXTRACTOR_COMPILER_OPTIONS = {
|
|
10740
11446
|
target: ts2.ScriptTarget.ESNext,
|
|
@@ -11057,7 +11763,8 @@ function buildSystemPrompt(inputs) {
|
|
|
11057
11763
|
const scr = inputs.screen ?? "universal";
|
|
11058
11764
|
const shellDesc = SHELL_DESCRIPTIONS[shell] ?? SHELL_DESCRIPTIONS.fullscreen;
|
|
11059
11765
|
const screenDesc = SCREEN_DESCRIPTIONS[scr] ?? SCREEN_DESCRIPTIONS.universal;
|
|
11060
|
-
const
|
|
11766
|
+
const designMode = inputs.designMode ?? DEFAULT_DESIGN_MODE;
|
|
11767
|
+
const criteriaBlock = inputs.criteriaBlock ?? buildCodingCriteriaSummary(designMode);
|
|
11061
11768
|
const pitfallsBlock = inputs.pitfallsBlock ?? "";
|
|
11062
11769
|
const designSystemDocs = inputs.designSystemDocs ?? "";
|
|
11063
11770
|
const primitivesDoc = inputs.primitivesDoc ?? "";
|
|
@@ -11070,6 +11777,17 @@ function buildSystemPrompt(inputs) {
|
|
|
11070
11777
|
## Shape Guidance
|
|
11071
11778
|
${inputs.axisDelta}
|
|
11072
11779
|
` : "";
|
|
11780
|
+
if (designMode === "free") {
|
|
11781
|
+
return buildFreeDesignPrompt({
|
|
11782
|
+
userRequest: inputs.userRequest,
|
|
11783
|
+
canvas: inputs.canvas ?? canvasForRendering(inputs.shellType, inputs.screen),
|
|
11784
|
+
axisSection,
|
|
11785
|
+
criteriaBlock,
|
|
11786
|
+
wireDoc,
|
|
11787
|
+
primitivesDoc,
|
|
11788
|
+
gadgetsSection
|
|
11789
|
+
});
|
|
11790
|
+
}
|
|
11073
11791
|
return `You are ggui's UI builder. You receive a typed boilerplate and fill it in using apply_changes.
|
|
11074
11792
|
|
|
11075
11793
|
## Your Task
|
|
@@ -11086,172 +11804,17 @@ ${inputs.userRequest}
|
|
|
11086
11804
|
|
|
11087
11805
|
${criteriaBlock}
|
|
11088
11806
|
${axisSection}
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
| Surface | Direction | Role |
|
|
11102
|
-
| -------------------- | -------------------------- | -------------------------------------------------------------------- |
|
|
11103
|
-
| \`propsSpec\` | server \u2192 UI (one-shot) | Initial render values delivered once at \`ggui_render\` |
|
|
11104
|
-
| \`streamSpec\` | agent \u2192 UI (many) | Typed channels for live updates via \`ggui_emit\` |
|
|
11105
|
-
| \`actionSpec\` | UI \u2192 agent (events) | Discrete events driving the agent's next turn (consumed via \`ggui_consume\`) |
|
|
11106
|
-
| \`contextSpec\` | UI \u2192 server (state mirror) | UI state the agent observes between turns |
|
|
11107
|
-
| \`agentCapabilities.tools\` | catalog | Tools the contract references via \`actionSpec[*].nextStep\` and \`streamSpec[*].source.tool\` |
|
|
11108
|
-
| \`clientCapabilities.gadgets\` | catalog | Browser-capability gadget hooks the component code mounts (e.g., \`useGeolocation\`) |
|
|
11109
|
-
|
|
11110
|
-
**Placement rule for inbound specs**: actions drive turns; context observes state. There is no third category.
|
|
11111
|
-
|
|
11112
|
-
**Data vs behavior**: the contract describes data flow; the component code describes behavior. Scroll, focus, toast, animation, clipboard write \u2014 all component code, never contract fields.
|
|
11113
|
-
|
|
11114
|
-
## Defensive coding for absent / late-arriving data
|
|
11115
|
-
|
|
11116
|
-
Props arrive at mount and may be partial on first render; later prop changes reach the SAME mounted component via \`ggui_amend\` (in-place repaint) on the live channel. Stream channels start empty and fill over time. Context slots start at their declared default (often \`null\`). **Never assume a field exists before you read it.**
|
|
11117
|
-
|
|
11118
|
-
- **Optional contract props**: a \`Props\` field marked \`?\` in the boilerplate may be omitted by the agent ENTIRELY \u2014 the wire gate only enforces \`required: true\` entries, and the harness verifies your component with those props stripped. Guard them (\`?? default\`, \`?.\`, or an empty-state branch) from the FIRST render.
|
|
11119
|
-
- **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.all, stream.latest, etc.
|
|
11120
|
-
- **Object access**: optional-chain through nested fields. \`props.user?.name ?? 'Anonymous'\` not \`props.user.name\`.
|
|
11121
|
-
- **Number ops**: default before arithmetic. \`(props.count ?? 0) + 1\` not \`props.count + 1\`.
|
|
11122
|
-
- **Stream latest**: \`useStream\` returns \`{latest: T | null, all: T[], isComplete: boolean}\`. The default \`all\` is \`[]\` so it's safe to map; \`latest\` is null until the first frame arrives \u2014 guard before reading \`.foo\`.
|
|
11123
|
-
- **Stream reconciliation**: when a stream event carries an \`action\` discriminant (e.g. \`create | move | edit | delete\`), the channel is a CRUD feed \u2014 your handler MUST branch on EVERY value: append on \`create\`, drop on \`delete\`, replace-by-id on \`move\` / \`edit\`. Merging only the "edit" case silently loses created and deleted items. Reconcile into the SAME state that seeds from \`props\` (e.g. \`useState(() => props.tasks ?? [])\`) so the seed data and the live feed render as one list \u2014 and handle an event whose id is not yet present (a \`create\` for an unknown item) by inserting it, not ignoring it.
|
|
11124
|
-
- **Props re-sync on repaint**: props the agent owns are the source of truth for display \u2014 prefer rendering DIRECTLY from \`props\` (controlled, no local copy). When local optimistic state genuinely needs a prop seed (a click bumps a count before the agent confirms), it MUST re-sync on prop change \u2014 \`useEffect(() => setCount(props.count ?? 0), [props.count])\` \u2014 or a \`ggui_amend\` repaint applies the new props while the display keeps the stale local value. BAD: \`const [count, setCount] = useState(props.count ?? 0)\` alone. GOOD: the same seed + the one-line re-sync effect.
|
|
11125
|
-
- **Loading state**: while data is still absent, render \`<Skeleton>\` placeholders \u2014 never a blank screen. \`<Skeleton variant="text" />\` for a text line, \`variant="circle"\` for an avatar slot, default \`rect\` for a block.
|
|
11126
|
-
- **Empty state**: when a list or results array is empty, render \`<EmptyState title="\u2026" description="\u2026" />\` \u2014 a region that renders nothing when empty looks broken to the user.
|
|
11127
|
-
|
|
11128
|
-
Unhandled \`Cannot read properties of undefined\` errors trip the iframe error boundary and the user sees "Something went wrong" \u2014 a regression class the runtime can't recover from.
|
|
11129
|
-
|
|
11130
|
-
## Picking the right primitive for user gestures
|
|
11131
|
-
|
|
11132
|
-
Choose by what the user is DOING, not where the result goes \u2014 the runtime handles the routing.
|
|
11133
|
-
|
|
11134
|
-
| Gesture intent | LLM writes | Notes |
|
|
11135
|
-
| -------------- | ---------- | ----- |
|
|
11136
|
-
| Fire a server-side action | \`useAction(name)\` + call \`dispatch(name, payload)\` | Every action is agent-routed. The runtime emits an event on \`ggui_consume\`; the agent reacts on its next turn. If the contract entry declares \`nextStep: 'X'\`, that names the tool the agent SHOULD call next \u2014 advisory hint forwarded as event metadata. |
|
|
11137
|
-
| Surface state to the agent's context | the auto-generated \`setSlotName\` setter (from the boilerplate's \`useGguiContext\` line) | The runtime owns useState + Provider; the boilerplate emits one \`const [slot, setSlot] = useGguiContext<T>('slot')\` line per declared \`contextSpec\` slot. Write plain JSX, no \`useState\`, no Provider wrap. Every value change auto-flows to the host LLM (debounced). One-way client \u2192 agent \u2014 see "Observable state via \`contextSpec\`" below. |
|
|
11138
|
-
| Use a browser capability (camera, mic, geolocation, clipboard, file picker, notifications) | call the hook the contract declared, e.g., \`const loc = useGeolocation();\` and \`await loc.start()\` | The contract's \`clientCapabilities.gadgets\` declares which gadget exports the UI uses. The hook implementations live in \`@ggui-ai/gadgets\` (or a third-party package named in the \`Package\` column). Read \`status\` ("idle" / "prompting" / "active" / "completed" / "denied" / "error") to gate UI, and thread the resolved \`value\` into a contextSpec slot or actionSpec payload if the agent needs to see it. |
|
|
11139
|
-
| Open external link | Plain \`<a href="https://...">\` (or \`target="_blank"\`) | External cross-origin clicks are intercepted and routed through the host (security warnings, app-internal navigation, audit). Same-origin links and \`#fragment\` jumps stay native. |
|
|
11140
|
-
| Toggle fullscreen / chrome | Plain \`el.requestFullscreen()\` / \`document.exitFullscreen()\` | The native browser API is intercepted; the host adjusts iframe chrome accordingly. Returns a resolved promise so \`.then()\` / \`await\` chains don't break. |
|
|
11141
|
-
|
|
11142
|
-
Every gesture fires a uniform server-side audit envelope (\`ggui_runtime_submit_action\`) so operators see all three patterns in RenderInspector with the same shape.
|
|
11143
|
-
|
|
11144
|
-
**Don't import wire hooks for link / display-mode.** \`useAction\` is the only wire hook for user gestures; links and fullscreen use plain HTML / browser APIs.
|
|
11145
|
-
|
|
11146
|
-
**All actions are agent-routed.** Every action emits an event the agent reacts to on its next turn via \`ggui_consume\`. The optional \`nextStep: '<tool>'\` field on an \`actionSpec\` entry is a HINT naming the tool the agent SHOULD call next \u2014 the contract author's recommendation, NOT a binding directive. The agent decides whether to honor it. If you want to declare a tool catalog entry the contract references, add it to \`agentCapabilities.tools[<name>]\` with input/output schemas; the cross-ref linter rejects dangling \`nextStep\` values that don't resolve to a declared catalog entry.
|
|
11147
|
-
|
|
11148
|
-
## Making a primitive interactive \u2014 \`as={Trait}\`
|
|
11149
|
-
|
|
11150
|
-
Structural primitives (\`Box\`, \`Stack\`, \`Row\`, \`Card\`) have NO \`onClick\` by default. Add interactivity with the \`as\` prop \u2014 a trait, not a wrapper:
|
|
11151
|
-
|
|
11152
|
-
\`\`\`tsx
|
|
11153
|
-
<Card as={Clickable} onClick={() => dispatch('select', { id })}
|
|
11154
|
-
hoverStyle={{ boxShadow: 'var(--ggui-shape-shadow-lg)' }}>\u2026</Card>
|
|
11155
|
-
\`\`\`
|
|
11156
|
-
|
|
11157
|
-
- \`as={Clickable}\` \u2192 \`onClick\` + keyboard activation (Enter/Space) + \`role="button"\` + \`hoverStyle\`/\`activeStyle\`/\`cursor\`.
|
|
11158
|
-
- \`as={Hoverable}\` \u2192 \`hoverStyle\` only (no click). \`as={Pressable}\` \u2192 \`onPress\` + \`pressStyle\`.
|
|
11159
|
-
|
|
11160
|
-
\`as={Trait}\` is a PROP \u2014 it does NOT re-nest the JSX. Never write \`<Clickable>\u2026</Clickable>\` around a primitive; put \`as={Clickable}\` on the primitive itself. The trait carries the keyboard + ARIA wiring, so don't hand-write \`onKeyDown\` / \`role\`. Trait components (\`Clickable\`, \`Hoverable\`, \`Pressable\`) import from \`@ggui-ai/design\` like everything else \u2014 the boilerplate already imports them.
|
|
11161
|
-
|
|
11162
|
-
**Semantic components are already interactive** \u2014 \`Button\` (\`onClick\`), \`Link\` (\`href\`), \`Input\` / \`Select\` (\`onChange\`). Use their own props; never put \`as\` on them. \`Text\` picks its element with \`is\` (\`<Text is="label">\`), not \`as\`.
|
|
11163
|
-
|
|
11164
|
-
**Never nest two interactive elements.** Interactive content MUST NOT contain other interactive content \u2014 a gesture on the inner control bubbles to the outer one and fires BOTH handlers (one user click \u2192 the action dispatched twice). Do NOT put a \`Button\`, \`Checkbox\`, \`Input\`, \`Select\`, \`Link\`, or another \`as={Clickable}\` primitive inside a \`Card\` / \`Box\` / \`Row\` / \`Stack\` that is itself \`as={Clickable}\`. Wire each \`useAction\` callback to exactly ONE surface: EITHER the whole card is the trigger (interactive container, no interactive children) OR an inner control is the trigger (plain container, no \`as={Clickable}\`) \u2014 never both. A row with a checkbox: put the action on the \`Checkbox onChange\` and leave the row plain.
|
|
11165
|
-
|
|
11166
|
-
**\`Text\` / \`Heading\` accept NO event handlers and NO \`as\` \u2014 only \`style\` / \`className\` plus their own typed props.** \`onClick\`, \`onDoubleClick\`, \`as={Clickable}\`, \`color\` are all type errors on \`Text\`. When the request says a label is "clickable", "editable", "edit on click / double-click", or "tap to \u2026", do ONE of these \u2014 never put the handler on \`Text\`:
|
|
11167
|
-
|
|
11168
|
-
\`\`\`tsx
|
|
11169
|
-
// Click-to-edit a label: wrap the Text in a Clickable structural primitive.
|
|
11170
|
-
<Box as={Clickable} onClick={() => setEditingId(task.id)}
|
|
11171
|
-
style={{ cursor: 'pointer' }}>
|
|
11172
|
-
<Text weight="semibold">{task.title}</Text>
|
|
11173
|
-
</Box>
|
|
11174
|
-
|
|
11175
|
-
// Or pair the label with an explicit edit Button (clearer affordance).
|
|
11176
|
-
<Row gap="xs" align="center">
|
|
11177
|
-
<Text weight="semibold">{task.title}</Text>
|
|
11178
|
-
<Button variant="ghost" size="xs" aria-label="Edit title"
|
|
11179
|
-
onClick={() => setEditingId(task.id)}>Edit</Button>
|
|
11180
|
-
</Row>
|
|
11181
|
-
|
|
11182
|
-
// In edit mode, swap the Text for an Input.
|
|
11183
|
-
{editingId === task.id
|
|
11184
|
-
? <Input value={draftTitle} onChange={setDraftTitle} label="Task title" />
|
|
11185
|
-
: <Text weight="semibold">{task.title}</Text>}
|
|
11186
|
-
\`\`\`
|
|
11187
|
-
|
|
11188
|
-
## Anti-patterns \u2014 DO NOT WRITE
|
|
11189
|
-
|
|
11190
|
-
The following identifiers / shapes are RETIRED from the contract surface as of 2026-05-11. Pre-2026-05-11 examples in your training data may include them; do not reproduce. The linter / CI grep gate rejects:
|
|
11191
|
-
|
|
11192
|
-
- \`useWiredTool\`, \`useClientTool\` \u2014 retired hooks. Replace with \`useAction\` (events) and the named hook from \`@ggui-ai/gadgets\` (browser capabilities).
|
|
11193
|
-
- \`dispatch: { kind: 'tool', tool: '...' }\` / \`dispatch: { kind: 'agent', intendedTool: '...' }\` \u2014 retired discriminated-union. Use the flat optional \`nextStep?: '<tool>'\` instead.
|
|
11194
|
-
- \`mode: 'host-routed'\` / \`mode: 'tool'\` \u2014 retired \`mode\` field. Same fix: flat \`nextStep?\`.
|
|
11195
|
-
- \`broadcast: {...}\` on the contract \u2014 retired top-level field. Use \`streamSpec[channel].source: {tool, args?}\` to declare a tool-fed channel.
|
|
11196
|
-
- \`wiredTools\` / \`agentTools\` (top-level) \u2014 retired catalog names. Use \`agentCapabilities.tools\`.
|
|
11197
|
-
- \`clientTools\` / \`clientCapabilities.capabilities\` \u2014 retired catalog shapes. Use \`clientCapabilities.gadgets\` (entries declare hooks, not RPC).
|
|
11198
|
-
- \`@ggui-ai/client-tools\` \u2014 retired package name. Import gadget hooks from \`@ggui-ai/gadgets\`.
|
|
11199
|
-
- \`intendedTool\` \u2014 retired. Use \`nextStep\` (flat).
|
|
11200
|
-
- \`props: { properties: {...} }\` as a CONTRACT field \u2014 retired. The contract field is \`propsSpec\` (the wire \`props\` field on render / update still carries VALUES).
|
|
11201
|
-
|
|
11202
|
-
## Cross-reference rules
|
|
11203
|
-
|
|
11204
|
-
When you declare a reference, also declare the catalog entry it points at:
|
|
11205
|
-
|
|
11206
|
-
- \`actionSpec[X].nextStep = 'fetch_inbox'\` \u2192 \`agentCapabilities.tools.fetch_inbox = { toolInfo: { inputSchema, description?, outputSchema? }, usage?, example? }\` MUST exist. Cross-ref code: \`CTR_REF_NEXT_STEP\`.
|
|
11207
|
-
- \`streamSpec[X].source.tool = 'list_messages'\` \u2192 \`agentCapabilities.tools.list_messages\` MUST exist. Cross-ref code: \`CTR_REF_STREAM_SOURCE\`.
|
|
11208
|
-
- The catalog entry's schemas MUST be a superset of the referencing spec's schema. Cross-ref code: \`CTR_SCHEMA_INCOMPAT\`.
|
|
11209
|
-
|
|
11210
|
-
## clientCapabilities \u2014 registered catalog
|
|
11211
|
-
|
|
11212
|
-
${gadgetsSection}
|
|
11213
|
-
|
|
11214
|
-
Each hook conforms to \`GadgetHook<TOutput, TOptions>\`: call \`start(opts?)\` to fire, read \`{value, status, error, stop?}\`. \`status\` walks through \`idle \u2192 prompting \u2192 active|completed\` or routes to \`denied\` / \`error\` on failure.
|
|
11215
|
-
|
|
11216
|
-
3rd-party plugins (Leaflet maps, Mapbox, Stripe, Chart.js, \u2026) are registered via \`createGguiGadget\` from \`@ggui-ai/gadgets\` and surface in this same table when the operator has added them to \`App.gadgets\`. Reference any registered hook by name \u2014 render validation rejects hooks not in this catalog with \`gadget_not_registered\`.
|
|
11217
|
-
|
|
11218
|
-
## Observable state via \`contextSpec\`
|
|
11219
|
-
|
|
11220
|
-
When the contract declares \`contextSpec\`, the boilerplate auto-generates one \`useGguiContext\` call per slot at the top of your component. The runtime owns the underlying \`useState\` and the Provider tree \u2014 **you do NOT write \`useState\` or any \`<Provider>\` wrap yourself**:
|
|
11221
|
-
|
|
11222
|
-
\`\`\`tsx
|
|
11223
|
-
import { useGguiContext } from '@ggui-ai/wire';
|
|
11224
|
-
|
|
11225
|
-
export default function Component(props: Props) {
|
|
11226
|
-
// AUTO-GENERATED \u2014 do not remove or rename:
|
|
11227
|
-
const [currentStep, setCurrentStep] = useGguiContext<number>('currentStep');
|
|
11228
|
-
const [draftText, setDraftText] = useGguiContext<string>('draftText');
|
|
11229
|
-
|
|
11230
|
-
// Plain JSX. No Provider wrap. The runtime already wrapped your
|
|
11231
|
-
// component in nested SingleSlotProviders before this code ran.
|
|
11232
|
-
return (
|
|
11233
|
-
<Container>
|
|
11234
|
-
<Text>Step {currentStep}</Text>
|
|
11235
|
-
<Input value={draftText} onChange={(e) => setDraftText(e.target.value)} />
|
|
11236
|
-
<Button onClick={() => setCurrentStep((s) => s + 1)}>Next</Button>
|
|
11237
|
-
</Container>
|
|
11238
|
-
);
|
|
11239
|
-
}
|
|
11240
|
-
\`\`\`
|
|
11241
|
-
|
|
11242
|
-
For every declared slot you have **\`slotName\` + \`setSlotName\`** in scope:
|
|
11243
|
-
- **Read** the value to render: \`<Text>Step {currentStep}</Text>\`
|
|
11244
|
-
- **Write** via the setter: \`setCurrentStep(s => s + 1)\` (in callbacks, effects, anywhere)
|
|
11245
|
-
|
|
11246
|
-
Every value change is mirrored to the host LLM's context automatically (debounced, default 300ms \u2014 adjustable per-slot via \`entry.debounceMs\` in the contract). The agent sees the user's interaction state \u2014 drafts, current step, hover, selection \u2014 without you calling any API.
|
|
11247
|
-
|
|
11248
|
-
**When to use the auto-generated state.** Any slot the contract declared. If \`contextSpec.draftText\` exists, bind \`<Input value={draftText} onChange={e => setDraftText(e.target.value)}>\` so the agent sees the typing live. If \`contextSpec.currentStep\` exists, render the step indicator from \`currentStep\` and bump it via \`setCurrentStep\` in your "next" callback.
|
|
11249
|
-
|
|
11250
|
-
**When NOT to use it.** Local UI state the contract did NOT declare \u2014 \`isDropdownOpen\`, hover flags, animation phase, ephemeral toggles. For those, use a plain \`useState\` directly. The runtime ignores undeclared state.
|
|
11251
|
-
|
|
11252
|
-
**\`contextSpec\` direction is one-way: client \u2192 agent.** The agent uses \`propsSpec\` (via \`ggui_amend\` for in-place repaints of this mount, or \`ggui_update\` when it mints a new history card) and \`streamSpec\` (via the live channel) to push state TO the client. Don't try to write to the agent via \`contextSpec\` \u2014 there is no return path.
|
|
11253
|
-
|
|
11254
|
-
**Schema mismatches drop silently.** If you set a value that doesn't match the slot's schema (e.g. a string into a \`{type: 'number'}\` slot), the runtime logs a dev \`console.warn\` and skips the post. Make sure your setter calls produce values that match the declared shape.
|
|
11807
|
+
` + [
|
|
11808
|
+
PROTOCOL_NOTES,
|
|
11809
|
+
CONTRACT_SURFACE,
|
|
11810
|
+
DEFENSIVE_CODING,
|
|
11811
|
+
GESTURE_ROUTING,
|
|
11812
|
+
INTERACTIVE_TRAITS,
|
|
11813
|
+
ANTI_PATTERNS,
|
|
11814
|
+
CROSS_REFERENCE_RULES,
|
|
11815
|
+
renderGadgetCatalogSection(gadgetsSection),
|
|
11816
|
+
CONTEXT_SPEC_SECTION
|
|
11817
|
+
].join("\n\n") + `
|
|
11255
11818
|
|
|
11256
11819
|
${pitfallsBlock}
|
|
11257
11820
|
|
|
@@ -11267,7 +11830,7 @@ ${designSystemDocs}
|
|
|
11267
11830
|
${primitivesDoc}
|
|
11268
11831
|
`;
|
|
11269
11832
|
}
|
|
11270
|
-
var
|
|
11833
|
+
var DESIGN_SYSTEM_GUIDANCE_SURFACE_AND_TOKENS = `## Imports & Component Surface
|
|
11271
11834
|
|
|
11272
11835
|
Import ONLY from: \`react\`, \`@ggui-ai/design\`, \`@ggui-ai/wire\`. The ENTIRE design system \u2014 every primitive, component, composition and trait \u2014 is exported from the single \`@ggui-ai/design\` entry: \`import { Card, Grid, Stack, Modal, Clickable } from '@ggui-ai/design'\`. There are NO subpaths (\`/primitives\`, \`/components\`, \u2026) \u2014 never import from them. Use the design components \u2014 DO NOT use raw HTML elements (\`<button>\`, \`<input>\`, \`<div>\` for layout) or Tailwind classes; those render unstyled in the iframe runtime.
|
|
11273
11836
|
|
|
@@ -11302,7 +11865,7 @@ EXACT primitive prop values (other values are silently ignored \u2014 the design
|
|
|
11302
11865
|
- \`<Grid columns={N} gap="md">\` \u2014 2-D layout (rows AND columns). Reach for it for card galleries, stat grids and dashboards \u2014 NEVER hand-roll \`style={{ display: 'grid' }}\`. When the request names exact per-breakpoint counts ("3 per row on desktop, 1 on mobile"), pass a map: \`<Grid columns={{ base: 1, md: 3 }}>\` (breakpoints \`sm\`/\`md\`/\`lg\`/\`xl\`; the design system emits the media queries). For an open-ended gallery where any column count is fine, use \`<Grid minColumnWidth={220}>\` \u2014 it fits as many equal columns as the width allows. \`radius\` (Card / Box / Image) takes the scale \`none | sm | md | lg | xl\`.
|
|
11303
11866
|
- \`<Stat label="\u2026" value="\u2026" delta="+12%" trend="up">\` \u2014 KPI display (label + big value + trend-coloured delta + optional \`icon\`). \`trend\` is \`up | down | neutral\` (delta renders green / red / muted). Reach for it for any "show a number" UI; drop several into a \`<Grid>\` for a stat grid instead of hand-building label+value pairs.
|
|
11304
11867
|
- \`<Stepper steps={STEPS} current={step} />\` \u2014 display-only step indicator for wizards/checkouts. \`steps\` is a top-level \`const\` array of labels; \`current\` is YOUR zero-indexed \`useState\` value; \`orientation\` is \`horizontal | vertical\`; optional \`onStepClick={(i) => \u2026}\` makes steps clickable. Stepper never owns navigation state \u2014 your Next/Back handlers move \`current\`.
|
|
11305
|
-
- \`<Badge variant="...">\` \u2014 \`default | primary | secondary | success | warning | error | info\` for colored pills. Great for status/condition labels. There is NO \`neutral\` variant \u2014 use \`default\` (or \`secondary\`) for an un-tinted pill.
|
|
11868
|
+
- \`<Badge variant="...">\` \u2014 \`default | primary | secondary | tertiary | success | warning | error | info\` for colored pills. Great for status/condition labels. There is NO \`neutral\` variant \u2014 use \`default\` (or \`secondary\`) for an un-tinted pill.
|
|
11306
11869
|
|
|
11307
11870
|
**Color choice rule of thumb.** Reach for typed slots first: Button \`variant\`, Badge \`variant\`, Alert \`variant\`, Text/Heading/Icon/Spinner/Link/Divider \`tone\`, Box/Card \`surface\`. NEVER hardcode hex \`#XXXXXX\`, rgba, or hsl \u2014 tier-0 self-check rejects them with \`tokens:hex-color\` / \`tokens:hardcoded-color-fn\` and the LLM must remediate. Hardcoded colors break the operator's theme switch (Indigo \u2192 Claudic \u2192 Cyberpunk preset has zero effect on a card hardcoded with \`background: '#000'\`).
|
|
11308
11871
|
|
|
@@ -11335,8 +11898,8 @@ MANDATORY:
|
|
|
11335
11898
|
|
|
11336
11899
|
Token categories:
|
|
11337
11900
|
- Brand: \`var(--ggui-color-primary-600)\`, \`var(--ggui-color-primary-50)\`
|
|
11338
|
-
- Text: \`var(--ggui-color-
|
|
11339
|
-
- Backgrounds: \`var(--ggui-color-
|
|
11901
|
+
- Text: \`var(--ggui-color-onContainer)\`, \`var(--ggui-color-onSunken)\`
|
|
11902
|
+
- Backgrounds: \`var(--ggui-color-container)\`, \`var(--ggui-color-sunken)\`
|
|
11340
11903
|
- Borders: \`var(--ggui-color-outline)\`
|
|
11341
11904
|
- Spacing: \`var(--ggui-spacing-4)\`, \`var(--ggui-spacing-6)\`
|
|
11342
11905
|
- Typography: \`var(--ggui-font-size-sm)\`, \`var(--ggui-font-weight-semibold)\`
|
|
@@ -11357,7 +11920,7 @@ Use the FULL primary palette throughout the component \u2014 NOT only on submit
|
|
|
11357
11920
|
| Buttons, CTAs, filled interactive elements | \`primary-600\` / \`primary-700\` | Primary actions |
|
|
11358
11921
|
| Headings on light primary backgrounds | \`primary-800\` / \`primary-900\` | High-contrast branded text |
|
|
11359
11922
|
|
|
11360
|
-
Use
|
|
11923
|
+
Use the layering roles' inks (\`onContainer\` for body text on a card, \`onSunken\` for secondary text, captions, labels) \u2014 never raw neutrals. NEVER use raw \`neutral-*\` or \`gray-*\` for body text \u2014 they break in dark themes.
|
|
11361
11924
|
|
|
11362
11925
|
### Theme-Agnostic Design
|
|
11363
11926
|
|
|
@@ -11366,7 +11929,7 @@ Components MUST be theme-agnostic \u2014 they reference CSS variables but NEVER
|
|
|
11366
11929
|
DO:
|
|
11367
11930
|
- Use \`var(--ggui-color-primary-*)\` for brand elements \u2014 the theme controls what "primary" means
|
|
11368
11931
|
- Use \`var(--ggui-shape-shadow-*)\` for depth, \`var(--ggui-shape-radius-*)\` for corners
|
|
11369
|
-
- Use semantic color roles: primary for brand
|
|
11932
|
+
- Use semantic color roles: primary for brand; the layering roles (ground / container / elevated / sunken + their on* inks) for structure; success/error/warning for state
|
|
11370
11933
|
|
|
11371
11934
|
DON'T:
|
|
11372
11935
|
- Don't assume primary is blue \u2014 could be red, green, purple
|
|
@@ -11387,59 +11950,8 @@ Generated components become reusable blueprints \u2014 the same blueprint serves
|
|
|
11387
11950
|
2. Relative/fluid units \u2014 prefer \`%\`, \`em\`, \`rem\`, \`min()\`, \`max()\`, \`clamp()\` over fixed \`px\`.
|
|
11388
11951
|
3. Fluid widths \u2014 \`max-width\` with \`width: 100%\`. Never set a fixed width.
|
|
11389
11952
|
4. Compact padding \u2014 components are embedded in containers that provide their own chrome.
|
|
11390
|
-
5. No raw \`@media\` queries in component code \u2014 for a layout that must change by breakpoint, use \`<Grid columns={{ base: 1, md: 3 }}>\` (the design system emits the media queries for you) or a fluid \`minColumnWidth\` grid
|
|
11391
|
-
|
|
11392
|
-
## Data Parameterization (CRITICAL)
|
|
11393
|
-
|
|
11394
|
-
Generated components are CACHED blueprints reused across requests. NEVER hardcode request-specific data (names, cities, numbers, dates) into the component body. Define data as default prop values so the blueprint works for ANY similar request:
|
|
11395
|
-
|
|
11396
|
-
\`\`\`tsx
|
|
11397
|
-
// BAD \u2014 hardcoded, only works for Tokyo
|
|
11398
|
-
const city = "Tokyo";
|
|
11399
|
-
const temp = 18;
|
|
11400
|
-
|
|
11401
|
-
// GOOD \u2014 parameterized via props with defaults from the request
|
|
11402
|
-
interface Props {
|
|
11403
|
-
city?: string;
|
|
11404
|
-
temperature?: number;
|
|
11405
|
-
}
|
|
11406
|
-
export default function WeatherCard({ city = "Tokyo", temperature = 18 }: Props) {
|
|
11407
|
-
// A controller can override for Seoul, Paris, etc.
|
|
11408
|
-
}
|
|
11409
|
-
\`\`\`
|
|
11410
|
-
|
|
11411
|
-
Rules:
|
|
11412
|
-
1. All request-specific data \u2192 props with defaults. City names, tickers, user names, dates, counts.
|
|
11413
|
-
2. Layout and styling are universal. Colors, spacing, structure \u2014 these are the reusable part.
|
|
11414
|
-
3. Default values come from the current request \u2014 so the component renders correctly standalone.
|
|
11415
|
-
4. Props interface must be typed and exported.
|
|
11416
|
-
|
|
11417
|
-
## Component Structure
|
|
11418
|
-
|
|
11419
|
-
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.
|
|
11420
|
-
|
|
11421
|
-
\`\`\`tsx
|
|
11422
|
-
import { useState } from 'react';
|
|
11423
|
-
import { Container, Card, Stack, Text, Button, Input } from '@ggui-ai/design';
|
|
11424
|
-
|
|
11425
|
-
interface Props {
|
|
11426
|
-
onSubmit?: (data: unknown) => void;
|
|
11427
|
-
}
|
|
11428
|
-
|
|
11429
|
-
function ItemCard({ item, onEdit }: { item: Item; onEdit: (id: string) => void }) {
|
|
11430
|
-
return <Card padding="md">\u2026</Card>;
|
|
11431
|
-
}
|
|
11432
|
-
|
|
11433
|
-
export default function GeneratedComponent({ onSubmit }: Props) {
|
|
11434
|
-
return (
|
|
11435
|
-
<Container>
|
|
11436
|
-
{items.map((item) => <ItemCard key={item.id} item={item} onEdit={handleEdit} />)}
|
|
11437
|
-
</Container>
|
|
11438
|
-
);
|
|
11439
|
-
}
|
|
11440
|
-
\`\`\`
|
|
11441
|
-
|
|
11442
|
-
## Aesthetic Guidance (READ CAREFULLY \u2014 this is what separates "polished" from "ok")
|
|
11953
|
+
5. No raw \`@media\` queries in component code \u2014 for a layout that must change by breakpoint, use \`<Grid columns={{ base: 1, md: 3 }}>\` (the design system emits the media queries for you) or a fluid \`minColumnWidth\` grid.`;
|
|
11954
|
+
var DESIGN_SYSTEM_GUIDANCE_AESTHETICS_AND_CHECKLIST = `## Aesthetic Guidance (READ CAREFULLY \u2014 this is what separates "polished" from "ok")
|
|
11443
11955
|
|
|
11444
11956
|
### Visual hierarchy \u2014 the SCALE GAP rule
|
|
11445
11957
|
|
|
@@ -11461,11 +11973,11 @@ A polished UI has ONE hero that dominates. Everything else supports it. Bad layo
|
|
|
11461
11973
|
### Color discipline \u2014 the 60/30/10 rule
|
|
11462
11974
|
|
|
11463
11975
|
Don't paint everything in primary. Use:
|
|
11464
|
-
- **60%
|
|
11465
|
-
- **30%
|
|
11976
|
+
- **60% container** (\`var(--ggui-color-container)\` / \`onContainer\`) \u2014 body text, default backgrounds, structure
|
|
11977
|
+
- **30% sunken + onSunken** \u2014 wells, secondary text, captions, labels, dividers
|
|
11466
11978
|
- **10% primary** \u2014 hero number, ONE highlight element, CTAs, brand accent
|
|
11467
11979
|
|
|
11468
|
-
If your component is 100% purple text on purple backgrounds, you've lost the eye. Headings can be \`
|
|
11980
|
+
If your component is 100% purple text on purple backgrounds, you've lost the eye. Headings can be \`onContainer\` (the card's ink) \u2014 they'll still feel weighty. Save the primary palette for one or two STAR moments.
|
|
11469
11981
|
|
|
11470
11982
|
\`\`\`tsx
|
|
11471
11983
|
// BAD \u2014 everything purple, eye has no anchor
|
|
@@ -11474,7 +11986,7 @@ If your component is 100% purple text on purple backgrounds, you've lost the eye
|
|
|
11474
11986
|
<Text tone="emphasized">all body text</Text>
|
|
11475
11987
|
|
|
11476
11988
|
// GOOD \u2014 hero pops, body is neutral, primary is reserved
|
|
11477
|
-
<Heading>Title</Heading> {/* defaults to
|
|
11989
|
+
<Heading>Title</Heading> {/* defaults to onContainer */}
|
|
11478
11990
|
<Text size="4xl" weight="bold" tone="emphasized">42</Text>
|
|
11479
11991
|
<Text tone="muted">all body text</Text>
|
|
11480
11992
|
\`\`\`
|
|
@@ -11532,6 +12044,12 @@ Hero sections should feel airy. Use \`padding="xl"\` (32px) on the main card, no
|
|
|
11532
12044
|
- [ ] Live/streaming regions wrapped in \`aria-live="polite"\`
|
|
11533
12045
|
- [ ] Headings nest \u2014 one \`level={1}\`, then \`level={2}\`/\`{3}\` \u2014 never skipped or inverted
|
|
11534
12046
|
- [ ] Wire hooks (\`useAction\`, \`useStream\`) imported from \`@ggui-ai/wire\` and consumed`;
|
|
12047
|
+
var DESIGN_SYSTEM_GUIDANCE = [
|
|
12048
|
+
DESIGN_SYSTEM_GUIDANCE_SURFACE_AND_TOKENS,
|
|
12049
|
+
DATA_PARAMETERIZATION,
|
|
12050
|
+
COMPONENT_STRUCTURE,
|
|
12051
|
+
DESIGN_SYSTEM_GUIDANCE_AESTHETICS_AND_CHECKLIST
|
|
12052
|
+
].join("\n\n");
|
|
11535
12053
|
|
|
11536
12054
|
// src/tools.ts
|
|
11537
12055
|
var APPLY_CHANGES_TOOL = {
|
|
@@ -11843,6 +12361,7 @@ function pickWorkflow(_classification) {
|
|
|
11843
12361
|
|
|
11844
12362
|
// src/create-harness.ts
|
|
11845
12363
|
init_policy();
|
|
12364
|
+
init_design_mode();
|
|
11846
12365
|
var HARNESS_VERSION = "harness@1";
|
|
11847
12366
|
var HOW_VERSION = "how@1";
|
|
11848
12367
|
var WHAT_VERSION = "what@1";
|
|
@@ -11862,13 +12381,16 @@ function countCacheTiers(fragments) {
|
|
|
11862
12381
|
function createHarness(input) {
|
|
11863
12382
|
const { classification, contract, prompt, shellType, screen, overrides } = input;
|
|
11864
12383
|
const ctx = { classification, contract, prompt };
|
|
12384
|
+
const designMode = input.designMode ?? DEFAULT_DESIGN_MODE;
|
|
11865
12385
|
const composed = compose(classification);
|
|
11866
12386
|
const systemPromptBuilder = input.systemPromptBuilder ?? buildSystemPrompt;
|
|
11867
12387
|
const systemPrompt = systemPromptBuilder({
|
|
11868
12388
|
userRequest: prompt,
|
|
11869
12389
|
shellType,
|
|
11870
12390
|
screen,
|
|
11871
|
-
axisDelta: composed.promptText
|
|
12391
|
+
axisDelta: composed.promptText,
|
|
12392
|
+
designMode,
|
|
12393
|
+
...input.canvas !== void 0 ? { canvas: input.canvas } : {}
|
|
11872
12394
|
});
|
|
11873
12395
|
const howFragments = composed.fragments.filter(
|
|
11874
12396
|
(f) => f.promptText && f.promptText.trim().length > 0
|
|
@@ -11886,7 +12408,8 @@ function createHarness(input) {
|
|
|
11886
12408
|
shellType,
|
|
11887
12409
|
screen,
|
|
11888
12410
|
composed.boilerplateSections,
|
|
11889
|
-
input.appGadgets
|
|
12411
|
+
input.appGadgets,
|
|
12412
|
+
designMode
|
|
11890
12413
|
);
|
|
11891
12414
|
const whatFragments = composed.fragments.filter(
|
|
11892
12415
|
(f) => f.boilerplateMarker && f.boilerplateMarker.trim().length > 0
|
|
@@ -11937,6 +12460,7 @@ function createHarness(input) {
|
|
|
11937
12460
|
if (overrides?.process) overrideLabels.push("process");
|
|
11938
12461
|
if (overrides?.label) overrideLabels.push(`label:${overrides.label}`);
|
|
11939
12462
|
if (!isDefaultHarnessPolicy(policy)) overrideLabels.push("policy");
|
|
12463
|
+
if (designMode !== DEFAULT_DESIGN_MODE) overrideLabels.push(`designMode:${designMode}`);
|
|
11940
12464
|
const id = computeHarnessId({
|
|
11941
12465
|
classificationHash,
|
|
11942
12466
|
howVersion: how.version,
|
|
@@ -11964,6 +12488,7 @@ function createHarness(input) {
|
|
|
11964
12488
|
id,
|
|
11965
12489
|
name,
|
|
11966
12490
|
classification,
|
|
12491
|
+
designMode,
|
|
11967
12492
|
how,
|
|
11968
12493
|
what,
|
|
11969
12494
|
check,
|
|
@@ -12039,20 +12564,24 @@ These tokens adapt automatically to any theme (light, dark, branded). **ALWAYS u
|
|
|
12039
12564
|
|
|
12040
12565
|
| Token | CSS Variable | Default | Role |
|
|
12041
12566
|
|-------|-------------|---------|------|
|
|
12042
|
-
|
|
|
12043
|
-
|
|
|
12044
|
-
|
|
|
12045
|
-
|
|
|
12567
|
+
| ground | var(--ggui-color-ground) | The page / canvas behind everything |
|
|
12568
|
+
| onGround | var(--ggui-color-onGround) | Text on the canvas |
|
|
12569
|
+
| container | var(--ggui-color-container) | A card, a bubble, a panel \u2014 the unit of content |
|
|
12570
|
+
| onContainer | var(--ggui-color-onContainer) | Body text on a card |
|
|
12571
|
+
| elevated | var(--ggui-color-elevated) | Menus, popovers, modals, toasts \u2014 floats above a card |
|
|
12572
|
+
| onElevated | var(--ggui-color-onElevated) | Text on an elevated surface |
|
|
12573
|
+
| sunken | var(--ggui-color-sunken) | Wells, inputs at rest, code blocks \u2014 recessed inside a card |
|
|
12574
|
+
| onSunken | var(--ggui-color-onSunken) | Muted / secondary text |
|
|
12046
12575
|
| container | var(--ggui-color-container) | Primary-branded containers |
|
|
12047
12576
|
| onContainer | var(--ggui-color-onContainer) | Text on branded containers |
|
|
12048
12577
|
| outline | var(--ggui-color-outline) | Borders, dividers |
|
|
12049
12578
|
| outlineVariant | var(--ggui-color-outlineVariant) | Subtle borders |
|
|
12050
12579
|
|
|
12051
12580
|
**Usage pattern:**
|
|
12052
|
-
- Page/section background \u2192 \`var(--ggui-color-
|
|
12053
|
-
- Body text \u2192 \`var(--ggui-color-
|
|
12054
|
-
- Card/panel background \u2192 \`var(--ggui-color-
|
|
12055
|
-
- Secondary/muted text \u2192 \`var(--ggui-color-
|
|
12581
|
+
- Page/section background \u2192 \`var(--ggui-color-ground)\`
|
|
12582
|
+
- Body text \u2192 \`var(--ggui-color-onContainer)\`
|
|
12583
|
+
- Card/panel background \u2192 \`var(--ggui-color-container)\`; recessed wells / inputs \u2192 \`var(--ggui-color-sunken)\`
|
|
12584
|
+
- Secondary/muted text \u2192 \`var(--ggui-color-onSunken)\`
|
|
12056
12585
|
- Branded section/header \u2192 \`var(--ggui-color-container)\` bg + \`var(--ggui-color-onContainer)\` text
|
|
12057
12586
|
- Borders/dividers \u2192 \`var(--ggui-color-outline)\` or \`var(--ggui-color-outlineVariant)\`
|
|
12058
12587
|
|
|
@@ -12069,11 +12598,11 @@ Prefer the primitives' \`tone="success|warning|error|info"\` prop over hand-writ
|
|
|
12069
12598
|
- **NEVER** use rgba(), hsl(), or other CSS color functions with hardcoded values.
|
|
12070
12599
|
- **NEVER** use raw neutral-* or gray-* scale tokens (neutral-50, neutral-900, etc.) \u2014 these are internal to the theme and break in dark mode.
|
|
12071
12600
|
- **ALWAYS** use semantic tokens for text and backgrounds:
|
|
12072
|
-
- Text: \`var(--ggui-color-
|
|
12073
|
-
- Backgrounds: \`var(--ggui-color-
|
|
12601
|
+
- Text: \`var(--ggui-color-onContainer)\` or \`var(--ggui-color-onSunken)\`
|
|
12602
|
+
- Backgrounds: \`var(--ggui-color-container)\` or \`var(--ggui-color-sunken)\`
|
|
12074
12603
|
- Borders: \`var(--ggui-color-outline)\` or \`var(--ggui-color-outlineVariant)\`
|
|
12075
12604
|
- For branded elements use \`var(--ggui-color-primary-*)\` scale tokens \u2014 these ARE safe because primary adapts per theme.
|
|
12076
|
-
- For card backgrounds use \`var(--ggui-color-
|
|
12605
|
+
- For card backgrounds use \`var(--ggui-color-container)\` or \`var(--ggui-color-primary-50)\`
|
|
12077
12606
|
|
|
12078
12607
|
## Spacing
|
|
12079
12608
|
|
|
@@ -12168,7 +12697,7 @@ Use \`var(--ggui-spacing-N)\` for all padding, gap, and margin values. **Never u
|
|
|
12168
12697
|
\`\`\`
|
|
12169
12698
|
|
|
12170
12699
|
### Color usage guide
|
|
12171
|
-
- **
|
|
12700
|
+
- **Layering roles** (ground, container, elevated, sunken + their on* inks; outline): one kind of AREA each \u2014 use them for every background/text/border decision; they adapt to any theme
|
|
12172
12701
|
- **primary-50/100**: Section backgrounds, highlight strips, card headers
|
|
12173
12702
|
- **primary-200/300**: Borders, dividers, focus rings, input outlines
|
|
12174
12703
|
- **primary-500/600**: Icons, links, labels, badges, buttons, CTAs
|
|
@@ -12177,7 +12706,7 @@ Use \`var(--ggui-spacing-N)\` for all padding, gap, and margin values. **Never u
|
|
|
12177
12706
|
|
|
12178
12707
|
The primary palette is the app's brand \u2014 use it throughout (headers, accents, borders, interactive elements), not just on the submit button.
|
|
12179
12708
|
|
|
12180
|
-
**Note:** Do NOT add fallback values to var() (e.g., var(--ggui-color-
|
|
12709
|
+
**Note:** Do NOT add fallback values to var() (e.g., var(--ggui-color-container, #fafafa)). Just use var(--ggui-color-container) \u2014 the theme provides all values.
|
|
12181
12710
|
|
|
12182
12711
|
## Motion & Animation
|
|
12183
12712
|
|
|
@@ -12433,7 +12962,7 @@ ${bullets}`;
|
|
|
12433
12962
|
}
|
|
12434
12963
|
|
|
12435
12964
|
// src/harness/runtime.ts
|
|
12436
|
-
function buildSystemPrompt2(userRequest, shellType, screen, axisDelta, appGadgets, gadgetTypes) {
|
|
12965
|
+
function buildSystemPrompt2(userRequest, shellType, screen, axisDelta, appGadgets, gadgetTypes, designMode, canvas) {
|
|
12437
12966
|
return buildSystemPrompt({
|
|
12438
12967
|
userRequest,
|
|
12439
12968
|
shellType,
|
|
@@ -12444,7 +12973,9 @@ function buildSystemPrompt2(userRequest, shellType, screen, axisDelta, appGadget
|
|
|
12444
12973
|
primitivesDoc: PRIMITIVES_DOCUMENTATION,
|
|
12445
12974
|
wireDoc: WIRE_DOCUMENTATION,
|
|
12446
12975
|
appGadgets,
|
|
12447
|
-
gadgetTypes
|
|
12976
|
+
gadgetTypes,
|
|
12977
|
+
designMode,
|
|
12978
|
+
canvas
|
|
12448
12979
|
// criteriaBlock left undefined — ui-gen fills default from open CRITERIA.
|
|
12449
12980
|
});
|
|
12450
12981
|
}
|
|
@@ -13599,6 +14130,7 @@ var stepperAdopted = {
|
|
|
13599
14130
|
values: ["multi-step"],
|
|
13600
14131
|
run(input) {
|
|
13601
14132
|
if (input.compiledCode === null) return [];
|
|
14133
|
+
if (input.designMode === "free") return [];
|
|
13602
14134
|
if (/<Stepper[\s/>]/.test(input.sourceCode)) return [];
|
|
13603
14135
|
return [
|
|
13604
14136
|
mkIssue2(
|
|
@@ -15503,6 +16035,7 @@ function getSoftWarnings(code) {
|
|
|
15503
16035
|
// src/coding-agent/tools.ts
|
|
15504
16036
|
init_check();
|
|
15505
16037
|
init_validation();
|
|
16038
|
+
init_design_mode();
|
|
15506
16039
|
function getComponentDocumentation(name) {
|
|
15507
16040
|
const marker = `### ${name}`;
|
|
15508
16041
|
const startIdx = PRIMITIVES_DOCUMENTATION.indexOf(marker);
|
|
@@ -15513,7 +16046,7 @@ function getComponentDocumentation(name) {
|
|
|
15513
16046
|
const section = nextMarker === -1 ? PRIMITIVES_DOCUMENTATION.slice(startIdx) : PRIMITIVES_DOCUMENTATION.slice(startIdx, nextMarker);
|
|
15514
16047
|
return section.replace(/\\n/g, "\n").replace(/\\"/g, '"').slice(0, 3e3);
|
|
15515
16048
|
}
|
|
15516
|
-
async function autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes) {
|
|
16049
|
+
async function autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode = DEFAULT_DESIGN_MODE) {
|
|
15517
16050
|
const commitStart = Date.now();
|
|
15518
16051
|
const raw = workspace.read();
|
|
15519
16052
|
if (!raw && raw !== "") {
|
|
@@ -15550,7 +16083,8 @@ async function autoCommit(workspace, commitMeta, message, contract, contextPolic
|
|
|
15550
16083
|
formatted,
|
|
15551
16084
|
contract,
|
|
15552
16085
|
buildResult2.errors,
|
|
15553
|
-
gadgetTypes
|
|
16086
|
+
gadgetTypes,
|
|
16087
|
+
designMode
|
|
15554
16088
|
);
|
|
15555
16089
|
const tier0Fails = tier0Issues.filter((i) => i.result === "fail");
|
|
15556
16090
|
const selfCheckPassed = tier0Fails.length === 0;
|
|
@@ -15595,7 +16129,7 @@ ${errors.join("\n")}
|
|
|
15595
16129
|
Fix the issues.`
|
|
15596
16130
|
};
|
|
15597
16131
|
}
|
|
15598
|
-
async function executeTool(workspace, tool, input, commitMeta, contract, applyPatch2, contextPolicy, gadgetTypes) {
|
|
16132
|
+
async function executeTool(workspace, tool, input, commitMeta, contract, applyPatch2, contextPolicy, gadgetTypes, designMode = DEFAULT_DESIGN_MODE) {
|
|
15599
16133
|
switch (tool) {
|
|
15600
16134
|
case "write":
|
|
15601
16135
|
case "rewrite": {
|
|
@@ -15614,7 +16148,7 @@ async function executeTool(workspace, tool, input, commitMeta, contract, applyPa
|
|
|
15614
16148
|
const lineCount = code.split("\n").length;
|
|
15615
16149
|
const message = input.commit_message || `write ${lineCount} lines`;
|
|
15616
16150
|
console.log(`[coding-agent] write: ${lineCount} lines \u2192 auto-commit`);
|
|
15617
|
-
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes);
|
|
16151
|
+
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
|
|
15618
16152
|
}
|
|
15619
16153
|
case "apply_changes": {
|
|
15620
16154
|
const currentFile = workspace.read();
|
|
@@ -15789,7 +16323,7 @@ The file now reflects your latest patch. Submit a follow-up apply_changes target
|
|
|
15789
16323
|
await workspace.stage();
|
|
15790
16324
|
const message = input.commit_message || "apply changes";
|
|
15791
16325
|
console.log(`[coding-agent] apply_changes: ${changes.length} changes applied \u2192 auto-commit`);
|
|
15792
|
-
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes);
|
|
16326
|
+
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
|
|
15793
16327
|
}
|
|
15794
16328
|
case "get_components_info": {
|
|
15795
16329
|
const names = input.names ?? [];
|
|
@@ -16245,7 +16779,10 @@ ${iconNamesCache}`;
|
|
|
16245
16779
|
// Forward the third-party wrapper `.d.ts` map so autoCommit's
|
|
16246
16780
|
// typecheck overlays the real wrapper declarations and a generated
|
|
16247
16781
|
// direct gadget import gets strict option/return narrowing.
|
|
16248
|
-
harness.what.gadgetTypes
|
|
16782
|
+
harness.what.gadgetTypes,
|
|
16783
|
+
// Which tier-0 legs fire — the design-vocabulary legs stand down in
|
|
16784
|
+
// `free` mode; every contract leg runs in both.
|
|
16785
|
+
harness.designMode
|
|
16249
16786
|
);
|
|
16250
16787
|
const toolMs = Date.now() - toolStart;
|
|
16251
16788
|
if (forceEscape && ctx.dupeBreak && call.name === "apply_changes") {
|
|
@@ -16656,7 +17193,11 @@ async function runEvalRound(ctx, input) {
|
|
|
16656
17193
|
{
|
|
16657
17194
|
sourceCode: currentSource,
|
|
16658
17195
|
originalPrompt: userPrompt,
|
|
16659
|
-
contract
|
|
17196
|
+
contract,
|
|
17197
|
+
// Criteria + mother prompt follow the triad that produced
|
|
17198
|
+
// the source (free mode: color-only tokens, arm-neutral
|
|
17199
|
+
// visual judging).
|
|
17200
|
+
designMode: harness.designMode
|
|
16660
17201
|
},
|
|
16661
17202
|
{
|
|
16662
17203
|
provider: mapProviderForEvaluator(evaluationAgent.provider),
|
|
@@ -16685,6 +17226,8 @@ async function runEvalRound(ctx, input) {
|
|
|
16685
17226
|
passThreshold: visualThreshold,
|
|
16686
17227
|
sampleProps: visualEvaluation?.sampleProps,
|
|
16687
17228
|
viewport: visualEvaluation?.viewport,
|
|
17229
|
+
// Per-canvas judging (arm-neutral) — absent = single shot.
|
|
17230
|
+
...visualEvaluation?.canvases !== void 0 ? { canvases: visualEvaluation.canvases } : {},
|
|
16688
17231
|
// #484/#489 — same threading as the LLM-eval leg above:
|
|
16689
17232
|
// the visual eval's multimodal call runs inside the same
|
|
16690
17233
|
// concurrent generation, so it needs the same routing
|
|
@@ -16698,7 +17241,8 @@ async function runEvalRound(ctx, input) {
|
|
|
16698
17241
|
]);
|
|
16699
17242
|
evalLlmMs = Date.now() - evalLlmStart;
|
|
16700
17243
|
llmResult = llm;
|
|
16701
|
-
visualIssues = visual;
|
|
17244
|
+
visualIssues = visual?.issues ?? null;
|
|
17245
|
+
const visualSummary = visual?.summary;
|
|
16702
17246
|
if (llmResult) {
|
|
16703
17247
|
costTracker.record(evaluationAgent.model, llmResult.inputTokens, llmResult.outputTokens);
|
|
16704
17248
|
evalTokens = {
|
|
@@ -16715,7 +17259,11 @@ async function runEvalRound(ctx, input) {
|
|
|
16715
17259
|
evalResult = {
|
|
16716
17260
|
issues: allIssues,
|
|
16717
17261
|
pass: allPass,
|
|
16718
|
-
...llmResult?.criteriaCoverage !== void 0 ? { criteriaCoverage: llmResult.criteriaCoverage } : {}
|
|
17262
|
+
...llmResult?.criteriaCoverage !== void 0 ? { criteriaCoverage: llmResult.criteriaCoverage } : {},
|
|
17263
|
+
// Per-canvas visual summary — present only when canvases were
|
|
17264
|
+
// requested and the leg ran; every later re-stamp spreads this
|
|
17265
|
+
// base so it survives to the assembled result.
|
|
17266
|
+
...visualSummary !== void 0 ? { visual: visualSummary } : {}
|
|
16719
17267
|
};
|
|
16720
17268
|
const fails = allIssues.filter((i) => i.result === "fail");
|
|
16721
17269
|
const warns = allIssues.filter((i) => i.result === "warn");
|
|
@@ -17359,7 +17907,8 @@ async function initSession(input) {
|
|
|
17359
17907
|
sourceCode: "",
|
|
17360
17908
|
originalPrompt: params.userPrompt,
|
|
17361
17909
|
contract: params.contract,
|
|
17362
|
-
designContext: void 0
|
|
17910
|
+
designContext: void 0,
|
|
17911
|
+
designMode: harness.designMode
|
|
17363
17912
|
},
|
|
17364
17913
|
{
|
|
17365
17914
|
provider: mapProviderForEvaluator(evalSpec.provider),
|
|
@@ -17422,6 +17971,7 @@ async function initSession(input) {
|
|
|
17422
17971
|
}
|
|
17423
17972
|
|
|
17424
17973
|
// src/adapters/generation-dispatch.ts
|
|
17974
|
+
init_design_mode();
|
|
17425
17975
|
function mapProviderForLLMRouter(provider) {
|
|
17426
17976
|
return provider === "claude" ? "anthropic" : provider;
|
|
17427
17977
|
}
|
|
@@ -17481,6 +18031,9 @@ async function dispatchGeneration(params) {
|
|
|
17481
18031
|
// systemPromptBuilder closure below also captures `gadgetTypes`
|
|
17482
18032
|
// from `params` directly for the prompt `Type:` lines.
|
|
17483
18033
|
...params.gadgetTypes !== void 0 ? { gadgetTypes: params.gadgetTypes } : {},
|
|
18034
|
+
// Design mode + canvas — the harness carries them to every leg.
|
|
18035
|
+
...params.designMode !== void 0 ? { designMode: params.designMode } : {},
|
|
18036
|
+
...params.canvas !== void 0 ? { canvas: params.canvas } : {},
|
|
17484
18037
|
// Policy is pre-resolved at the dispatch boundary so `createHarness`
|
|
17485
18038
|
// stays free of experiment plumbing. `resolveHarnessPolicy` applies
|
|
17486
18039
|
// the `GGUI_POLICY_PROFILE` experimental-profile layer and hands the
|
|
@@ -17500,13 +18053,15 @@ async function dispatchGeneration(params) {
|
|
|
17500
18053
|
// prompt's gadget catalog renders a `Type:` line per third-party
|
|
17501
18054
|
// gadget (the LLM sees a wrapper's real call shape it otherwise
|
|
17502
18055
|
// can't know).
|
|
17503
|
-
systemPromptBuilder: ({ userRequest, shellType, screen, axisDelta }) => buildSystemPrompt2(
|
|
18056
|
+
systemPromptBuilder: ({ userRequest, shellType, screen, axisDelta, designMode, canvas }) => buildSystemPrompt2(
|
|
17504
18057
|
userRequest,
|
|
17505
18058
|
shellType,
|
|
17506
18059
|
screen,
|
|
17507
18060
|
axisDelta,
|
|
17508
18061
|
params.appGadgets,
|
|
17509
|
-
params.gadgetTypes
|
|
18062
|
+
params.gadgetTypes,
|
|
18063
|
+
designMode,
|
|
18064
|
+
canvas
|
|
17510
18065
|
),
|
|
17511
18066
|
// Pre-filtered axis-check registry. The `matches()` predicate
|
|
17512
18067
|
// selects the checks relevant to this generation's axis vector.
|
|
@@ -17650,7 +18205,16 @@ Your plan is a commitment, not a design doc. Keep it tight. Do not over-plan; ju
|
|
|
17650
18205
|
);
|
|
17651
18206
|
}
|
|
17652
18207
|
const finalSource = runResult.finalSource ?? "";
|
|
17653
|
-
|
|
18208
|
+
const result = await assembleGenerationResult({ session, telemetry, source: finalSource });
|
|
18209
|
+
return withArmRecord(result, params);
|
|
18210
|
+
}
|
|
18211
|
+
function withArmRecord(result, params) {
|
|
18212
|
+
const recordedCanvas = params.canvas ?? (params.designMode === "free" ? canvasForRendering(params.shellType, params.screen) : void 0);
|
|
18213
|
+
return {
|
|
18214
|
+
...result,
|
|
18215
|
+
...params.designMode !== void 0 ? { designMode: params.designMode } : {},
|
|
18216
|
+
...recordedCanvas !== void 0 ? { canvas: recordedCanvas } : {}
|
|
18217
|
+
};
|
|
17654
18218
|
}
|
|
17655
18219
|
|
|
17656
18220
|
export { dispatchGeneration };
|