# Introduction (/docs) Gridland renders the same React components to a browser canvas, a terminal, or plain text for AI agents, and ships component source directly into your project instead of as an installable package. ## Two Runtimes, One Tree Your Gridland components run in two places from the same source. In a browser, [`@gridland/web`](/docs/api/gridland-web) draws them to an HTML5 ``, so you can drop a TUI inside any React app. In a terminal, [`@gridland/bun`](/docs/api/gridland-bun) draws them to real stdout over a native FFI bridge, so you can ship standalone CLIs with `bun build --compile`. Both runtimes are built on the [OpenTUI](https://opentui.com) engine with Yoga for Flexbox layout, wired into React through a custom reconciler. No xterm.js, no terminal emulator, no hidden iframe. Just cells. ## Readable by Agents The same component tree renders to plain text for AI agents, crawlers, and screen readers. A TUI your users run interactively is, by default, also a document an LLM can read. No separate accessibility layer, no server-side rewrite. See [SSR for Agents](/docs/guides/ssr-for-agents). ## You Own the Components Gridland borrows [shadcn/ui](https://ui.shadcn.com)'s distribution model. Run `create-gridland add button` and the source lands in your project, where you can read, edit, and version it like any other file. If a component doesn't fit your app, change it. It's yours. ## Pixel-Perfect Alignment Every element sizes in character cells, not pixels, so layouts stay perfectly aligned across font sizes, zoom levels, and terminal widths. The API is standard Flexbox via Yoga, so `` is 40 columns wide and the rest of the layout composes the way you already know. See [Rendering](/docs/core-concepts/rendering) for the full mental model.
Scaffold a new project or add Gridland to an existing app. Browse the component library and install them via the shadcn registry. Render your TUI as plain text so AI agents and crawlers can read it. Ship a standalone CLI using Bun's compile step. # @gridland/bun (/docs/api/gridland-bun) `@gridland/bun` is the Bun-native runtime package for building Gridland CLI apps. It re-exports everything from [`@gridland/utils`](/docs/api/gridland-utils) plus native-only exports that require the Bun runtime. ## Installation ```bash bun add @gridland/bun ``` ## Native Exports These exports are only available in Bun and are not included in `@gridland/utils`: ```tsx title="Import" createCliRenderer, CliRenderer, TerminalConsole, NativeSpanFeed, setRenderLibPath, } from "@gridland/bun" ``` | Export | Description | | ----------------------------- | -------------------------------------------------- | | `createCliRenderer(options?)` | Create a CLI renderer that renders to the terminal | | `CliRenderer` | The underlying renderer class | | `TerminalConsole` | Terminal-aware console implementation | | `NativeSpanFeed` | Native span feed for styled text rendering | | `setRenderLibPath(path)` | Set the path to the native rendering library | ## Re-exported from @gridland/utils All exports from `@gridland/utils` are re-exported, including hooks (`useKeyboard`, `useTerminalDimensions`) and helpers (`isBrowser`, `calculateGridSize`). See the [`@gridland/utils` API reference](/docs/api/gridland-utils) for details. Headless rendering APIs (`HeadlessRenderer`, `createHeadlessRoot`, `bufferToText`) live in [`@gridland/web`](/docs/api/gridland-web), not here. ## Usage See the [Compile to Binary](/docs/guides/compile-binary) guide for a full walkthrough of building CLI apps with `@gridland/bun`. # @gridland/testing (/docs/api/gridland-testing) `@gridland/testing` mounts Gridland components into an in-memory buffer for unit tests. It exposes one entry point — `renderTui` — plus the `Screen`, `KeySender`, and `waitFor` primitives. See the [Testing guide](/docs/guides/testing) for end-to-end examples. ## `renderTui` ```ts const tui = renderTui(node, options?) ``` Mounts a React tree into an in-memory buffer and returns a `TuiInstance`. The call is synchronous — the initial render pass completes before `renderTui` returns. **Parameters:** | Parameter | Type | Description | | --------- | ------------------- | --------------------------- | | `node` | `ReactNode` | The Gridland tree to render | | `options` | `RenderTuiOptions?` | Buffer dimensions | **`RenderTuiOptions`:** | Option | Type | Default | Description | | ------ | -------- | ------- | ----------------- | | `cols` | `number` | `80` | Number of columns | | `rows` | `number` | `24` | Number of rows | **Returns:** `TuiInstance` ## `TuiInstance` | Property | Type | Description | | ---------- | ---------------------------------------- | -------------------------------------------------------------- | | `screen` | `Screen` | Query helpers for reading buffer content | | `keys` | `KeySender` | Keyboard input simulation | | `waitFor` | `(condition, options?) => Promise` | Poll until `condition` holds (bound to this instance's screen) | | `flush` | `() => void` | Force a synchronous render pass and capture a new frame | | `rerender` | `(node: ReactNode) => void` | Replace the rendered tree with a new one | | `unmount` | `() => void` | Unmount the tree and release the renderer | ## `Screen` The `Screen` class provides read-only access to the cell buffer. | Method | Description | | -------------------------- | ------------------------------------------------------ | | `text()` | Full screen text with trailing spaces trimmed per line | | `rawText()` | Full screen text preserving all spaces | | `contains(text: string)` | `true` if the trimmed text includes `text` | | `matches(pattern: RegExp)` | `true` if the trimmed text matches `pattern` | | `line(n: number)` | The `n`th line (0-indexed) or `""` if out of range | | `lines()` | All non-empty lines as an array | | `frames()` | Captured frame snapshots — one per render pass | | `captureFrame()` | Manually push the current frame onto the snapshot list | | `attributeAt(row, col)` | Raw u32 text attributes at a cell | | `fgAt(row, col)` | Foreground RGBA tuple `[r, g, b, a]` at a cell | | `width` | Buffer width in cells | | `height` | Buffer height in cells | ## `KeySender` Dispatches synthetic keypress events through the focus system. | Method | Description | | ---------------------------------------- | ----------------------------------------- | | `type(text: string)` | Type a string character by character | | `press(char: string)` | Press a single character key | | `raw(data: string)` | Send a raw sequence (e.g. an escape code) | | `enter()` | Press Enter | | `escape()` | Press Escape | | `tab()` | Press Tab | | `backspace()` | Press Backspace | | `delete()` | Press Delete | | `space()` | Press Space | | `up()` / `down()` / `left()` / `right()` | Arrow keys | | `home()` / `end()` | Home / End | | `pageUp()` / `pageDown()` | Page Up / Page Down | All key helpers are methods, not properties — call them with `()`. ## `waitFor` Standalone version of `tui.waitFor`. Polls a condition until it holds or times out. ```ts const tui = renderTui() // Wait for a literal string on the screen await waitFor(tui.screen, "Loaded") // Or wait for an assertion to stop throwing await waitFor(tui.screen, () => { expect(tui.screen.contains("Ready")).toBe(true) }) ``` **Parameters:** | Parameter | Type | Description | | ----------- | ------------------------ | -------------------------------------------------------------- | | `screen` | `Screen` | The screen to poll | | `condition` | `string \| (() => void)` | Literal substring, or an assertion that throws until satisfied | | `options` | `WaitForOptions?` | Timeout and poll interval | **`WaitForOptions`:** | Option | Type | Default | Description | | ---------- | -------- | ------- | ------------------- | | `timeout` | `number` | `3000` | Max wait time in ms | | `interval` | `number` | `50` | Poll interval in ms | On timeout, `waitFor` throws an error that includes the current screen contents. ## `cleanup` ```ts afterEach(() => { cleanup() }) ``` Unmounts every active `TuiInstance` created by `renderTui`. Wire this into `afterEach` once and you can skip calling `tui.unmount()` in each test. # @gridland/ui (/docs/api/gridland-ui) Individual components can be installed via the shadcn CLI. See [Manual Installation → Install components](/docs/getting-started/manual-installation#install-components) for details. ```bash bunx create-gridland add ``` Or, if you'd rather call shadcn directly: ```bash bunx shadcn@latest add @gridland/ ``` ## Components | Component | Description | Docs | | ------------------ | -------------------------------------------------------------------------- | --------------------------------------------------- | | `GridlandProvider` | Root provider supplying theme and an implicit `` | — | | `Ascii` | ASCII art text with multiple font styles | [Ascii](/docs/components/ascii) | | `ChainOfThought` | Expandable reasoning blocks with step status | [ChainOfThought](/docs/components/chain-of-thought) | | `Gradient` | Color gradients across text characters | [Gradient](/docs/components/gradient) | | `Link` | Clickable hyperlink with configurable underline | [Link](/docs/components/link) | | `Message` | Chat message with role-based styling and streaming | [Message](/docs/components/message) | | `Modal` | Overlay dialog with border styles | [Modal](/docs/components/modal) | | `MultiSelect` | Multi-selection input with groups and validation | [MultiSelect](/docs/components/multi-select) | | `PromptInput` | Chat input with slash commands and AI SDK integration | [PromptInput](/docs/components/prompt-input) | | `SelectInput` | Single-selection dropdown with groups | [SelectInput](/docs/components/select-input) | | `SideNav` | Sidebar navigation with keyboard-driven focus | [SideNav](/docs/components/side-nav) | | `Spinner` | Animated loading indicators | [Spinner](/docs/components/spinner) | | `StatusBar` | Horizontal bar displaying keybinding hints | [StatusBar](/docs/components/status-bar) | | `Table` | Data table with compound sub-components | [Table](/docs/components/table) | | `Tabs` | Tabbed content with keyboard navigation | [Tabs](/docs/components/tabs) | | `TerminalWindow` | macOS-style terminal window chrome | [TerminalWindow](/docs/components/terminal-window) | | `TextInput` | Single-line text input with validation | [TextInput](/docs/components/text-input) | ## Utility Functions ```tsx ``` | Function | Signature | Description | | ------------------ | ----------------------------------------------- | -------------------------------------------- | | `generateGradient` | `(colors: string[], steps: number) => string[]` | Generate an array of interpolated hex colors | | `hexToRgb` | `(hex: string) => { r, g, b }` | Convert hex to RGB | | `rgbToHex` | `(rgb: { r, g, b }) => string` | Convert RGB to hex | # @gridland/utils (/docs/api/gridland-utils) `@gridland/utils` contains portable hooks and utilities that work in both browser and CLI environments. It has no DOM or Canvas dependencies. ## Hooks ### `useKeyboard` Subscribe to keyboard events. See [useKeyboard](/docs/hooks/use-keyboard). ```tsx title="Import" ``` ### `useTerminalDimensions` Get the current terminal dimensions. See [useTerminalDimensions](/docs/hooks/use-terminal-dimensions). ```tsx title="Import" ``` ## Headless Rendering Headless rendering APIs live in `@gridland/web`, not `@gridland/utils`. See [SSR for Agents](/docs/guides/ssr-for-agents) and the [@gridland/web API reference](/docs/api/gridland-web) for details. ## Utilities ```tsx title="Import" ``` | Function | Description | | ------------------------------------------------------------- | -------------------------------------------------- | | `isBrowser()` | Returns `true` if running in a browser environment | | `isCanvasSupported()` | Returns `true` if HTML5 Canvas is available | | `calculateGridSize(widthPx, heightPx, cellWidth, cellHeight)` | Convert pixel dimensions to grid columns/rows | # @gridland/web (/docs/api/gridland-web) ## TypeScript Setup Gridland uses custom JSX elements (``, ``, ``, etc.) that aren't standard HTML. To get type-checking and autocomplete, add the JSX type declarations to your `tsconfig.json`: ```json { "include": ["src", "node_modules/@gridland/web/src/gridland-jsx.d.ts"] } ``` Or add a triple-slash reference in any `.d.ts` file in your project: ```ts /// ``` This is required for TypeScript projects using React 19. The declarations handle conflicts with React's built-in `IntrinsicElements` automatically. ## TUI The main mounting component. See [Rendering → In the browser](/docs/core-concepts/rendering#in-the-browser) for full documentation. ```tsx title="Import" ``` ## Vite Plugin ```tsx title="Import" ``` ### `gridlandWebPlugin()` Returns an array of Vite plugins that configure module resolution for the opentui engine. Resolves `@opentui/core`, `@opentui/react`, and `@opentui/ui` from peer dependencies. ## Hooks Portable hooks (`useKeyboard`, `useTerminalDimensions`) live in [`@gridland/utils`](/docs/api/gridland-utils). ### Browser-only hooks ```tsx title="Import" ``` ## Browser Context ```tsx title="Import" ``` `BrowserContext` is the React context that `` installs for its children. It carries the renderer, canvas element, and cell dimensions so browser-only hooks like `useFileDrop`, `usePaste`, and `useBrowserContext` can read from the same source of truth that `` provides. Call `useBrowserContext()` inside a descendant of `` to read the current `BrowserContextValue`. You only need to reach for the raw `BrowserContext` symbol when wiring up a custom `Context.Consumer` or a test provider. ## Internal Modules These modules are used internally by `TUI` and are not part of the public API. They may change without notice between releases. ### BrowserBuffer In-memory cell grid that stores character, foreground, background, and attribute data for each cell position. ### BrowserRenderContext Implements the `RenderContext` interface for the browser environment. Manages the buffer, layout, painting, and input handling. ### CanvasPainter Draws the cell grid to an HTML5 Canvas context. Handles font measurement, glyph caching, and efficient dirty-region repainting. ### BrowserTextBuffer / BrowserTextBufferView Browser-safe replacements for the native text buffer modules. Provide line-based text storage and viewport windowing. ### BrowserSyntaxStyle Stub replacement for the native syntax highlighting module. Returns no highlighting in the browser (syntax highlighting requires tree-sitter). ### SelectionManager Handles text selection on the canvas. Tracks selection start/end positions and provides methods to get selected text. # AI Chat Interface (/docs/blocks/ai-chat-interface) A full-featured AI chat interface with `SideNav` for conversation history, multi-model switching via slash commands, `ChainOfThought` reasoning blocks, and streaming support. Built on the Vercel AI SDK and compatible with any model provider via OpenRouter, OpenAI, Anthropic, or custom endpoints.
This demo connects to a live LLM via a Next.js API route. Set your `OPENROUTER_API_KEY` in `.env` to try it. ## Installation ```bash title="Terminal" bun add @gridland/ui @ai-sdk/react @openrouter/ai-sdk-provider ai ``` ```bash title="Terminal" npm install @gridland/ui @ai-sdk/react @openrouter/ai-sdk-provider ai ``` ```bash title="Terminal" yarn add @gridland/ui @ai-sdk/react @openrouter/ai-sdk-provider ai ``` ```bash title="Terminal" pnpm add @gridland/ui @ai-sdk/react @openrouter/ai-sdk-provider ai ``` ## Server Route Create an API route that streams responses from your model provider. ```ts title="app/api/chat/route.ts" const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY, }) export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json() const result = streamText({ model: openrouter.chat("openai/gpt-4o-mini"), messages: await convertToModelMessages(messages), }) return result.toUIMessageStreamResponse() } ``` ## Client Component The chat interface uses `SideNav` for conversation history, `useChat` from the Vercel AI SDK for streaming, and `CommandProvider` for slash commands like `/model` and `/clear`. ```tsx title="chat-interface.tsx" ChainOfThought, ChainOfThoughtHeader, ChainOfThoughtContent, ChainOfThoughtStep, } from "@/components/ui/chain-of-thought" function ChatPanel({ conversationId, selectedModelId, isInteracting }) { const [showModelPicker, setShowModelPicker] = useState(false) const { messages, status, sendMessage, stop, setMessages } = useChat({ id: conversationId, api: "/api/chat", }) const chatStatus: ChatStatus = status === "streaming" ? "streaming" : status === "submitted" ? "submitted" : status === "error" ? "error" : "ready" return ( {/* Message area */} {messages.map((msg, i) => { const isLast = i === messages.length - 1 const msgStreaming = isLast && msg.role === "assistant" && status === "streaming" return ( {msg.parts?.map((part, j) => { switch (part.type) { case "text": return {part.text} default: return null } })} ) })} {/* Prompt input — registers with the focus system via focusId */} ) } function ChatApp() { const [navItems] = useState([{ id: "new-chat", name: "+ New chat" }]) return ( {({ activeItem, isInteracting }) => ( )} ) } ``` ## Components Used This block combines several Gridland components: | Component | Role | | ------------------------------------------------------------------- | -------------------------------------------------------------------------- | | [`SideNav`](/docs/components/side-nav) | Sidebar navigation with conversation history and keyboard-driven switching | | [`Message`](/docs/components/message) | Renders individual messages with role-based styling and streaming | | [`PromptInput`](/docs/components/prompt-input) | Input field with submit/stop, slash commands, and file mentions | | [`ChainOfThought`](/docs/components/chain-of-thought) | Expandable reasoning blocks for models with extended thinking | | [`SelectInput`](/docs/components/select-input) | Model picker dropdown inside a modal | | [`Modal`](/docs/components/modal) | Overlay for model selection | | [`CommandProvider`](/docs/components/prompt-input#command-registry) | Registers `/model` and `/clear` slash commands | ## Customization ### Using a different model Swap the model ID in the API route to use any provider on OpenRouter: ```ts title="route.ts" const result = streamText({ model: openrouter.chat("anthropic/claude-sonnet-4"), messages: await convertToModelMessages(messages), }) ``` ### Direct provider (no OpenRouter) ```ts title="route.ts" const result = streamText({ model: anthropic("claude-sonnet-4-20250514"), messages: await convertToModelMessages(messages), }) ``` ### Adding ChainOfThought for reasoning models When using a model that supports extended thinking (e.g. `deepseek-r1`, `o1`), reasoning parts appear automatically. Compose `ChainOfThought` directly as a child of `Message`, above `MessageContent`: ```tsx const [expanded, setExpanded] = useState(false) const hasReasoning = msg.parts?.some(p => p.type === "reasoning") {hasReasoning && ( )} {msg.parts?.filter(p => p.type === "text").map((part, j) => ( {part.text} ))} ``` # Ascii (/docs/components/ascii) Renders text as large ASCII art using the `` OpenTUI intrinsic. Supports multiple font styles and custom colors.
## Installation ```bash title="Terminal" bunx create-gridland add ascii ``` ## Usage ```tsx ``` ```tsx ``` ## Examples ### Fonts Use the `font` prop to change the ASCII art style. ```tsx title="Font styles" ``` ### Custom Color Override the default theme color. ```tsx title="Custom color" ``` ### With Theme Color Use `useTheme()` for semantic coloring. ```tsx title="Theme color" const theme = useTheme() ``` ### As a Header ASCII art works well as a splash header for CLI apps. ```tsx title="App header" v1.2.0 — A tool for doing things ``` ## API Reference ### Ascii | Prop | Type | Default | Description | | ------- | ----------------------------------------- | --------------- | --------------------------- | | `text` | `string` | - | Text to render as ASCII art | | `font` | `"tiny" \| "block" \| "slick" \| "shade"` | `"tiny"` | ASCII font style | | `color` | `string` | `theme.primary` | Foreground color | # Chain of Thought (/docs/components/chain-of-thought) A collapsible compound component that visualizes AI reasoning steps with animated spinners, status indicators, and optional output content. SDK-agnostic — works with any provider that exposes thinking/reasoning data.
## Installation ```bash title="Terminal" bunx create-gridland add chain-of-thought ``` ## Usage ```tsx ChainOfThought, ChainOfThoughtHeader, ChainOfThoughtContent, ChainOfThoughtStep, } from "@/components/ui/chain-of-thought" ``` ```tsx ``` ## Examples ### Collapsed The default state is collapsed — only the header is visible. ```tsx title="Collapsed" ```
### Expanded Pass `defaultOpen` to start expanded. Steps show their status indicators. ```tsx title="Expanded" Build pipeline ```
### Step Output Pass children to a step to render output content below it with a pipe gutter. ```tsx title="With output" FAIL src/auth.test.ts — expected 200, got 401 ```
### Custom Header Pass children to the header to replace the default "Thought for" label. ```tsx title="Custom header" Analyzing code ``` ### Custom Icon Override the default status dot with a custom character. ```tsx title="Custom icon" ``` ### Controlled Use `open` and `onOpenChange` to control the collapsed state externally. ```tsx title="Controlled" const [open, setOpen] = useState(false) ``` ## Compound Components | Component | Description | | ----------------------- | ----------------------------------------------------------- | | `ChainOfThought` | Root container with collapsible state | | `ChainOfThoughtHeader` | Expand/collapse arrow with label and duration | | `ChainOfThoughtContent` | Content wrapper — renders `null` when collapsed | | `ChainOfThoughtStep` | Individual step with status dot, label, and optional output | ## API Reference ### ChainOfThought | Prop | Type | Default | Description | | -------------- | ------------------------- | ------- | ---------------------------------- | | `open` | `boolean` | - | Controlled open state | | `defaultOpen` | `boolean` | `false` | Default open state (uncontrolled) | | `onOpenChange` | `(open: boolean) => void` | - | Called when the open state changes | | `children` | `ReactNode` | - | Sub-components | ### ChainOfThoughtHeader | Prop | Type | Default | Description | | ---------- | ----------- | --------------- | ----------------------------------------------------- | | `duration` | `string` | - | Duration string shown after the label (e.g. `"3.2s"`) | | `children` | `ReactNode` | `"Thought for"` | Custom header text | ### ChainOfThoughtContent | Prop | Type | Description | | ---------- | ----------- | ------------------------- | | `children` | `ReactNode` | Steps to render when open | ### ChainOfThoughtStep | Prop | Type | Default | Description | | ------------- | --------------------------------------------- | -------- | --------------------------------------------------------- | | `label` | `string` | - | Primary label for the step | | `description` | `string` | - | Secondary detail shown dimmed after the label | | `status` | `"done" \| "running" \| "pending" \| "error"` | `"done"` | Step status | | `icon` | `string` | - | Custom icon character. Overrides the status-based default | | `isLast` | `boolean` | `false` | Hide the vertical pipe connector below | | `children` | `ReactNode` | - | Output content rendered below the step | ### ChainOfThoughtStepData Data type for driving steps from an array (e.g. via `.map()`). | Field | Type | Description | | ------------- | --------------------------------------------- | --------------------------------------------- | | `label` | `string` | Primary label for the step | | `description` | `string` | Secondary detail shown dimmed after the label | | `status` | `"done" \| "running" \| "pending" \| "error"` | Step status | | `icon` | `string` | Custom icon character | | `output` | `string` | Output text shown below the step | > **Note:** The `Step` type alias is deprecated. Use `ChainOfThoughtStepData` instead. ### Status Indicators | Status | Dot | Color | Style | | --------- | ---- | --------------- | -------------- | | `done` | ● | `theme.success` | Normal | | `running` | ⠋⠙⠹… | `theme.primary` | Bold, animated | | `pending` | ○ | `theme.muted` | Dim | | `error` | ● | `theme.error` | Normal | # Gradient (/docs/components/gradient) Applies a color gradient across text characters. Choose from 13 named presets or provide custom hex colors.
## Installation ```bash title="Terminal" bunx create-gridland add gradient ``` ## Usage ```tsx ``` ```tsx Hello World! ``` ## Examples ### Named Presets Use the `name` prop to select a built-in gradient. ```tsx title="Named gradient" Rainbow text Morning vibes Passion gradient ``` ### Custom Colors Pass an array of hex colors to create a custom gradient. ```tsx title="Custom colors" Custom gradient ``` ### Multiline Gradients work across multiline text — each line shares the same color stops. ```tsx title="Multiline" {"Line one\nLine two\nLine three"} ``` ## Available Gradients `cristal` · `teen` · `mind` · `morning` · `vice` · `passion` · `fruit` · `instagram` · `atlas` · `retro` · `summer` · `pastel` · `rainbow` ## API Reference ### Gradient | Prop | Type | Default | Description | | ---------- | -------------- | ------- | ----------------------------------------- | | `children` | `string` | - | Text content to apply the gradient to | | `name` | `GradientName` | - | Named gradient preset | | `colors` | `string[]` | - | Array of hex colors for a custom gradient | > `name` and `colors` are mutually exclusive — provide exactly one. ### Utility Functions The component also exports utility functions for programmatic gradient generation. | Function | Signature | Description | | ------------------ | ----------------------------------------------- | -------------------------------------------- | | `generateGradient` | `(colors: string[], steps: number) => string[]` | Generate an array of interpolated hex colors | | `hexToRgb` | `(hex: string) => { r, g, b }` | Convert hex to RGB | | `rgbToHex` | `(rgb: { r, g, b }) => string` | Convert RGB to hex | # Link (/docs/components/link) A clickable hyperlink for terminal UIs with configurable underline style and color.
## Installation ```bash title="Terminal" bunx create-gridland add link ``` ## Usage ```tsx ``` ```tsx Visit opentui.com ``` ## Examples ### Underline Styles Use the `underline` prop to change the underline rendering. ```tsx title="Underline styles" Solid underline Dashed underline Dotted underline No underline ``` ### Custom Color Override the default theme color. ```tsx title="Custom color" Green link Cyan link ``` ### Inline with Text Embed a link within a line of text using `` siblings. ```tsx title="Inline link" Check out opentui.com for more info. ``` ### Multiple Links ```tsx title="Navigation links" GitHub Docs Contact ``` ## API Reference ### Link | Prop | Type | Default | Description | | ----------- | ------------------------------------------- | -------------- | ----------------- | | `children` | `ReactNode` | - | Link text content | | `url` | `string` | - | Target URL | | `underline` | `"solid" \| "dashed" \| "dotted" \| "none"` | `"solid"` | Underline style | | `color` | `string` | `theme.accent` | Link text color | # Message (/docs/components/message) A chat message component with role-based styling and streaming support. Message is a thin layout shell — it provides alignment, background color, and context. Content goes inside `MessageContent`, text inside `MessageText`. Other concerns (tool calls, sources, reasoning, footer) are separate components composed alongside Message, not inside it.
## Installation ```bash title="Terminal" bunx create-gridland add message ``` ## Usage ```tsx ``` ```tsx Hello, can you help me? Of course! What do you need? ``` ## Examples ### With PromptInput Combine `Message` with `PromptInput` for a complete conversation view.
### Streaming Pass `isStreaming` to indicate the message is still being generated. ```tsx title="Streaming message" {partialText} ``` ### Markdown Use `MessageMarkdown` to render markdown content via the OpenTUI markdown intrinsic. ```tsx title="Markdown content" {"# Hello\n\nThis is **bold** text."} ``` ### Custom Background Color Override the default role-based background with `backgroundColor`. ```tsx title="Custom background" Custom styled message. ``` ### Mapping Vercel AI SDK Parts Map `message.parts` from `useChat` to sub-components. The consumer owns the mapping — the component has no SDK dependency. ```tsx title="Vercel AI SDK" const { messages, status } = useChat({ api: "/api/chat" }) {messages.map((msg, i) => { const isLast = i === messages.length - 1 const msgStreaming = isLast && msg.role === "assistant" && status === "streaming" return ( {msg.parts?.filter(p => p.type === "text").map((part, j) => ( {part.text} ))} ) })} ``` ## Compound Components All compound components read shared state (role, streaming, background color) from `Message` via context. No prop drilling needed. | Component | Description | | ----------------- | ----------------------------------------------- | | `MessageContent` | Bubble wrapper with role-based background color | | `MessageText` | Text with word wrap | | `MessageMarkdown` | Markdown content via OpenTUI intrinsic | ### useMessage Access message context from within any sub-component. ```tsx title="Custom sub-component" function MyCustomStatus() { const { role, isStreaming, textColor } = useMessage() return {role}: {isStreaming ? "streaming..." : "done"} } ``` ## API Reference ### Message | Prop | Type | Default | Description | | ----------------- | ----------------------------------- | ------- | -------------------------------------------------- | | `role` | `"user" \| "assistant" \| "system"` | - | Message role — determines alignment and background | | `isStreaming` | `boolean` | `false` | Whether the message is currently streaming | | `backgroundColor` | `string` | - | Override the default role-based background color | | `children` | `ReactNode` | - | Sub-components | ### MessageContent | Prop | Type | Default | Description | | ---------- | ----------- | ------- | ------------------------------------------------------- | | `children` | `ReactNode` | - | Compound components to render inside the message bubble | ### MessageText | Prop | Type | Default | Description | | ---------- | -------- | ------- | ------------ | | `children` | `string` | - | Text content | ### MessageMarkdown | Prop | Type | Default | Description | | ---------- | -------- | ------- | ------------------------- | | `children` | `string` | - | Markdown string to render | ### Migrating from v0 The following compound sub-components were removed in this version: | Removed | Replacement | | ------------------- | ---------------------------------------------- | | `Message.Content` | `MessageContent` (named export) | | `Message.Text` | `MessageText` (named export) | | `Message.Reasoning` | Use `ChainOfThought` component as a sibling | | `Message.ToolCall` | Build your own with `useMessage()` for context | | `Message.Source` | Build your own with `useMessage()` for context | | `Message.Footer` | Build your own with `useMessage()` for context | ### MessageContextValue ```tsx interface MessageContextValue { role: MessageRole isStreaming: boolean backgroundColor: string textColor: string } ``` # Modal (/docs/components/modal) A bordered container that overlays content, optionally displays a title, and listens for the Escape key to trigger a close callback.
## Installation ```bash title="Terminal" bunx create-gridland add modal ``` ## Usage ```tsx ``` ```tsx setOpen(false)}> Modal content here ``` ## Examples ### With Title Display a bold title at the top of the bordered container. ```tsx title="With title" setOpen(false)}> Your settings ``` ### Border Styles Use the `borderStyle` prop to change the border character set. ```tsx title="Border styles" Content Content Content ``` ### Custom Border Color Override the default border color. ```tsx title="Custom color" setOpen(false)}> Something went wrong ``` ### Without Title Omit the `title` prop for a plain bordered container. ```tsx title="No title" setOpen(false)}> Plain modal content ``` ## Controls * **Escape**: Calls `onClose` when provided ## API Reference ### Modal | Prop | Type | Default | Description | | ------------- | ---------------------------------------------- | ------------- | -------------------------------------------- | | `children` | `ReactNode` | - | Content rendered inside the modal | | `title` | `string` | - | Title displayed at the top inside the border | | `borderColor` | `string` | `theme.muted` | Color of the border | | `borderStyle` | `"single" \| "double" \| "rounded" \| "heavy"` | `"rounded"` | Border character set | | `onClose` | `() => void` | - | Called when Escape is pressed | Modal listens for Escape via `useKeyboard` from `@gridland/utils` internally — no keyboard plumbing is required from the caller. Render it anywhere inside a `GridlandProvider` or `FocusProvider` and the Escape handler will fire. # MultiSelect (/docs/components/multi-select) A multi-selection list with checkbox indicators, keyboard navigation, group headers, a submit row, and a submitted state.
## Installation ```bash title="Terminal" bunx create-gridland add multi-select ``` ## Usage ```tsx ``` ```tsx console.log("Selected:", values)} /> ``` `MultiSelect` registers with the focus system via `focusId`. Wrap your app in `GridlandProvider` (which mounts `` implicitly) or a standalone ``. Tab focuses the component, Enter selects it for interaction, then arrow keys navigate and space toggles items. ## Examples ### Controlled Use `selected` and `onChange` to control the selection externally. ```tsx title="Controlled" const [selected, setSelected] = useState(["ts"]) console.log("Selected:", values)} /> ``` ### Default Selected Set the initially selected items in uncontrolled mode. ```tsx title="Default selected" console.log("Selected:", values)} /> ``` ### Groups Use the `group` field on items to render group headers with separators. ```tsx title="With groups" console.log(values)} /> ``` ### Disabled Items Disable individual items so they cannot be toggled. ```tsx title="Disabled items" console.log(values)} /> ``` ### Max Selection Limit how many items can be selected. A counter is shown in the title. ```tsx title="Max selection" console.log(values)} /> ``` ### Required Show a required indicator and use `invalid` to display an error state. ```tsx title="Required with validation" console.log(values)} /> ``` ### Allow Empty Show the Submit row even when nothing is selected. ```tsx title="Allow empty submission" console.log(values)} /> ``` ### Disabled Disable the entire component. Navigation and selection are blocked. ```tsx title="Disabled" ``` ## Controls * **↑/↓** or **j/k**: Navigate items * **Enter** or **Space**: Toggle item selection (or submit when on the Submit row) * **a**: Select all * **x**: Clear all ## API Reference ### MultiSelect | Prop | Type | Default | Description | | ----------------- | ----------------------- | ------------------------------------- | --------------------------------------------- | | `items` | `MultiSelectItem[]` | `[]` | Array of selectable items | | `defaultSelected` | `V[]` | `[]` | Initially selected values (uncontrolled) | | `selected` | `V[]` | - | Selected values (controlled) | | `onChange` | `(values: V[]) => void` | - | Called when the selection changes | | `disabled` | `boolean` | `false` | Disable the entire component | | `invalid` | `boolean` | `false` | Show error state with destructive styling | | `errorMessage` | `string` | `"Please select at least one option"` | Custom error message when invalid | | `required` | `boolean` | `false` | Show required indicator (`*`) next to title | | `placeholder` | `string` | - | Placeholder text when items list is empty | | `maxCount` | `number` | - | Maximum number of selectable items | | `title` | `string` | `"Select"` | Title shown next to the diamond indicator | | `submittedStatus` | `string` | `"submitted"` | Status text shown after submit | | `limit` | `number` | `12` | Max visible rows before scrolling | | `enableSelectAll` | `boolean` | `true` | Enable select all with `a` key | | `enableClear` | `boolean` | `true` | Enable clear all with `x` key | | `highlightColor` | `string` | `theme.primary` | Color of the highlighted item | | `checkboxColor` | `string` | `theme.accent` | Color of the selection indicator | | `allowEmpty` | `boolean` | `false` | Show Submit row even when nothing is selected | | `onSubmit` | `(values: V[]) => void` | - | Called when the Submit row is selected | | `focusId` | `string` | auto-generated | Stable id for the focus system | | `autoFocus` | `boolean` | `false` | Focus this component on mount | ### MultiSelectItem | Field | Type | Description | | ---------- | ---------- | ----------------------- | | `label` | `string` | Display text | | `value` | `V` | Item value | | `key` | `string?` | Optional React key | | `group` | `string?` | Group header label | | `disabled` | `boolean?` | Disable individual item | # PromptInput (/docs/components/prompt-input) A chat input bar with slash command autocomplete, file mention suggestions, command history, and direct Vercel AI SDK integration. Supports both a self-contained default layout and a fully composable compound mode.
## Installation ```bash title="Terminal" bunx create-gridland add prompt-input ``` ## Usage ```tsx ``` ```tsx console.log("Sent:", msg.text)} /> ``` `PromptInput` registers with the focus system via `focusId`. Wrap your app in `GridlandProvider` (which mounts `` implicitly) or a standalone ``. When the component is focused, it accepts keystrokes and renders an `` intrinsic in the terminal runtime. ## Usage with AI SDKs `onSubmit` receives `{ text: string }`. Map it to your SDK of choice. ### Vercel AI SDK Direct pass-through — `sendMessage` accepts `{ text }` natively. ```tsx title="Vercel AI SDK" const { status, sendMessage, stop } = useChat({ /* transport config */ }) ``` ### Anthropic SDK ```tsx title="Anthropic SDK" const [status, setStatus] = useState("ready") { setStatus("submitted") const stream = anthropic.messages.stream({ model: "claude-sonnet-4-20250514", messages: [...history, { role: "user", content: msg.text }], }) setStatus("streaming") for await (const event of stream) { /* handle deltas */ } setStatus("ready") }} onStop={() => { stream.abort(); setStatus("ready") }} /> ``` ### OpenAI SDK ```tsx title="OpenAI SDK" { setStatus("submitted") const stream = await openai.chat.completions.create({ model: "gpt-4o", messages: [...history, { role: "user", content: msg.text }], stream: true, }) setStatus("streaming") for await (const chunk of stream) { /* handle deltas */ } setStatus("ready") }} /> ``` ## Examples ### Slash Commands Provide a `commands` array for `/` autocomplete. ```tsx title="Slash commands" console.log(msg.text)} /> ``` ### File Mentions Provide a `files` array for `@` mention autocomplete. ```tsx title="File mentions" console.log(msg.text)} /> ``` ### Custom Suggestion Provider Override the built-in `/` and `@` suggestions with your own logic. ```tsx title="Custom suggestions" { if (value.startsWith("#")) { return [ { text: "#bug", desc: "Bug report" }, { text: "#feature", desc: "Feature request" }, ].filter((s) => s.text.startsWith(value)) } return [] }} onSubmit={(msg) => console.log(msg.text)} /> ``` ### Controlled Use `value` and `onChange` to control the input externally. ```tsx title="Controlled" const [value, setValue] = useState("") console.log(msg.text)} /> ``` ### Model Label Display the active model name below the input. ```tsx title="With model" console.log(msg.text)} /> ``` ### Disabled (legacy) When not using `status`, use `disabled` and `disabledText` directly. ```tsx title="Disabled" ``` ## Compound Components For full control over layout, pass children to enter compound mode. Subcomponents read state from `PromptInput` via context — no prop drilling needed. ```tsx title="Compound usage" ``` ### Compound Components | Component | Description | | ------------------------- | ------------------------------------------------------------ | | `PromptInput.Textarea` | Prompt character + text with cursor | | `PromptInput.Suggestions` | Autocomplete dropdown (slash commands, @mentions) | | `PromptInput.Submit` | Status indicator: ⏎ ready, ◐ submitted, ■ streaming, ✕ error | | `PromptInput.Divider` | Horizontal rule (`─`) | | `PromptInput.StatusText` | Error text shown when `status` is `"error"` | | `PromptInput.Model` | Muted model label shown below the input | ## Provider Wrap your app in `PromptInputProvider` to lift input state outside of `PromptInput`. This lets you read or modify the input value and suggestions from sibling components. ```tsx title="Provider usage" ``` ### usePromptInputController Access lifted state from anywhere inside the provider tree. ```tsx title="Controller hook" const controller = usePromptInputController() controller.textInput.value // current text controller.textInput.setValue(v) // set text controller.textInput.clear() // clear text controller.suggestions.suggestions // current suggestions controller.suggestions.selectedIndex // selected index controller.suggestions.setSuggestions(s) // set suggestions controller.suggestions.setSelectedIndex(i) controller.suggestions.clear() ``` ### usePromptInput Access rendering state from within any compound subcomponent. ```tsx title="Custom subcomponent" function MyCustomStatus() { const { status, value, disabled } = usePromptInput() return {status}: {value.length} chars } ``` ## Command Registry Wrap your app in `CommandProvider` to let sibling components register slash commands that automatically appear in `PromptInput`'s autocomplete — without passing `commands` as a prop. ```tsx title="Command Registry" function ModelSwitcher() { useRegisterCommand({ cmd: "/model", desc: "Switch model" }) return null } function ClearButton() { useRegisterCommand({ cmd: "/clear", desc: "Clear conversation", onExecute: () => clearChat() }) return null } ``` ### useRegisterCommand Register a single command. Automatically unregisters on unmount. ```tsx title="Single command" useRegisterCommand({ cmd: "/help", desc: "Show commands" }) ``` ### useRegisterCommands Register multiple commands at once. Automatically unregisters on unmount. ```tsx title="Multiple commands" useRegisterCommands([ { cmd: "/model", desc: "Switch model" }, { cmd: "/clear", desc: "Clear conversation" }, ]) ``` ### useRegistryCommands Subscribe to all registered commands. Returns the current command list reactively. ```tsx title="Consuming commands" const commands = useRegistryCommands() // Returns PromptInputCommand[] — updates when commands are added/removed ``` ### CommandProvider | Prop | Type | Default | Description | | ---------- | ----------------- | ------- | -------------------------------------------------------------------- | | `children` | `ReactNode` | - | Components that can register and consume commands | | `registry` | `CommandRegistry` | - | Use an existing registry instance. If omitted, a new one is created. | ## Controls * **Enter**: Submit message (or accept suggestion) * **Tab**: Cycle through suggestions * **↑/↓**: Navigate suggestions or command history * **Escape**: Dismiss suggestions, or stop generation when streaming * **/**: Trigger slash command suggestions * **@**: Trigger file mention suggestions ## API Reference ### PromptInput | Prop | Type | Default | Description | | ---------------- | ------------------------------------------------------ | --------------------------------- | ----------------------------------------------------------------------- | | `value` | `string` | - | Controlled input value | | `defaultValue` | `string` | `""` | Default value for uncontrolled mode | | `onSubmit` | `(message: { text: string }) => void \| Promise` | - | Called on submit. Clears on resolve, preserves on reject. | | `onChange` | `(text: string) => void` | - | Called when input value changes | | `placeholder` | `string` | `"Type a message..."` | Placeholder text when empty | | `prompt` | `string` | `"❯ "` | Prompt character before input | | `promptColor` | `string` | `theme.muted` | Color of the prompt character | | `status` | `ChatStatus` | - | AI chat status. Drives disabled state and hint text. | | `onStop` | `() => void` | - | Called when Escape is pressed during streaming | | `onError` | `(error: unknown) => void` | - | Called when async `onSubmit` rejects | | `submittedText` | `string` | `"Thinking..."` | Hint text when status is `"submitted"` | | `streamingText` | `string` | `"Generating..."` | Hint text when status is `"streaming"` | | `errorText` | `string` | `"An error occurred. Try again."` | Text shown when status is `"error"` | | `disabled` | `boolean` | `false` | Disable input. Ignored when `status` is provided. | | `disabledText` | `string` | `"Generating..."` | Text shown when disabled. Ignored when `status` is provided. | | `commands` | `PromptInputCommand[]` | `[]` | Slash commands for autocomplete | | `skills` | `PromptInputCommand[]` | `[]` | Dynamically-provided skills merged into autocomplete alongside commands | | `files` | `string[]` | `[]` | File paths for `@` mention autocomplete | | `getSuggestions` | `(value: string) => Suggestion[]` | - | Custom suggestion provider — overrides commands/files | | `maxSuggestions` | `number` | `5` | Max visible suggestions | | `enableHistory` | `boolean` | `true` | Enable command history with ↑/↓ | | `model` | `string` | - | Model name displayed below the input | | `focusId` | `string` | auto-generated | Stable id for the focus system | | `autoFocus` | `boolean` | `false` | Focus this component on mount | | `showDividers` | `boolean` | `true` | Show horizontal dividers above and below input | | `dividerColor` | `string` | - | Override divider line color (e.g. for focus indicators) | | `dividerDashed` | `boolean` | - | Use dashed divider lines (`╌`) instead of solid (`─`) | | `children` | `ReactNode` | - | When provided, enables compound mode | ### PromptInputProvider | Prop | Type | Default | Description | | -------------- | ----------- | ------- | ----------------------------------------------- | | `initialInput` | `string` | `""` | Initial text input value | | `children` | `ReactNode` | - | Components that can access the provider context | ### PromptInputCommand | Field | Type | Description | | ----------- | ------------- | -------------------------------------------------------------------- | | `cmd` | `string` | Slash command string (e.g. `"/help"`) | | `desc` | `string?` | Description shown in autocomplete | | `group` | `string?` | Group name for categorization in autocomplete | | `onExecute` | `() => void?` | When provided, PromptInput calls this directly instead of `onSubmit` | | `hidden` | `boolean?` | Hide from autocomplete suggestions but still executable | ### Suggestion | Field | Type | Description | | --------- | --------- | -------------------------------------------------------------------------------------------------- | | `text` | `string` | Suggestion text | | `desc` | `string?` | Optional description | | `trigger` | `string?` | Character that triggered this suggestion (e.g. `"@"`, `"#"`). Used to determine replacement range. | ### ChatStatus | Value | Description | | ------------- | ----------------------------------------------------------- | | `"ready"` | Input enabled, accepts user input | | `"submitted"` | Input disabled, shows submitted text | | `"streaming"` | Input disabled, shows streaming text, Escape calls `onStop` | | `"error"` | Input enabled, shows error indicator | # SelectInput (/docs/components/select-input) A single-selection list with radio indicators, keyboard navigation, group headers, and a submitted state.
## Installation ```bash title="Terminal" bunx create-gridland add select-input ``` ## Usage ```tsx ``` ```tsx console.log("Selected:", value)} /> ``` `SelectInput` registers with the focus system via `focusId`. Wrap your app in `GridlandProvider` (which implicitly mounts ``) or a standalone `` for keyboard navigation to work. Tab to focus, Enter to select (start interacting), then use arrow keys. Escape deselects. ## Examples ### Controlled Use `value` and `onChange` to control the selection externally. ```tsx title="Controlled" const [value, setValue] = useState("ts") console.log("Selected:", value)} /> ``` ### Default Value Set the initially selected item in uncontrolled mode. ```tsx title="Default value" console.log("Selected:", value)} /> ``` ### Groups Use the `group` field on items to render group headers with separators. ```tsx title="With groups" console.log(value)} /> ``` ### Disabled Items Disable individual items so they cannot be selected. ```tsx title="Disabled items" console.log(value)} /> ``` ### Required Show a required indicator and use `invalid` to display an error state. ```tsx title="Required with validation" console.log(value)} /> ``` ### Disabled Disable the entire component. Navigation and submission are blocked. ```tsx title="Disabled" ``` ### Placeholder Show placeholder text when the items list is empty. ```tsx title="Placeholder" ``` ## Controls * **↑/↓** or **j/k**: Navigate and select * **Enter**: Submit selected item ## API Reference ### SelectInput | Prop | Type | Default | Description | | ----------------- | ---------------------- | --------------------------- | ------------------------------------------- | | `items` | `SelectInputItem[]` | `[]` | Array of selectable items | | `defaultValue` | `V` | - | Initially selected value (uncontrolled) | | `value` | `V` | - | Selected value (controlled) | | `onChange` | `(value: V) => void` | - | Called when selection changes | | `disabled` | `boolean` | `false` | Disable the entire component | | `invalid` | `boolean` | `false` | Show error state with destructive styling | | `errorMessage` | `string` | `"Please select an option"` | Custom error message when invalid | | `required` | `boolean` | `false` | Show required indicator (`*`) next to title | | `placeholder` | `string` | - | Placeholder text when items list is empty | | `title` | `string` | `"Select"` | Title shown next to the diamond indicator | | `submittedStatus` | `string` | `"submitted"` | Status text shown after submit | | `limit` | `number` | `12` | Max visible rows before scrolling | | `highlightColor` | `string` | `theme.primary` | Color of the highlighted item | | `radioColor` | `string` | `theme.muted` | Color of the radio indicator | | `onSubmit` | `(value: V) => void` | - | Called on Enter with the selected value | | `focusId` | `string` | auto-generated | Stable id for the focus system | | `autoFocus` | `boolean` | `false` | Focus this component on mount | ### SelectInputItem | Field | Type | Description | | ---------- | ---------- | ----------------------- | | `label` | `string` | Display text | | `value` | `V` | Item value | | `key` | `string?` | Optional React key | | `group` | `string?` | Group header label | | `disabled` | `boolean?` | Disable individual item | # SideNav (/docs/components/side-nav) A sidebar + main panel layout with built-in keyboard navigation. Arrow keys navigate the sidebar, Enter selects an item for interaction, Escape goes back.
## Installation ```bash title="Terminal" bunx create-gridland add side-nav ``` ## Usage ```tsx ``` ```tsx const items = [ { id: "inbox", name: "Inbox" }, { id: "drafts", name: "Drafts" }, { id: "sent", name: "Sent" }, ] {({ activeItem, isInteracting }) => ( )} ``` ## Examples ### Basic A simple sidebar navigating between static content panels. ```tsx title="Basic" const items = [ { id: "files", name: "Files" }, { id: "search", name: "Search" }, { id: "settings", name: "Settings" }, ] {({ activeItem }) => ( Viewing: {activeItem.name} )} ``` ### Interactive Content When the user presses Enter on a sidebar item, `SideNav` wraps the panel in a ``. Panel content just needs to use its own focus-aware components (`TextInput`, `SelectInput`, etc. with `focusId`) — the scope trap keeps focus contained until Esc exits. ```tsx title="Interactive" function MyPanel() { const [value, setValue] = useState("") return ( ) } {({ activeItem, isInteracting }) => ( )} ``` ### Custom Sidebar Width ```tsx title="Wide sidebar" {({ activeItem }) => } ``` ### Item Suffixes Use `suffix` on items to show additional context like counts or badges. ```tsx title="Suffixes" const items = [ { id: "inbox", name: "Inbox", suffix: "(3)" }, { id: "drafts", name: "Drafts", suffix: "(1)" }, { id: "sent", name: "Sent" }, ] {({ activeItem }) => } ``` ### Without Header ```tsx title="No header" {({ activeItem }) => } ``` ### Without Status Bar ```tsx title="No status bar" {({ activeItem }) => } ``` ### Programmatic Navigation Use `requestedActiveId` to switch the active item from outside the component. ```tsx title="Programmatic navigation" const [activeId, setActiveId] = useState() {({ activeItem }) => } ``` ## Controls | Key | Action | | --------- | ---------------------------------------------- | | `↑` / `↓` | Navigate sidebar items | | `Enter` | Select item — interact with main panel content | | `Escape` | Deselect — return to sidebar navigation | ## API Reference ### SideNav | Prop | Type | Default | Description | | -------------------- | ----------------------------- | ------- | ------------------------------------------------------- | | `items` | `SideNavItem[]` | - | List of navigable items | | `children` | `(ctx) => ReactNode` | - | Render function for main panel content | | `sidebarWidth` | `number` | `20` | Width of the sidebar in columns | | `title` | `string` | - | Optional title above the sidebar | | `showStatusBar` | `boolean` | `true` | Show the keyboard shortcuts status bar | | `showHeader` | `boolean` | `true` | Show the active item name as a header in the main panel | | `requestedActiveId` | `string` | - | Programmatically switch active item by ID | | `onActiveItemChange` | `(item: SideNavItem) => void` | - | Called when the active item changes | Colors are derived from the theme (`useTheme()`): focus indicators use `theme.focusSelected`/`focusFocused`/`focusIdle`, structural borders use `theme.borderMuted`, title and header use `theme.primary`, and idle text uses `theme.muted`. Customize by providing a different theme via `ThemeProvider`. ### SideNavItem | Field | Type | Description | | -------- | -------- | ------------------------------------------ | | `id` | `string` | Unique identifier | | `name` | `string` | Display label in the sidebar | | `suffix` | `string` | Optional text appended after the item name | ### Children Render Context | Field | Type | Description | | --------------- | ------------- | ---------------------------------------------- | | `activeItem` | `SideNavItem` | The currently focused/active item | | `isInteracting` | `boolean` | Whether the user has selected the item (Enter) | # Spinner (/docs/components/spinner) An animated loading indicator with 5 built-in animation styles selectable via the `variant` prop.
## Installation ```bash title="Terminal" bunx create-gridland add spinner ``` ## Usage ```tsx ``` ```tsx ```
## Examples ### Variants Use the `variant` prop to change the animation style. `dots` · `pulse` · `meter` · `bloom` · `ellipsis` ```tsx title="With variant" ```
### Custom Color Use the `color` prop to override the default theme color. ```tsx title="Custom color" ```
### Completion States Use the `status` prop to replace the spinner with a completion symbol. The animation stops and a semantic icon is shown with the appropriate theme color. ```tsx title="Completion states" ``` | Status | Symbol | Color | | --------- | ------ | --------------- | | `success` | ✔ | `theme.success` | | `error` | ✖ | `theme.error` | | `warning` | ⚠ | `theme.warning` | | `info` | ℹ | `theme.accent` | ## API Reference ### Spinner | Prop | Type | Default | Description | | --------- | ----------------------------------------------------------- | -------------- | ------------------------------------------------------------ | | `variant` | `"dots" \| "pulse" \| "meter" \| "bloom" \| "ellipsis"` | `"dots"` | Animation style | | `text` | `string` | - | Text displayed next to the spinner | | `color` | `string` | `theme.accent` | Foreground color of the spinner frame | | `status` | `"spinning" \| "success" \| "error" \| "warning" \| "info"` | `"spinning"` | Completion state — replaces the spinner with a status symbol | ### SpinnerPicker Interactive variant picker with keyboard navigation. Cycles through variants with ←→ keys. | Prop | Type | Default | Description | | ------------- | ----------------------------------------- | ------- | ------------------------------------ | | `useKeyboard` | `(handler: (event: any) => void) => void` | - | Keyboard hook from `@gridland/utils` | ### SpinnerShowcase Renders all 5 variants side by side with sample text. Takes no props. # StatusBar (/docs/components/status-bar) A horizontal bar that displays keybinding hints with key labels and descriptions. Commonly placed at the bottom of a view.
## Installation ```bash title="Terminal" bunx create-gridland add status-bar ``` ## Usage ```tsx ``` ```tsx ``` ## Examples ### Multiple Items ```tsx title="Multiple keybindings" ``` ### With Extra Content Render custom inline content to the left of the hints. A dim `│` separator appears between the extra content and the keybinding items. ```tsx title="With extra" const theme = useTheme() dots
} /> ``` ### Dynamic Items Build items based on component state. ```tsx title="Dynamic items" const items: StatusBarItem[] = [ { key: "↑↓", label: "navigate" }, { key: "Enter", label: "select" }, ] if (isEditing) { items.push({ key: "Esc", label: "cancel" }) } ``` ### With Theme Colors Style the extra content using theme tokens. ```tsx title="Themed extra" function MyStatusBar({ model }: { model: string }) { const theme = useTheme() return ( {model}
} /> ) } ``` ## API Reference ### StatusBar | Prop | Type | Default | Description | | ------- | ----------------- | ------- | ---------------------------------------------------------------------- | | `items` | `StatusBarItem[]` | - | Array of keybinding hints to display | | `extra` | `ReactNode` | - | Inline content rendered left of the hints (must be `` or string) | ### StatusBarItem | Field | Type | Description | | ------- | -------- | -------------------------------------------------- | | `key` | `string` | Key or key combination text (e.g. `"Tab"`, `"←→"`) | | `label` | `string` | Description of what the key does | # Table (/docs/components/table) A data table with automatic column detection, horizontal-only separators, and a compound sub-component API for full layout control.
## Installation ```bash title="Terminal" bunx create-gridland add table ``` ## Usage ```tsx ``` ```tsx ``` ## Examples ### Custom Columns Select and order which columns to display. ```tsx title="Custom columns" ``` ### Custom Colors Override the default header and separator colors. ```tsx title="Custom colors" ``` ### Compound API For full layout control, use `TableRoot` with sub-components. ```tsx title="Compound table" TableRoot, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption, } from "@/components/ui/table" A list of your recent invoices. Invoice Status Amount INV001 Paid $250.00 Total $250.00 ``` ### Cell Alignment Right-align or center-align individual cells using the `align` prop. ```tsx title="Right-aligned prices" Item Price Widget $10.00 Gadget $25.00 ``` ### Cell Color Override the text color for individual cells. ```tsx title="Colored status cells" Name Status Alice Active Bob Inactive ``` ### Column Span Use `colSpan` to span a cell across multiple columns. ```tsx title="Footer with colSpan" Item Qty Price Widget 2 $20.00 Total $20.00 ``` ## Compound Components | Component | Description | | -------------- | ------------------------------------------------------------------ | | `TableRoot` | Compound root — computes column widths and provides layout context | | `TableHeader` | Wraps header rows with a solid separator below | | `TableBody` | Wraps body rows with dimmed separators between them | | `TableFooter` | Wraps footer rows with a solid separator above | | `TableRow` | Reads `TableHead`/`TableCell` children and renders a styled row | | `TableHead` | Header cell — does not render on its own | | `TableCell` | Body cell — does not render on its own | | `TableCaption` | Muted caption text below the table | ## API Reference ### Table Data-driven table with automatic column detection. | Prop | Type | Default | Description | | ------------- | ------------- | ------------------ | ----------------------------------------------------- | | `data` | `T[]` | - | Array of data objects to display | | `columns` | `(keyof T)[]` | - | Keys to display as columns (auto-detected if omitted) | | `padding` | `number` | `1` | Cell padding in characters | | `headerColor` | `string` | `theme.foreground` | Header text color | | `borderColor` | `string` | `theme.muted` | Separator line color | ### TableRoot Compound root that computes column widths from the component tree. | Prop | Type | Default | Description | | ------------- | ----------- | ------------------ | -------------------------- | | `children` | `ReactNode` | - | Table sub-components | | `padding` | `number` | `1` | Cell padding in characters | | `headerColor` | `string` | `theme.foreground` | Header text color | | `borderColor` | `string` | `theme.muted` | Separator line color | ### TableHeader | Prop | Type | Default | Description | | ---------- | ----------- | ------- | --------------------------------- | | `children` | `ReactNode` | - | One or more `TableRow` components | ### TableBody | Prop | Type | Default | Description | | ---------- | ----------- | ------- | --------------------------------- | | `children` | `ReactNode` | - | One or more `TableRow` components | ### TableFooter | Prop | Type | Default | Description | | ---------- | ----------- | ------- | --------------------------------- | | `children` | `ReactNode` | - | One or more `TableRow` components | ### TableRow | Prop | Type | Default | Description | | ---------- | ----------- | ------- | ------------------------------------- | | `children` | `ReactNode` | - | `TableHead` or `TableCell` components | ### TableHead | Prop | Type | Default | Description | | ---------- | ------------------------------- | -------- | ---------------------------------------- | | `children` | `ReactNode` | - | Cell content (typically a string) | | `align` | `"left" \| "right" \| "center"` | `"left"` | Text alignment within the cell | | `color` | `string` | - | Override text color for this header cell | | `colSpan` | `number` | `1` | Number of columns this cell should span | ### TableCell | Prop | Type | Default | Description | | ---------- | ------------------------------- | -------- | --------------------------------------- | | `children` | `ReactNode` | - | Cell content (typically a string) | | `align` | `"left" \| "right" \| "center"` | `"left"` | Text alignment within the cell | | `color` | `string` | - | Override text color for this body cell | | `colSpan` | `number` | `1` | Number of columns this cell should span | ### TableCaption | Prop | Type | Default | Description | | ---------- | ----------- | ------- | ------------ | | `children` | `ReactNode` | - | Caption text | # Tabs (/docs/components/tabs) A compound tab component. Declare triggers and content panels side by side — the active panel renders automatically.
## Installation ```bash title="Terminal" bunx create-gridland add tab-bar ``` ## Usage ```tsx ``` ```tsx Files Search Git Browse project files Search across files View git status ``` `TabsList` is **focused-is-interactive**: pass `focusId` and arrow/h/l keys fire whenever that id is focused (no separate "Enter to select" step needed). Wrap your app in `GridlandProvider` or a `FocusProvider` for the focus system to route keys. ## Examples ### Full Example
### Simple API For cases where you don't need content panels, use the `TabBar` convenience wrapper. ```tsx title="Simple API" ``` ### With Label Show a text label before the triggers. ```tsx title="With label" Code Preview Source code Live preview ``` ### Controlled Use `value` and `onValueChange` to control the active tab externally. ```tsx title="Controlled" const [tab, setTab] = useState("files") Files Search Files panel Search panel ``` ### Unfocused Set `focused={false}` on `TabsList` to render in a dimmed, unfocused style. ```tsx title="Unfocused" Tab A Tab B ``` ### Disabled Tab Disable individual tabs to prevent keyboard navigation to them. ```tsx title="Disabled tab" Code Preview Settings Source code Settings panel ``` ### No Separator Hide the horizontal separator line below the tab bar. ```tsx title="No separator" Tab A Tab B ``` ## Compound Components | Component | Description | | ------------- | ------------------------------------------------------------------------------- | | `Tabs` | Root container with active tab state | | `TabsList` | Horizontal tab bar built from `TabsTrigger` children | | `TabsTrigger` | Declares a tab option. Does not render on its own — `TabsList` reads its props. | | `TabsContent` | Renders its children only when its value matches the active tab | | `TabBar` | Simple convenience wrapper (no content panels) | ## API Reference ### Tabs | Prop | Type | Default | Description | | --------------- | ------------------------- | ------- | ---------------------------------- | | `value` | `string` | - | Controlled active tab value | | `defaultValue` | `string` | `""` | Default active tab (uncontrolled) | | `onValueChange` | `(value: string) => void` | - | Called when the active tab changes | | `children` | `ReactNode` | - | Sub-components | ### TabsList | Prop | Type | Default | Description | | ------------- | ----------- | -------------- | ------------------------------------------------------------------------------------ | | `label` | `string` | - | Text label shown before the triggers | | `focused` | `boolean` | `true` | Whether the tab bar appears focused | | `activeColor` | `string` | `theme.accent` | Foreground color of the active trigger | | `separator` | `boolean` | `true` | Show horizontal separator below triggers | | `focusId` | `string` | auto-generated | Stable id for the focus system. Arrow/h/l navigation fires while this id is focused. | | `autoFocus` | `boolean` | `false` | Focus this tab bar on mount | | `children` | `ReactNode` | - | `TabsTrigger` components | ### TabsTrigger | Prop | Type | Default | Description | | ---------- | ----------- | ------- | ------------------------------------------------------------------------- | | `value` | `string` | - | Unique value linking this trigger to its `TabsContent` | | `disabled` | `boolean` | `false` | Disables the tab — skipped during keyboard navigation and rendered dimmed | | `children` | `ReactNode` | - | Label text displayed in the tab bar | ### TabsContent | Prop | Type | Description | | ---------- | ----------- | ----------------------------------------- | | `value` | `string` | Must match the active tab value to render | | `children` | `ReactNode` | Content shown when this tab is active | ### TabBar | Prop | Type | Default | Description | | --------------- | ------------------------- | -------------- | ---------------------------------------------------------- | | `label` | `string` | - | Text label shown before the options | | `options` | `string[]` | - | Array of option strings to display | | `selectedIndex` | `number` | - | Zero-based index of the selected option | | `focused` | `boolean` | `true` | Whether the tab bar appears focused | | `activeColor` | `string` | `theme.accent` | Foreground color of the selected option | | `separator` | `boolean` | `true` | Show horizontal separator below tabs | | `focusId` | `string` | auto-generated | Stable id for the focus system | | `autoFocus` | `boolean` | `false` | Focus this tab bar on mount | | `onValueChange` | `(index: number) => void` | - | Called when the active tab changes via keyboard navigation | ## Controls | Key | Action | | --------- | --------------------------- | | `←` / `h` | Previous tab (wraps around) | | `→` / `l` | Next tab (wraps around) | # TerminalWindow (/docs/components/terminal-window) A decorative container that mimics a terminal window with traffic light buttons and an optional centered title. Defaults to a dark gray background.
## Installation ```bash title="Terminal" bunx create-gridland add terminal-window ``` ## Usage ```tsx ``` ```tsx
Window content here
``` ## Examples ### With Title Display a centered title in the title bar. ```tsx title="With title"
Terminal content
``` ### Transparent Background Use `transparent` to remove the default dark gray background and inherit from the parent. ```tsx title="Transparent"
Content on transparent background
``` ### Window Controls Pass `onClose`, `onMinimize`, and `onMaximize` callbacks to make the traffic light buttons interactive. ```tsx title="With callbacks" console.log("close")} onMinimize={() => console.log("minimize")} onMaximize={() => console.log("maximize")} >
Window content
``` ### Min Width Set a minimum width for the window container. ```tsx title="Min width"
Content
``` ## API Reference ### TerminalWindow | Prop | Type | Default | Description | | ------------- | ------------------ | ------- | ------------------------------- | | `children` | `ReactNode` | - | Content below the title bar | | `className` | `string` | - | Additional CSS classes | | `title` | `string` | - | Centered title bar text | | `minWidth` | `number \| string` | - | Minimum width of the window | | `transparent` | `boolean` | `false` | Remove the dark gray background | | `onClose` | `() => void` | - | Close button callback | | `onMinimize` | `() => void` | - | Minimize button callback | | `onMaximize` | `() => void` | - | Maximize button callback | # TextInput (/docs/components/text-input) A single-line text input field with label, prompt prefix, placeholder, character count, and validation states.
## Installation ```bash title="Terminal" bunx create-gridland add text-input ``` ## Usage ```tsx ``` ```tsx const [name, setName] = useState("") ``` `TextInput` registers with the focus system via `focusId`. Wrap your app in `GridlandProvider` (which mounts `` implicitly) for keyboard routing. Tab to focus the field, Enter to select (start editing), then type. Escape exits edit mode. ## Examples ### Form Multiple inputs composed into a form with keyboard navigation.
### With onSubmit Use `onSubmit` to handle Enter key presses. ```tsx title="With onSubmit" const [email, setEmail] = useState("") console.log("Submitted:", v)} placeholder="user@example.com" prompt="> " /> ``` ### Required Show a required indicator (`*`) next to the label. ```tsx title="Required" const [name, setName] = useState("") ``` ### Description Display helper text inline next to the label. ```tsx title="With description" const [email, setEmail] = useState("") ``` ### Error Pass an `error` string to show a validation message. It replaces the description. ```tsx title="With error" const [email, setEmail] = useState("") ``` ### Max Length Limit input length and show a character counter when the user starts typing. ```tsx title="Max length" const [bio, setBio] = useState("") ``` ### Disabled A disabled input ignores focus and keystrokes. ```tsx title="Disabled" ``` ### Form with keyboard navigation TextInput registers with the focus system via `focusId`, so composing multiple inputs into a form is just a matter of giving each one its own id. Tab and Shift-Tab cycle between them automatically — no external keyboard wiring needed. ```tsx title="Form" const FIELDS = [ { id: "username", label: "Username" }, { id: "email", label: "Email" }, { id: "password", label: "Password" }, ] function MyForm() { const [values, setValues] = useState(FIELDS.map(() => "")) return ( <> {FIELDS.map((field, i) => ( setValues((prev) => prev.map((old, j) => j === i ? v : old))} prompt="> " /> ))} ) } ``` ## Controls * **Tab / Shift-Tab**: Move between fields * **Enter**: Start editing the focused field (and submit on re-press) * **Escape**: Exit edit mode ## API Reference ### TextInput | Prop | Type | Default | Description | | ------------- | ------------------------- | -------------- | ------------------------------------------------------ | | `label` | `string` | - | Field label shown above the input | | `description` | `string` | - | Helper text shown inline next to the label | | `error` | `string` | - | Error message — overrides description when set | | `required` | `boolean` | `false` | Show required indicator (`*`) on the label | | `disabled` | `boolean` | `false` | Disable the input | | `value` | `string` | *required* | Current value of the input | | `onChange` | `(value: string) => void` | - | Called on every keystroke | | `onSubmit` | `(value: string) => void` | - | Called when Enter is pressed | | `placeholder` | `string` | - | Hint text shown when the input is empty | | `prompt` | `string` | - | Prompt string before the input (e.g. `"> "`) | | `focusId` | `string` | auto-generated | Stable id for the focus system | | `autoFocus` | `boolean` | `false` | Focus this input on mount | | `maxLength` | `number` | - | Maximum characters allowed (shows counter when typing) | # Cells and Layout (/docs/core-concepts/cells-and-layout) The single biggest mental-model shift when coming to Gridland from the web is that **everything is measured in character cells**, not pixels. If you remember one thing from this page, remember that. In a normal React app, `
` means 300 pixels wide. In Gridland, `` means 300 character columns wide which, on a typical 80-column terminal, is almost four screen-widths and your content will overflow. This page explains what a cell is, why the framework works this way, and how to size and lay out components without fighting the grid. ## What is a cell? A cell is one character position in the terminal font. It has a width and a height, both determined by the monospace font you render with. Every character of text occupies exactly one cell. Every `` occupies a rectangular region of cells. Every border, every padding unit, every flex gap is expressed as an integer number of cells. There are no half-cells, no subpixel positioning, no DPI scaling math. The grid is discrete. A cell is to Gridland what a pixel is to the DOM the smallest unit you can position, size, or paint. The difference is that cells are much larger (a whole character) and align to a fixed grid. Gridland defaults to JetBrains Mono at 14px in the browser runtime, but you can override both via the `fontFamily` and `fontSize` props on the [`TUI` component](/docs/core-concepts/rendering#tui-props). In a real terminal, the terminal emulator decides the font. ## Why cells and not pixels? Three reasons: 1. **It matches the terminal.** A terminal has no concept of pixels. When you ship the same component tree to an actual terminal via [`@gridland/bun`](/docs/api/gridland-bun), everything must be in cells because that's the only unit the terminal understands. Using cells in the browser runtime too keeps the code identical across both targets. 2. **Integer-aligned layout is simpler.** No half-pixel rounding bugs, no subpixel antialiasing artifacts, no "why is this one pixel off" debugging sessions. A box either starts at column 5 or column 6 never column 5.5. 3. **It matches the aesthetic.** Gridland is for terminal-style UIs. Those UIs were designed for character grids. Writing them against a character grid keeps the design language honest. ## Sizing a `` The `width` and `height` props on `` accept two kinds of values: * **Numbers** integer cell counts. `width={40}` is 40 columns wide. * **Percentage strings** fractions of the parent. `width="50%"` is half of whatever the parent is. ```tsx title="Fixed width, half height" 40 columns wide, half the parent's height ``` Use percentages for anything that should adapt to the viewport the root of your app, full-width rows, flexible columns. Use fixed cell counts for things that should be a specific character-column size fixed-width sidebars, columns in a table, status bars with an exact row count. ## Padding, margin, and gap All three are integer cell counts. There are no `em`, `rem`, or `px` units just numbers. ```tsx title="A padded, gapped column layout" First row Second row 1 cell gap above Third row 1 cell gap above ``` `padding={2}` adds 2 cells of space on every side inside the border. `gap={1}` adds 1 cell of space between each child. `margin` works the same way but on the outside of the box. Borders count as part of the box. A `` has 10 cells of total width, including 1 cell of border on each side leaving 8 cells of content area. This is analogous to CSS `box-sizing: border-box`. ## Flexbox, in cell units Layout in Gridland uses [Yoga](https://www.yogalayout.dev/), Facebook's C++ Flexbox engine the same one React Native uses. The API is the standard Flexbox model you already know: `flexDirection`, `flexGrow`, `flexShrink`, `justifyContent`, `alignItems`. The only difference from CSS Flexbox is that every measurement is in cells. ```tsx title="A split-pane layout, 30/70" Left pane 30% of the width Right pane 70% of the width ``` Flex ratios, justification, and alignment all behave the way you'd expect. If you know Flexbox for the web, you know layout in Gridland. ## "Why doesn't `width: 300` work?" It does but it probably doesn't mean what you think. Coming from the web, `width: 300` reflexively reads as "300 pixels." In Gridland, `width={300}` means **300 character columns**. On a typical 80-column terminal, that's nearly four screen-widths, so your content will be clipped or will cause the parent to overflow. **Rule of thumb:** if you want a box that fills the screen, use a percentage (`width="100%"`). If you want a specific character count (a fixed-width sidebar, exactly 30 columns), use a small integer. If you find yourself reaching for three-digit cell counts, you almost certainly want a percentage or a `flexGrow` instead. There is no `px` unit. Numbers are always cells. If you're porting a component from a web layout, halve and halve again most pixel values become single-digit cell counts. ## Common patterns ### Centered box ```tsx title="Centered 40 × 10 box inside a fullscreen parent" Centered, 40 × 10 cells ``` ### Sidebar + flexible main area ```tsx title="Fixed 30-column sidebar, main area fills the rest" Sidebar exactly 30 columns Main area fills the rest ``` ### Header, body, footer ```tsx title="Fullscreen app shell" Header (3 rows) Body fills the remaining height Footer (1 row) ``` ## Next steps * [Intrinsic Elements](/docs/core-concepts/intrinsic-elements) the full list of built-in tags (``, ``, ``, etc.) with prop tables * [Rendering](/docs/core-concepts/rendering) how the cell grid becomes canvas pixels or terminal stdout * [Theming → Breakpoints](/docs/theming/breakpoints) adapt layouts to the viewport's cell dimensions # Intrinsic Elements (/docs/core-concepts/intrinsic-elements) In a normal React app, JSX tags like `
`, ``, and ` ``` | Prop | Type | Default | Description | | ------------------ | --------- | ------- | --------------------------------------------------------- | | `trap` | `boolean` | `false` | Prevent Tab from leaving the scope | | `selectable` | `boolean` | `false` | Enable Enter/Esc selection within this scope | | `autoFocus` | `boolean` | `false` | Focus first element on mount | | `autoSelect` | `boolean` | `false` | Auto-select if only one focusable element exists on mount | | `restoreOnUnmount` | `boolean` | `true` | Restore previous focus on unmount | ## Related * [Pointer Events](/docs/interaction/pointer-events) mouse handlers, event shape, hit testing, and propagation. * [Cursor Highlight](/docs/interaction/cursor-highlight) highlight the terminal cell under the mouse cursor. # Pointer Events (/docs/interaction/pointer-events) All intrinsic elements (``, ``, ``, etc.) accept pointer event handlers as props. Handlers receive a single event object with **cell coordinates** (not pixels) and modifier flags. This page is the full reference for the event shape, the handler set, hit testing, and propagation semantics.
## Supported Handlers Gridland's base `Renderable` exposes eleven pointer-event props. The handler dispatched depends on the DOM event the browser renderer observes. | Prop | Browser dispatch | Fires when | | ---------------- | ------------------------ | --------------------------------------------------------- | | `onMouseDown` | `mousedown` | Any button pressed over the element | | `onMouseUp` | `mouseup` | Any button released over the element | | `onClick` | synthesized on `mouseup` | Left-button press + release on the same element | | `onMouseMove` | `mousemove` | Pointer moves while over the element | | `onMouseOver` | `mousemove` (entering) | Pointer enters the element | | `onMouseOut` | `mousemove` (leaving) | Pointer leaves the element | | `onMouseScroll` | `wheel` | Scroll wheel scrolled over the element | | `onMouse` | all of the above | Catch-all that fires before the typed handler | | `onMouseDrag` | terminal only | Drag gesture (not dispatched by `@gridland/web`) | | `onMouseDragEnd` | terminal only | Drag released (not dispatched by `@gridland/web`) | | `onMouseDrop` | terminal only | Drop onto the element (not dispatched by `@gridland/web`) | The three `drag*` / `drop` handlers exist on the renderable type because terminal environments dispatch them, but `@gridland/web` does not currently synthesize them from DOM drag events. Code that sets them in a browser app compiles but never fires. ## Event Payload The event object is identical across handlers `type` discriminates which kind it is. ```ts interface PointerEvent { type: "down" | "up" | "move" | "over" | "out" | "scroll" button: number // 0 = left, 1 = middle, 2 = right x: number // cell column (NOT pixels) y: number // cell row (NOT pixels) target: Renderable modifiers: { shift: boolean; alt: boolean; ctrl: boolean } // Only populated when type === "scroll": scroll?: { direction: "up" | "down" | "left" | "right"; delta: number } // Propagation state getters, mutate only via the methods below: readonly propagationStopped: boolean readonly defaultPrevented: boolean stopPropagation(): void preventDefault(): void } ``` **React devs read this.** Several fields that you reach for by reflex do **not** exist: no `timestamp`, `nativeEvent`, `clientX`, `clientY`, `pageX`, or `pageY`. `x` / `y` are cell coordinates (columns and rows), not pixels `pixelToCell` has already translated them. `modifiers.alt` is the OS Alt/Option key (separate from `modifiers.ctrl`). `propagationStopped` and `defaultPrevented` are read-only getters; assigning them directly silently fails. ### Scroll payload `onMouseScroll` is the only handler where `event.scroll` is populated. The raw `WheelEvent.deltaX` / `deltaY` are reduced to a single `direction` (the dominant axis) and a positive integer `delta` (`Math.max(1, Math.abs(Math.round(raw / 40)))`). The DOM-style `deltaX` / `deltaY` fields are thrown away. ```tsx { if (e.scroll?.direction === "down") scrollDown(e.scroll.delta) if (e.scroll?.direction === "up") scrollUp(e.scroll.delta) }} > {items.map((item) => {item.label})} ``` ### `onClick` quirk `onClick` is synthesized when a left-button `mousedown` and the matching `mouseup` land on the same renderable. The synthesizer reuses the `mousedown` event object, so the handler receives `event.type === "down"` **not** `"click"`. Narrowing on `event.type === "click"` inside an `onClick` handler will never match. ```tsx { // e.type is "down", not "click" this is expected. console.log("clicked at cell", e.x, e.y) }}> Click me ``` ## Hover-Steals-Focus Pattern Combine `onMouseOver` with `useInteractive().focus()` to make hovering an element claim keyboard focus. This is the canonical way to make a multi-modal UI (keyboard + mouse) where hovering "previews" the focused item. ```tsx function Item({ id }: { id: string }) { const { focus, focusRef } = useInteractive({ id }) return ( focus()} border> {id} ) } ``` ## Hit Testing The browser renderer paints into a **hit grid** during the render pass, with each entry carrying the renderable's cell rectangle (clipped by any active `overflow="hidden"` scissor). On every mouse event, the hit-tester walks the grid **in reverse insertion order** and returns the first entry whose rect contains the cursor cell. Because entries are pushed in draw order, the last-drawn (topmost) renderable at a given cell wins. **What this means in practice:** * Children are drawn after their parent, so children naturally win over their own parent's empty regions. * Siblings are resolved by draw order. `zIndex` changes draw order, so raising a sibling's `zIndex` makes it win against overlapping peers. * Cells outside any registered rect return `null` the event is swallowed without dispatch. * Scissor rects (from `overflow="hidden"`) clip the hit grid entries at insertion time, so a child that overflows its parent's `overflow="hidden"` bounds is **not** hittable in the clipped region. ## Propagation When `processMouseEvent` fires on the hit renderable, it invokes the renderable's handlers in this order: `onMouse` (catch-all) → the typed handler (`onMouseDown`, `onMouseOver`, etc.) → the renderable's internal `onMouseEvent` hook → **then bubbles up to the parent**. The parent chain continues until either the root renderable is reached or `event.stopPropagation()` was called. ```tsx // Child consumes the click; parent's onClick never fires. { /* parent handler */ }}> { e.stopPropagation() }}> Click me ``` `preventDefault()` sets a flag on the event but does not affect bubbling. It is provided for framework-internal hooks that consult it (for example, `packages/core/src/renderer.ts:1187` suppresses auto-focus on a left-button `down` event when `defaultPrevented` is true). `onClick` is synthesized separately and does **not** bubble the click synthesizer calls `_clickHandler` directly on the hit renderable only, without walking the parent chain. ## Cursor Style The renderer automatically sets the canvas cursor to `"pointer"` when the pointer is over any renderable whose ancestor chain contains an `onClick` or `onMouseDown` handler no extra markup needed. Link regions (rendered via ``) also trigger the pointer cursor. ## Related * [Focus](/docs/interaction/focus) how keyboard focus interacts with pointer events. * [TUI Primitives](/docs/core-concepts/intrinsic-elements) the intrinsic elements that accept these handler props. # Breakpoints (/docs/theming/breakpoints) ## Overview Gridland apps run in terminals of varying widths, from narrow mobile screens to wide desktop terminals. The `useBreakpoints` hook provides reactive boolean flags for common width thresholds, making it easy to adapt your layout. ## Usage ```tsx function MyApp() { const { isMobile, isNarrow, isTiny, isDesktop, width, height } = useBreakpoints() return ( {isDesktop ? ( ) : ( )} ) } ``` ## Breakpoint Values | Flag | Condition | Use case | | ----------- | ------------- | ------------------------------------------------------------ | | `isTiny` | `width < 40` | Extremely narrow. Hide decorative elements, use compact text | | `isNarrow` | `width < 60` | Narrow. Stack layouts vertically, use abbreviated labels | | `isMobile` | `width < 70` | Mobile. Simplify layouts, merge text blocks | | `isDesktop` | `width >= 70` | Desktop. Full layouts with sidebars and multi-column content | The raw `width` and `height` values are also returned for custom thresholds. ## Constants The breakpoint thresholds are exported as `BREAKPOINTS` for use outside of React components: ```tsx // BREAKPOINTS.tiny = 40 // BREAKPOINTS.narrow = 60 // BREAKPOINTS.mobile = 70 ``` ## API ### `useBreakpoints()` Returns a `Breakpoints` object: ```ts interface Breakpoints { isTiny: boolean isNarrow: boolean isMobile: boolean isDesktop: boolean width: number height: number } ``` ## Examples ### Responsive text ```tsx function Subtitle() { const { isMobile } = useBreakpoints() return ( {isMobile ? "Short description for mobile." : "A longer, more detailed description for desktop terminals."} ) } ``` ### Responsive layout direction ```tsx function ActionBar() { const { isNarrow } = useBreakpoints() return ( ) } ``` # Colors (/docs/theming/colors) Gridland UI uses semantic color tokens to theme all components. A `Theme` is a flat object of 17 hex values covering brand, content, borders, status, focus indicators, and message bubbles. Without a `ThemeProvider`, all components fall back to the built-in dark theme. ## Quick Start ```tsx title="app.tsx" function App() { return ( ) } ``` ## Tokens ### Brand | Token | Purpose | | ----------- | ------------------------------------------------------------ | | `primary` | Main brand color headings, highlights, active elements | | `accent` | Secondary brand color interactive highlights, focused states | | `secondary` | Tertiary color user messages, checkboxes, prompts | ### Content | Token | Purpose | | ------------- | ----------------------------------------------------- | | `foreground` | Default foreground text color | | `background` | App background color | | `muted` | Subdued color disabled states, secondary text, cursor | | `placeholder` | Placeholder text in inputs | ### Borders | Token | Purpose | | ------------- | --------------------------------------------- | | `border` | Borders and dividers | | `borderMuted` | Muted border color subtle structural dividers | ### Status | Token | Purpose | | --------- | ------------------- | | `success` | Success state color | | `error` | Error state color | | `warning` | Warning state color | ### Focus Three-tier focus system (see [Focus](/docs/interaction/focus) for the full model): | Token | Purpose | | --------------- | ------------------------------------------------------------ | | `focusSelected` | Bright focus component is selected (entered for interaction) | | `focusFocused` | Medium focus component has keyboard focus | | `focusIdle` | Dimmed focus idle hint that the component is selectable | ### Messages | Token | Purpose | | ------------------ | ---------------------------------------------- | | `messageAssistant` | Background color for assistant message bubbles | | `messageUser` | Background color for user message bubbles | ## Built-in Themes ```tsx ``` ### Dark
### Light
## Custom Themes ```tsx title="my-theme.ts" export const myTheme: Theme = { // Brand primary: "#61afef", accent: "#c678dd", secondary: "#98c379", // Content foreground: "#abb2bf", background: "#282c34", muted: "#5c6370", placeholder: "#4b5263", // Borders border: "#5c6370", borderMuted: "#3e4451", // Status success: "#98c379", error: "#e06c75", warning: "#e5c07b", // Focus (bright → dim) focusSelected: "#61afef", focusFocused: "#4b8dc9", focusIdle: "#2c3e50", // Message bubbles messageAssistant: "#2c313a", messageUser: "#3a3f4b", } ``` Or extend a built-in theme: ```tsx title="custom-theme.ts" export const customTheme = { ...darkTheme, primary: "#61afef", } ``` ## useTheme Access tokens in your own components: ```tsx function MyComponent() { const theme = useTheme() return Themed text } ``` Explicit color props always override theme values: ```tsx {/* uses theme.accent */} {/* overrides theme */} ``` # Text Style (/docs/theming/text-style) Terminal elements accept `fg`, `bg`, and a numeric `attributes` bitmask in their `style` prop. The `textStyle()` helper lets you use boolean flags instead.
## Usage ```tsx Bold pink ``` Combine with `useTheme()` for themed styles: ```tsx function MyComponent() { const theme = useTheme() return Themed text } ``` ## Direct Text Styling Apply `textStyle()` directly on the `` element when the entire line shares one style. No `` wrappers needed. This is the simplest way to style text.
```tsx Single style applied to the entire text element ``` ## Mixed Span Styling Use `` elements inside `` when different parts of the same line need different styles. Each `` can have its own color, weight, and decoration, but they must always be wrapped in a parent `` element.
```tsx Server running on port 3000 ``` ## Options | Option | Type | Description | | ----------- | --------- | -------------------------------- | | `fg` | `string` | Foreground (text) color | | `bg` | `string` | Background color | | `bold` | `boolean` | Bold text | | `dim` | `boolean` | Dimmed text (reduced brightness) | | `italic` | `boolean` | Italic text | | `underline` | `boolean` | Underlined text | | `inverse` | `boolean` | Swap foreground and background | Terminals have three weight levels: `dim`, normal, and `bold`. There is no CSS font-weight scale.