# Component Reference

## Summary

- **Layout**: `container` (flexbox, scrollable), `tabs`/`tab`, `split-pane`, `dialog` (modal, draggable), `separator`
- **Text & Input**: `text`, `input`, `textarea`, `slider`, `checkbox`, `radio`, `button`
- **Declarative**: `command` (keyboard shortcut binding — non-visual, focus-scoped or global)
- **Data**: `data-table` (virtual-scrolled rows), `table` (HTML-style), `data-tree` (expandable hierarchy), `data-bars` (bar charts), `data-heatmap` (heatmap grid), `data-boxplot` (box-and-whisker plots)
- **Dropdowns**: `combobox` (type-to-filter), `select` (picker), `autocomplete` (async search), `command-palette` (Ctrl+K, draggable)
- **Graphics**: `canvas` (pixel drawing, shaders), `img` (PNG/JPEG/GIF/WebP), `video` (FFmpeg playback)
- **Visualization**: `progress`, `spinner`, `segment-display` (LCD digits), `graph` (node/edge diagrams), `connector` (graph edges)
- **Navigation**: `file-browser` (directory tree with keyboard nav)
- All components use `createElement(type, props, ...children)` — registered types get their class, others become `BasicElement`
- Scrollable containers are focusable and appear in tab order
- Dropdowns render as overlays to avoid parent clipping

---

Detailed documentation for Melker components.

## Element System

### Core Types (`src/types.ts`)

```
Element (abstract base class)
├── type: string         - Component type name
├── props: Record        - Component properties
├── children?: Element[] - Child elements
├── id: string           - Unique identifier (auto-generated if not provided)
├── _bounds: Bounds|null - Layout bounds (set during render)
├── getBounds()          - Get element's layout bounds after render
└── setBounds(bounds)    - Set element's layout bounds (called by renderer)
```

**Component Interfaces:**
- `Renderable` - Has `render()` and `intrinsicSize()` methods
- `Focusable` - Can receive keyboard focus
- `Clickable` - Handles click events with `onClick()`
- `Interactive` - Handles keyboard events with `handleKeyPress()`
- `TextSelectable` - Supports text selection (copy with Alt+C/Alt+N, requires `clipboard: true` in policy, shows toast on success/failure)
- `Draggable` - Handles mouse drag (scrollbars, resizers, dialogs). Optional `handleDragHover(x, y): boolean` for hover feedback on drag zones
- `Wheelable` - Handles mouse wheel events (scrollable tbody)

**Type Guards:**
- `isRenderable(el)`, `isFocusable(el)`, `isClickable(el)`, etc.
- `isScrollableType(type)` - Returns true for 'container' or 'tbody'

### Element Creation (`src/element.ts`)

```typescript
// Create elements via createElement()
const button = createElement('button', { label: 'Click' });
const container = createElement('container', { style: { display: 'flex' } }, button);
```

**Key functions:**
- `createElement(type, props, ...children)` - Create element (uses component registry)
- `registerComponent(definition)` - Register custom component class
- `findElementById(root, id)` - Find element in tree
- `cloneElement(element, newProps)` - Clone with merged props
- `traverseElements(root, callback)` - Walk element tree

### Component Registry

Components are registered with `registerComponent()`. Registered components use their class constructor; unregistered types become `BasicElement`.

See `src/components/*.ts` for component implementations.

### Component Constructor Timing

Component constructors run during template parsing, **before** `globalThis.melkerEngine` is set (the engine is created after parsing). This means `engine?.document` is `null` in constructors. For operations that need the engine (e.g. stylesheet registration), use lazy initialization: store a pending value and register it from `intrinsicSize()` or `render()` when the engine exists.

### Scrolling

Scrollable containers support axis-specific overflow control:

| Property     | Values                                    | Description                          |
|--------------|-------------------------------------------|--------------------------------------|
| `overflow`   | `visible`, `hidden`, `scroll`, `auto`     | Shorthand for both axes              |
| `overflow-x` | `visible`, `hidden`, `scroll`, `auto`     | Horizontal overflow (overrides shorthand) |
| `overflow-y` | `visible`, `hidden`, `scroll`, `auto`     | Vertical overflow (overrides shorthand)   |

Axis-specific properties override the shorthand. For example, `overflow: scroll; overflow-x: hidden` scrolls vertically but clips horizontally.

Only `container` and `tbody` elements support scrolling. Scrollbars are rendered per-axis: vertical on the right edge, horizontal on the bottom edge. Arrow keys, mouse wheel, and scrollbar drag all respect per-axis settings.

**Scrollable containers are focusable.** When `overflow: scroll` (or `overflow-x`/`overflow-y: scroll`) is set, the container appears in tab order and can be targeted by geometric arrow navigation. When focused, the scrollbar gutter highlights with the theme's `focusBorder` color. Arrow keys scroll the container; Shift+Arrow bypasses scroll for geometric navigation; Tab moves to the next focusable element.

See [`examples/basics/overflow.melker`](../examples/basics/overflow.melker) for a working demo.

### Focus Navigation

Arrow keys move focus to the nearest focusable element in the pressed direction (geometric navigation). This activates automatically for elements that don't handle their own arrow keys (buttons, checkboxes, radios). Components with internal arrow key handling (data-table, input, slider, etc.) are unaffected.

| Property    | Values             | Description                                               |
|-------------|--------------------|-----------------------------------------------------------|
| `arrow-nav` | `geometric`, `none` | Controls geometric focus navigation for descendant elements |

`arrow-nav: none` on a container disables geometric navigation for all focusable elements inside it. Arrow keys that aren't handled by components or scroll containers do nothing.

Shift+Arrow bypasses scroll containers and goes directly to geometric navigation, allowing users to escape scrollable regions without scrolling to the boundary.

See [keyboard-focus-navigation-architecture.md](keyboard-focus-navigation-architecture.md) for full algorithm details.

### ARIA Attributes

Elements support ARIA attributes as regular props (via `BaseProps extends Record<string, any>`). They are consumed exclusively by the AI context builder in `src/ai/context.ts` — they have no effect on rendering, layout, or keyboard navigation.

**Supported attributes:**

| Attribute          | Type    | Consumed by                                      |
|--------------------|---------|--------------------------------------------------|
| `role`             | string  | Replaces element type in AI output               |
| `aria-label`       | string  | Accessible name (overrides `title`/`placeholder`) |
| `aria-labelledby`  | string  | Space-separated IDs resolved via `document.getElementById()` — highest naming priority |
| `aria-hidden`      | boolean | Excludes element and subtree from AI context     |
| `aria-description` | string  | Supplementary text appended to element output    |
| `aria-expanded`    | boolean | Shows `expanded`/`collapsed` state               |
| `aria-controls`    | string  | Shows `controls: element-id` relationship        |
| `aria-busy`        | boolean | Shows `loading` indicator                        |
| `aria-required`    | boolean | Shows `required` on inputs/textareas/checkboxes  |
| `aria-invalid`     | boolean | Shows `invalid` on inputs/textareas              |

**Naming priority chain:** `aria-labelledby` > `aria-label` > native label (`title`, `placeholder`, `label`)

**Implementation:** All ARIA logic is in `src/ai/context.ts`. Three helper functions handle resolution:
- `isAriaTrue(value)` — normalizes boolean/string ARIA attribute checks
- `getAccessibleText(el)` — extracts text from a referenced element
- `resolveAriaLabelledBy(el, document)` — resolves space-separated ID references to concatenated text

These are used across `buildScreenContent()`, `buildElementTree()`, and `describeFocusedElement()`.

## Document Model (`src/document.ts`)

The `Document` class manages runtime state:

```typescript
const doc = new Document(rootElement);
doc.getElementById('myButton');    // Lookup by ID
doc.getElementsByType('button');   // Find all of type
doc.focus('inputId');              // Focus element
doc.focusedElement;                // Current focus
```

**Features:**
- Element registry (id → Element map)
- Focus tracking
- Event listener management

## Layout Engine (`src/layout.ts`)

### Layout Process

1. **Style computation** - Merge element style with parent inheritance
2. **Layout props** - Extract layout-related properties
3. **Bounds calculation** - Determine position and size
4. **Box model** - Calculate content/padding/border/margin
5. **Children layout** - Recursively layout children (flex or block)

### LayoutNode Structure

```
LayoutNode
├── element: Element
├── bounds: { x, y, width, height }      - Element position/size
├── contentBounds: Bounds                 - Inner content area
├── visible: boolean
├── children: LayoutNode[]
├── computedStyle: Style
├── layoutProps: AdvancedLayoutProps
├── boxModel: BoxModel
└── zIndex: number
```

### Flexbox Layout

The root viewport, `container`, `dialog`, and `tab` elements all default to `display: flex` with `flexDirection: column`.

**Container properties:**
- `display`: `'flex'` | `'block'` | `'none'`
- `flexDirection`: `'row'` | `'column'` | `'row-reverse'` | `'column-reverse'`
- `flexWrap`: `'nowrap'` | `'wrap'` | `'wrap-reverse'`
- `justifyContent`: `'flex-start'` | `'center'` | `'flex-end'` | `'space-between'` | `'space-around'`
- `alignItems`: `'stretch'` | `'flex-start'` | `'center'` | `'flex-end'`

**Item properties:**
- `flex`: Shorthand (`'1'`, `'1 1 auto'`, `'0 0 auto'`)
- `flexGrow`, `flexShrink`, `flexBasis`: Individual flex properties
- `alignSelf`: Override parent's alignItems

**Positioning:**
- `position`: `'static'` (default) | `'relative'` | `'absolute'` | `'fixed'`
- `top`, `right`, `bottom`, `left`: Numeric offset (cells)
- `zIndex`: Stacking order

`position: relative` offsets the element visually without affecting sibling layout. `top`/`left` win over `bottom`/`right` when both are set. These properties are animatable via CSS `@keyframes`.

**Container queries:**
- `containerType`: `'inline-size'` (width queries) | `'size'` (width + height) | `'normal'` (default)

Set on a container to enable `@container` rules for its descendants. See [container-query-architecture.md](container-query-architecture.md).

### Flex Layout Gotchas

**1. `display: 'flex'` is auto-inferred** *(Build 142+)*

When flex container properties are present (`flexDirection`, `justifyContent`, `alignItems`, `alignContent`, `flexWrap`, `gap`), `display: 'flex'` is automatically inferred:

```typescript
// Both work - display: flex is auto-inferred from flexDirection
{ flexDirection: 'column', width: 'fill', height: 'fill' }
{ display: 'flex', flexDirection: 'column', width: 'fill', height: 'fill' }
```

Note: Flex *item* properties (`flex`, `flexGrow`, `flexShrink`, `flexBasis`) don't trigger auto-inference.

**2. `flexDirection: 'row'` must be explicit for horizontal layouts**

The layout code checks `flexDirection === 'row'` explicitly. If `flexDirection` is undefined, it's treated as column layout:

```typescript
// In layout.ts:
const isRow = flexProps.flexDirection === 'row' || flexProps.flexDirection === 'row-reverse';
// undefined !== 'row', so isRow = false
```

For `justifyContent: 'flex-end'` to align items horizontally (right), you need explicit row direction:

```typescript
// WRONG - justifyContent works vertically (column is assumed)
{ display: 'flex', justifyContent: 'flex-end' }

// CORRECT - justifyContent works horizontally
{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-end' }
```

**3. Container needs width for horizontal justification**

For `justifyContent` to work, the container needs space to distribute. Add `width: 'fill'`:

```typescript
// Right-aligned button in a footer
{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-end', width: 'fill' }
```

**4. Complete example: Footer with right-aligned button**

```typescript
const footer = createElement('container', {
  style: {
    display: 'flex',
    flexDirection: 'row',
    justifyContent: 'flex-end',
    width: 'fill',
    height: 1,
    flexShrink: 0  // Prevent shrinking in column parent
  }
}, closeButton);
```

### Sizing (`src/sizing.ts`)

Uses **border-box** model by default (like modern CSS):
- `width`/`height` include padding and border
- Content area = total - padding - border

**Size values:**
- Number: Fixed character width/height (e.g., `width="40"`)
- `'auto'`: Size to content
- `'fill'`: Expand to fill *remaining* available space
- `'NN%'`: Percentage of parent's available space (e.g., `width="50%"`)

**Min/max constraints:**
- `minWidth`, `minHeight`: Minimum size (prevents shrinking below)
- `maxWidth`, `maxHeight`: Maximum size (prevents growing beyond)

```xml
<!-- Container that grows but caps at 80 characters -->
<container style="width: fill; max-width: 80;">
  <text>Content constrained to 80 chars max</text>
</container>

<!-- Flex item that won't shrink below 20 characters -->
<button style="min-width: 20; flex-shrink: 1;">Submit</button>
```

**`fill` vs percentage:**
- `fill` is context-aware - takes remaining space after siblings
- `100%` always means 100% of parent, regardless of siblings

```xml
<!-- fill takes remaining 80% -->
<container style="display: flex; flexDirection: row">
  <text width="20%">Sidebar</text>
  <container width="fill">Main content</container>
</container>

<!-- 100% would cause overflow (20% + 100% = 120%) -->
```

**Table column widths:**
Use `width` (via `style` or `colWidth` prop) on `<th>` elements for O(1) column sizing (skips row sampling):
```xml
<thead>
  <tr>
    <th style="width: 20%">Name</th>
    <th style="width: 10%">Status</th>
    <th style="width: fill">Description</th>
  </tr>
</thead>
```

Both `style.width` and the `colWidth` prop work for percentages, fixed widths, and `fill`. The `style.width` approach is preferred as it's consistent with other elements.

### Box Model

```
┌─────────────────────────────┐
│         margin              │
│  ┌───────────────────────┐  │
│  │       border          │  │
│  │  ┌─────────────────┐  │  │
│  │  │    padding      │  │  │
│  │  │  ┌───────────┐  │  │  │
│  │  │  │  content  │  │  │  │
│  │  │  └───────────┘  │  │  │
│  │  └─────────────────┘  │  │
│  └───────────────────────┘  │
└─────────────────────────────┘
```

### Single-Line Element Padding

For single-line elements (buttons, text, input) without borders, vertical padding is ignored in cross-axis sizing. This prevents buttons from expanding to multiple lines when padding is applied.

**Behavior:**
- Horizontal padding: Applied normally, adds space left/right
- Vertical padding: Ignored for elements with `intrinsicSize.height === 1` and no border

**Example:**
```xml
<!-- Button stays 1 line tall, horizontal padding adds 2 chars -->
<button label="Submit" style="padding: 1;" />

<!-- Bordered button respects vertical padding (3 lines) -->
<button label="Submit" style="padding: 1; border: thin;" />
```

This ensures default `[ ]` style buttons remain single-line while bordered buttons can expand.

### Chrome Collapse

When an element has insufficient space for content due to border and padding consuming all available space, Melker progressively collapses "chrome" (padding first, then border) to preserve minimum content space.

**Collapse Order:**
1. **Padding collapse** - Reduced proportionally per side
2. **Border collapse** - Symmetric: both left+right or both top+bottom removed together

**Behavior:**
- Symmetric border collapse: if both sides have borders, both are removed together for visual consistency (never removes just left or just top alone)
- Silent collapse with debug logging (`SizingModel: Chrome collapsed: bounds=...`)
- Inner containers collapse before outer (natural with recursive layout)
- Minimum content area: 1 character
- No visual indicator - collapsed elements simply render smaller

**Example:**
```xml
<!-- With height: 3, border: thin (2), padding: 1 (2) = 4 chars chrome -->
<!-- Only 3 available, so padding collapses to fit -->
<container style="width: 12; height: 3; border: thin; padding: 1;">
  <text>Content</text>
</container>
```

**Implementation:**
- `src/sizing.ts`: `ChromeCollapseState` interface, `calculateContentBounds()` logic
- `src/layout.ts`: `chromeCollapse` field in `LayoutNode`
- `src/rendering.ts`: `_renderBorder()` skips collapsed borders

## Tabs Component

The `<tabs>` component provides a tabbed interface with clickable tab headers. Each `<tab>` defaults to flex column layout.

### Usage

```xml
<tabs id="settings">
  <tab id="general" title="General">
    <text>General settings content</text>
  </tab>
  <tab id="advanced" title="Advanced">
    <text>Advanced settings content</text>
  </tab>
  <tab id="about" title="About">
    <text>About content</text>
  </tab>
</tabs>

<!-- To start on a specific tab, use activeTab with the tab's id -->
<tabs id="settings" activeTab="advanced">...</tabs>
```

### Props

**Tabs container:**
- `id` - Element identifier
- `activeTab` - ID of active tab (must match a tab's id attribute)
- `onChange` - Handler called when tab changes (`event.tabId`, `event.index`)

**Tab panel:**
- `id` - Tab identifier (used for activeTab reference)
- `title` - Tab header text (required)
- `disabled` - Disable tab selection

### Behavior

- Tab headers render as a button row: `│ General │ Advanced │ About │`
- Active tab is bold, focused tab is underlined
- Navigate with Tab/Shift+Tab, activate with Enter or click
- Default tab style includes `border: thin; margin-top: 1`

## Data Table Component

The `<data-table>` component is a high-performance table for displaying large datasets with simple array-based data.

### Usage

**Inline JSON (simplest for static data, parse errors logged):**

```xml
<data-table
  id="users"
  style="width: fill; height: 20;"
  selectable="single"
  sortColumn="0"
  sortDirection="asc"
>
{
  "columns": [
    { "header": "ID", "width": 5, "align": "right" },
    { "header": "Name", "width": "30%" },
    { "header": "Status", "width": 10 },
    { "header": "Description" }
  ],
  "rows": [
    [1, "Alice", "Active", "Engineer"],
    [2, "Bob", "Away", "Designer"]
  ]
}
</data-table>
```

### Props

| Prop                | Type                         | Default  | Description                          |
|---------------------|------------------------------|----------|--------------------------------------|
| `columns`           | DataTableColumn[]            | []       | Column definitions (set via script)  |
| `rows`              | CellValue[][]                | []       | Row data (set via script)            |
| `footer`            | CellValue[][]                | -        | Footer rows                          |
| `rowHeight`         | number                       | 1        | Lines per row                        |
| `showHeader`        | boolean                      | true     | Show header row                      |
| `showFooter`        | boolean                      | true     | Show footer if data exists           |
| `showColumnBorders` | boolean                      | false    | Show column separators               |
| `border`            | BorderStyle                  | 'thin'   | Table border style                   |
| `sortColumn`        | number                       | -        | Initial sort column index            |
| `sortDirection`     | `'asc'` \| `'desc'`          | -        | Initial sort direction               |
| `selectable`        | `'none'` \| `'single'` \| `'multi'` | 'none' | Selection mode                  |
| `onSelect`          | function                     | -        | Selection change handler             |
| `onActivate`        | function                     | -        | Enter/double-click handler           |
| `onSort`            | function                     | -        | Sort change notification (optional)  |
| `onGetId`           | function                     | -        | Map row → string ID for cross-component sync |
| `selectedIds`       | string[]                     | -        | Controlled selection by ID (overrides index-based) |
| `resizable`         | boolean                      | true     | Enable column resize by dragging     |
| `minColumnWidth`    | number                       | 3        | Minimum column width when resizing   |
| `onColumnResize`    | function                     | -        | Column resize handler                |

### Column Definition

```typescript
interface DataTableColumn {
  header: string;                              // Header text
  width?: number | `${number}%` | 'fill';     // Column width
  align?: 'left' | 'center' | 'right';        // Text alignment
  sortable?: boolean;                          // Enable sorting (default: true)
  comparator?: (a, b) => number;               // Custom sort function
}
```

### Behavior

- **Sorting**: Click headers to sort; handled internally, no handler needed
- **Selection**: Arrow keys navigate, Enter/double-click activates
- **Scrolling**: Mouse wheel, scrollbar drag, keyboard (PageUp/Down, Home/End)
- **Column resize**: Drag column borders in header; hover shows border indicator when `showColumnBorders` is off
- **Events**: Always report original row indices (not sorted positions)
- **Border color**: All borders render in `textMuted` theme color for a faded appearance

### When to Use data-table vs table

| Use `<data-table>` when        | Use `<table>` when                       |
|--------------------------------|------------------------------------------|
| Large datasets (100+ rows)     | Complex cell content (buttons, inputs)   |
| Simple text/number cells       | Variable row heights                     |
| Performance is critical        | Need nested elements in cells            |
| Data is array-based            | Building table dynamically with elements |

### Implementation Files

| File                                      | Purpose                                              |
|-------------------------------------------|------------------------------------------------------|
| `src/components/data-table.ts`            | Component implementation                             |
| `src/components/utils/component-utils.ts` | Shared text formatting, JSON parsing, bounds         |
| `src/components/utils/scroll-manager.ts`  | Shared scroll state management                       |

## Table Component

The `<table>` component provides data tables with optional scrollable body.

### Usage

```xml
<table id="users" style="width: fill; height: 20;">
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody style="overflow: scroll">
    <tr>
      <td>Alice</td>
      <td>alice@example.com</td>
      <td>Active</td>
    </tr>
    <tr>
      <td>Bob</td>
      <td>bob@example.com</td>
      <td>Inactive</td>
    </tr>
  </tbody>
</table>
```

### Structure

- `<table>` - Root container, handles column width calculation
- `<thead>` - Fixed header section (non-scrolling)
- `<tbody>` - Data rows, supports `overflow: scroll` style for scrolling
- `<tfoot>` - Fixed footer section (non-scrolling)
- `<tr>` - Table row
- `<td>` / `<th>` - Table cells (th renders bold)

### Props

**Table:**
- `border` - Border style: `'single'` (default), `'double'`, `'rounded'`, `'none'`
- `columnBorders` - Show vertical column separators (default: `true`)
- `cellPadding` - Padding inside cells in characters (default: `1`)
- `style.width` / `style.height` - Table dimensions

**tbody:**
- `style="overflow: scroll"` - Enable vertical scrolling when content exceeds height
- `maxHeight` - Maximum visible rows when scrolling enabled

### Behavior

- Column widths are calculated from max content width across all sections
- tbody scrolling uses the same scrollbar system as containers
- Wheel events are handled by the tbody element (Wheelable interface)
- Click events on cells bubble to the table element for hit testing

### Implementation Files

| File                               | Purpose                                  |
|------------------------------------|------------------------------------------|
| `src/components/table.ts`          | Table class, interface impls, re-exports |
| `src/components/table-types.ts`    | Type definitions (TableProps, events)    |
| `src/components/table-sorting.ts`  | Comparators, sort logic, cache           |
| `src/components/table-columns.ts`  | Column width calculation, cache          |
| `src/components/table-render.ts`   | All render helpers (borders, rows, etc.) |
| `src/components/table-section.ts`  | thead/tbody/tfoot sections               |
| `src/components/table-row.ts`      | Row container                            |
| `src/components/table-cell.ts`     | td/th cells                              |

## Data Tree Component

The `<data-tree>` component displays hierarchical data with expand/collapse, selection, keyboard navigation, multi-column support, and virtual scrolling.

### Usage

**Inline JSON:**

```xml
<data-tree
  id="filetree"
  style="width: fill; height: 20; border-color: cyan;"
  selectable="single"
  expandAll="true"
  tooltip="auto"
  onChange="$app.handleSelect(event)"
  onActivate="$app.handleActivate(event)"
>
{
  "nodes": [
    {
      "label": "src",
      "children": [
        { "label": "engine.ts", "value": "12,450 B" },
        { "label": "layout.ts", "value": "8,200 B" }
      ]
    },
    { "label": "README.md", "value": "580 B" }
  ]
}
</data-tree>
```

**Multi-column:**

```xml
<data-tree
  columns='[{"header": "Size", "width": 10, "align": "right"}, {"header": "Type", "width": 6}]'
  showColumnBorders="true"
  selectable="single"
>
{
  "nodes": [
    {
      "label": "src",
      "values": ["", "dir"],
      "children": [
        { "label": "engine.ts", "values": ["12,450 B", "ts"] }
      ]
    }
  ]
}
</data-tree>
```

### Props

| Prop                | Type                                  | Default | Description                                |
|---------------------|---------------------------------------|---------|--------------------------------------------|
| `nodes`             | TreeNode[]                            | []      | Tree node data (or inline JSON content)    |
| `showConnectors`    | boolean                               | true    | Show branch connector lines                |
| `indent`            | number                                | 2       | Characters per indent level                |
| `expandAll`         | boolean                               | false   | Start fully expanded                       |
| `showValues`        | boolean                               | false   | Show value column in single-column mode    |
| `border`            | BorderStyle                           | 'thin'  | Border style                               |
| `columns`           | TreeColumn[]                          | -       | Additional value columns (tree is implicit first) |
| `showColumnBorders` | boolean                               | false   | Show column separators                     |
| `showHeader`        | boolean                               | auto    | Show column headers (default: true when columns defined) |
| `selectable`        | `'none'` \| `'single'` \| `'multi'`  | 'none'  | Selection mode                             |
| `selectedNodes`     | string[]                              | -       | Controlled selection by node ID            |
| `onGetId`           | function                              | -       | Map node → string ID for cross-component sync |
| `selectedIds`       | string[]                              | -       | Controlled selection by ID (overrides selectedNodes) |
| `onChange`          | function                              | -       | Selection change handler                   |
| `onActivate`        | function                              | -       | Enter/double-click handler                 |
| `onExpand`          | function                              | -       | Node expanded handler                      |
| `onCollapse`        | function                              | -       | Node collapsed handler                     |

### Style Props

| Property          | Type        | Default | Description                          |
|-------------------|-------------|---------|--------------------------------------|
| `border-color`    | ColorInput  | textMuted | Color for box border               |
| `connector-color` | ColorInput  | gray    | Color for tree connector lines/icons |

### TreeNode Interface

```typescript
interface TreeNode {
  id?: string;            // Unique ID (auto-generated from label path if omitted)
  label: string;          // Display text
  value?: CellValue;      // Single-column mode value
  values?: CellValue[];   // Multi-column mode values
  children?: TreeNode[];  // Child nodes
  expanded?: boolean;     // Initial expand state
  disabled?: boolean;     // Not selectable
}
```

### Keyboard

| Key           | Action                                              |
|---------------|-----------------------------------------------------|
| Arrow Up/Down | Navigate visible nodes                              |
| Arrow Right   | Expand branch / move to first child if expanded     |
| Arrow Left    | Collapse branch / move to parent if leaf/collapsed  |
| Enter         | Activate node                                       |
| Space         | Toggle selection (multi) or expand/collapse (none)  |
| Home/End      | First/last visible node                             |
| PageUp/Down   | Scroll by viewport height                           |

### Methods

```typescript
const tree = document.getElementById('myTree');
tree.getValue();                      // Get TreeNode[]
tree.setValue(nodes);                  // Set tree data
tree.expandNode(nodeId);              // Expand node
tree.collapseNode(nodeId);            // Collapse node
tree.expandAll();                     // Expand all nodes
tree.collapseAll();                   // Collapse all nodes
tree.toggleNode(nodeId);              // Toggle expand
tree.setChildren(nodeId, children);   // Replace children (for lazy loading)
tree.getSelectedNodes();              // Get selected node IDs
tree.scrollToNode(nodeId);            // Scroll to node
```

### Implementation Files

| File                                      | Purpose                                              |
|-------------------------------------------|------------------------------------------------------|
| `src/components/data-tree.ts`             | Component implementation                             |
| `src/components/utils/component-utils.ts` | Shared text formatting, JSON parsing, bounds, theme  |
| `src/components/utils/scroll-manager.ts`  | Shared scroll state management                       |

## Dialog Component

The `<dialog>` component provides modal overlay dialogs. Defaults to flex column layout.

### Usage

```xml
<dialog id="settings" title="Settings" open=${true} modal=${true}>
  <text>Dialog content</text>
  <button label="Close" onClick="dialog.props.open = false" />
</dialog>
```

### Props

| Prop        | Type           | Default | Description                                      |
|-------------|----------------|---------|--------------------------------------------------|
| `title`     | string         | -       | Title bar text                                   |
| `open`      | boolean        | false   | Whether dialog is visible                        |
| `modal`     | boolean        | true    | Block interaction with background                |
| `backdrop`  | boolean        | true    | Show semi-transparent backdrop                   |
| `width`     | number\|string | 80%     | Width: number, "50%", "fill", or 0<v<1 decimal   |
| `height`    | number\|string | 70%     | Height: number, "50%", "fill", or 0<v<1 decimal  |
| `draggable` | boolean        | false   | Allow dragging by title bar                      |
| `offsetX`   | number         | 0       | Horizontal offset from center                    |
| `offsetY`   | number         | 0       | Vertical offset from center                      |

### Draggable Dialogs

When `draggable={true}`, users can click and drag the title bar to move the dialog:

```xml
<dialog id="movable" title="Drag me!" draggable=${true} open=${true}>
  <text>This dialog can be moved around</text>
</dialog>
```

The dialog position is stored in `offsetX` and `offsetY` props, which persist the drag offset from the centered position.

### Dialog Sizing Tips

The default dialog height is `min(floor(vpHeight * 0.7), 20)` — typically 16 rows on a standard 24-row terminal, giving ~12 rows of content area (after title bar and borders). For dialogs with multiple form fields, set an explicit `height` to ensure all content fits:

```xml
<!-- Form dialog with explicit height to fit inputs -->
<dialog id="form" title="Enter Details" open="true" height="14">
  <container style="display: flex; flex-direction: column; padding: 1;">
    <text>Name:</text>
    <input id="name" placeholder="Enter name" style="width: 30; margin-bottom: 1;" />
    <text>Email:</text>
    <input id="email" placeholder="Enter email" style="width: 30; margin-bottom: 1;" />
    <button label="Submit" />
  </container>
</dialog>
```

**Avoid `border: thin` on inputs inside dialogs** — the border adds 2 rows per input (top+bottom) and can cause content to overflow the dialog's content area.

## Filterable List Components

A family of searchable/selectable list components built on a shared `FilterableListCore` base.

See `agent_docs/filterable-list-architecture.md` for implementation details.

### Components

| Component           | Description                      |
|---------------------|----------------------------------|
| `<combobox>`        | Inline dropdown with text filter |
| `<select>`          | Dropdown picker without filter   |
| `<autocomplete>`    | Combobox with async loading      |
| `<command-palette>` | Modal command picker (draggable by title bar) |

### Child Elements

| Element    | Description                              |
|------------|------------------------------------------|
| `<option>` | Selectable item (value, disabled, shortcut) |
| `<group>`  | Groups options under a header            |

### Quick Examples

```xml
<!-- Combobox with text filtering -->
<combobox placeholder="Select country..." onChange="$app.setCountry(event.value)">
  <option value="us">United States</option>
  <option value="uk">United Kingdom</option>
</combobox>

<!-- Simple select dropdown -->
<select value="medium" onChange="$app.setSize(event.value)">
  <option value="small">Small</option>
  <option value="medium">Medium</option>
  <option value="large">Large</option>
</select>

<!-- Autocomplete with async search -->
<autocomplete
  placeholder="Search users..."
  onSearch="$app.searchUsers(event.query)"
  onSelect="$app.selectUser(event)"
  debounce="300"
/>

<!-- Command palette -->
<command-palette open="${$app.showPalette}" onSelect="$app.runCommand(event.value)">
  <group label="File">
    <option value="file.new" shortcut="Ctrl+N">New File</option>
  </group>
</command-palette>
```

### Key Props (shared)

| Prop         | Type     | Description                                    |
|--------------|----------|------------------------------------------------|
| `open`       | boolean  | Dropdown visibility                            |
| `filter`     | string   | 'fuzzy', 'prefix', 'contains', 'exact', 'none' |
| `maxVisible` | number   | Max dropdown height (default: 8)               |
| `onSelect`   | function | Called when option selected                    |

### Key Props (shared, continued)

| Prop      | Type              | Description                                        |
|-----------|-------------------|----------------------------------------------------|
| `options` | array or function | Data-driven options (alternative to `<option>` children) |

The `options` prop accepts `{ id, label, group?, disabled?, shortcut? }` objects or a function returning them.

### Methods (shared)

All filterable list components provide consistent methods:

```typescript
const select = document.getElementById('mySelect');
select.getValue();         // Get selected value (string | undefined)
select.setValue('option1'); // Set selected value (scrolls to option)

// For combobox/autocomplete, setValue also updates the input display
const combo = document.getElementById('myCombo');
combo.setValue('us');       // Selects option and shows its label in input
combo.setValue(undefined);  // Clear selection and input text

// Set options dynamically (invalidates filter cache automatically)
combo.setOptions([
  { id: 'us', label: 'United States' },
  { id: 'uk', label: 'United Kingdom' },
]);

// Or use a function for lazy evaluation
combo.setOptions(() => getAvailableOptions());

// After modifying children directly, refresh the child options cache
combo.refreshChildOptions();
```

### Implementation Files

| File                                              | Purpose                      |
|---------------------------------------------------|------------------------------|
| `src/components/filterable-list/core.ts`          | Shared base class            |
| `src/components/filterable-list/combobox.ts`      | Combobox component           |
| `src/components/filterable-list/select.ts`        | Select component             |
| `src/components/filterable-list/autocomplete.ts`  | Autocomplete component       |
| `src/components/filterable-list/command-palette.ts` | Command palette component  |
| `src/components/filterable-list/filter.ts`        | Fuzzy/prefix/contains matching |

## File Browser Component

The `<file-browser>` component provides file system navigation for selecting files and directories.

### Usage

```xml
<dialog id="file-dialog" title="Open File" open="false" modal="true" width="70" height="20">
  <file-browser
    id="fb"
    selectionMode="single"
    selectType="file"
    onSelect="$app.handleSelect(event)"
    onCancel="$app.closeDialog()"
    maxVisible="12"
  />
</dialog>
```

### Props

| Prop            | Type    | Default  | Description                      |
|-----------------|---------|----------|----------------------------------|
| `path`          | string  | cwd      | Initial directory                |
| `selectionMode` | string  | 'single' | 'single' or 'multiple'           |
| `selectType`    | string  | 'file'   | 'file', 'directory', or 'both'   |
| `filter`        | string  | 'fuzzy'  | Filter mode                      |
| `showHidden`    | boolean | false    | Show dotfiles                    |
| `maxVisible`    | number  | 10       | Visible rows                     |

### Events

| Event        | Properties                 | Description            |
|--------------|----------------------------|------------------------|
| `onSelect`   | path, paths, isDirectory   | File/dir selected      |
| `onCancel`   | -                          | Cancelled              |
| `onNavigate` | path                       | Navigated to directory |
| `onError`    | code, message              | Error occurred         |

### Keyboard

- Arrow keys: Navigate list
- Enter: Open directory / select file
- Backspace: Parent directory
- Escape: Cancel
- Type: Filter entries

### Implementation Files

| File                                        | Purpose                     |
|---------------------------------------------|-----------------------------|
| `src/components/file-browser/file-browser.ts` | Main component            |
| `src/components/file-browser/file-entry.ts` | Type definitions            |
| `src/components/file-browser/file-utils.ts` | Directory loading utilities |

See `agent_docs/file-browser-architecture.md` for detailed architecture.

## Progress Component

The `<progress>` component displays a progress bar using canvas pixels for smooth sub-character fill.

### Usage

```xml
<progress value="50" width="25" />
<progress value="75" showValue="true" />
<progress indeterminate="true" fillColor="cyan" />
```

### Props

| Prop             | Type    | Default | Description                          |
|------------------|---------|---------|--------------------------------------|
| `value`          | number  | 0       | Current progress value               |
| `max`            | number  | 100     | Maximum value                        |
| `min`            | number  | 0       | Minimum value                        |
| `width`          | number  | 20      | Bar width in terminal columns        |
| `height`         | number  | 1       | Bar height in terminal rows          |
| `showValue`      | boolean | false   | Display percentage text after bar    |
| `indeterminate`  | boolean | false   | Show animated loading state          |
| `fillColor`      | string  | theme   | Color for filled portion             |
| `emptyColor`     | string  | theme   | Color for empty portion              |
| `animationSpeed` | number  | 50      | Indeterminate animation speed (ms)   |

### Behavior

- Extends `CanvasElement` for pixel-level rendering (2x3 pixels per character)
- Uses sextant characters for smooth sub-character fill resolution
- Theme-aware defaults: B&W uses black/white, color uses green/#aaa
- `flexShrink: 0` prevents layout compression below specified dimensions
- Indeterminate mode shows animated sliding pulse

### Methods

```typescript
const progress = document.getElementById('myProgress');
progress.setValue(75);           // Set progress value
progress.getValue();             // Get current value
progress.setIndeterminate(true); // Enable/disable indeterminate mode
```

## Spinner Component

The `<spinner>` component displays an animated loading indicator with optional text or cycling verbs. All spinners share a single animation timer for efficiency.

### Usage

```xml
<!-- Basic spinner -->
<spinner text="Loading..." />

<!-- Different variants -->
<spinner variant="dots" text="Processing" />
<spinner variant="braille" text="Computing" />
<spinner variant="pulse" text="Waiting" />

<!-- With cycling verbs (theme name) -->
<spinner variant="dots" verbs="thinking" />

<!-- With custom verbs (comma-separated) -->
<spinner variant="line" verbs="Analyzing, Optimizing, Finalizing" />

<!-- Text-only with animated shade (no spinner char) -->
<spinner variant="none" verbs="dreaming" shade="true" />

<!-- Spinner on right side of text -->
<spinner text="Working" textPosition="right" />
```

### Props

| Prop           | Type                    | Default  | Description                                   |
|----------------|-------------------------|----------|-----------------------------------------------|
| `text`         | string                  | -        | Text displayed beside spinner                 |
| `variant`      | SpinnerVariant          | 'line'   | Animation style                               |
| `speed`        | number                  | 100      | Frame interval in milliseconds                |
| `textPosition` | 'left' \| 'right'       | 'left'   | Spinner position relative to text             |
| `spinning`     | boolean                 | true     | Whether spinner is animating                  |
| `verbs`        | VerbTheme \| string     | -        | Cycling text: theme name or comma-separated   |
| `verbSpeed`    | number                  | 800      | Verb cycle interval in milliseconds           |
| `shade`        | boolean                 | false    | Enable animated brightness wave across text   |
| `shadeSpeed`   | number                  | 60       | Shade wave speed (ms per character)           |

### Variants

| Variant   | Frames                         | Description           |
|-----------|--------------------------------|-----------------------|
| `none`    | (no spinner)                   | Text only, no spinner |
| `line`    | `\| / - \`                     | Classic ASCII spinner |
| `dots`    | `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`                  | Braille dots pattern  |
| `braille` | `⣷⣯⣟⡿⢿⣻⣽⣾`                     | Rotating braille      |
| `arc`     | `◜◠◝◞◡◟`                       | Curved arc rotation   |
| `bounce`  | `⠁⠂⠄⠂`                         | Bouncing dot          |
| `flower`  | `·✻✽✶✳✢`                       | Blooming flower       |
| `pulse`   | `·•●•`                         | Pulsating dot         |

### Verb Themes

Standard themes cycle through contextual variations:
- `loading`: Loading, Loading., Loading.., Loading...
- `thinking`: Thinking, Pondering, Contemplating, Reasoning
- `working`: Working, Processing, Computing, Calculating
- `waiting`: Please wait, Hold on, One moment, Almost there
- `fetching`: Fetching, Downloading, Retrieving, Receiving
- `saving`: Saving, Writing, Storing, Committing

Poetic themes have 8 words each:
- `dreaming`, `conjuring`, `brewing`, `weaving`, `unfolding`, `stargazing`

### Shade Effect

When `shade="true"`, text characters animate with a brightness gradient that moves left to right:
- Peak position: 100% brightness
- Adjacent characters: 75%, 50%, 50%...
- Creates a "spotlight" scanning effect

### Methods

```typescript
const spinner = document.getElementById('mySpinner');
spinner.start();           // Start animation
spinner.stop();            // Stop animation
spinner.getValue();        // Get text value
spinner.setValue('text');  // Set text value
```

### Behavior

- All spinners share a single 40ms timer (efficient for multiple spinners)
- Frame timing is calculated from elapsed time, not frame counters
- `none` variant displays text only (useful for animated verbs/shade without spinner character)
- Intrinsic width accounts for longest verb when verbs are set

## Canvas Component

The `<canvas>` component provides pixel graphics using Unicode sextant characters (2x3 pixels per cell).

**Important:** Use `width`/`height` **props** (not `style.width`/`style.height`) to define the pixel buffer size. Canvas-family components maintain two separate size concepts: `props.width`/`props.height` is the *declarative* value (`"100%"`, `30`, `"fill"`) used by the layout engine, while internal `_terminalWidth`/`_terminalHeight` is the *resolved* numeric terminal-cell size used for buffer allocation and rendering. `setSize()` updates the internal size, not props — this separation prevents responsive strings from being clobbered to numbers on resize. See [dx-footguns.md](dx-footguns.md) for details.

### Canvas Methods

| Method                                               | Description                                                      |
|------------------------------------------------------|------------------------------------------------------------------|
| `clear()`                                            | Clear the canvas                                                 |
| `getBufferSize()`                                    | Get pixel buffer dimensions `{ width, height }`                  |
| `getBufferWidth()`                                   | Get buffer width in pixels                                       |
| `getBufferHeight()`                                  | Get buffer height in pixels                                      |
| `getVisualSize()`                                    | Get aspect-corrected visual size                                 |
| `getPixelAspectRatio()`                              | Get pixel aspect ratio (~0.67 sextant, ~0.5 quadrant)            |
| `setPixel(x, y)`                                     | Set a pixel at coordinates                                       |
| `fillRect(x, y, w, h)`                               | Fill a rectangle                                                 |
| `drawLine(x1, y1, x2, y2)`                           | Draw a line between two points                                   |
| `drawCircleCorrected(x, y, radius)`                  | Draw aspect-corrected circle                                     |
| `drawSquareCorrected(x, y, size)`                    | Draw aspect-corrected square                                     |
| `drawImage(image, dx, dy, dw, dh)`                   | Draw full image at position                                      |
| `drawImageRegion(image, sx, sy, sw, sh, dx, dy, dw, dh)` | Draw portion of image                                        |
| `decodeImageBytes(bytes)`                            | Decode PNG/JPEG/GIF bytes to `{ width, height, data, bytesPerPixel }` (sync) |
| `decodeImageBytesAsync(bytes)`                       | Decode PNG/JPEG/GIF/WebP bytes (async, required for WebP)            |
| `markDirty()`                                        | Mark canvas for re-render                                        |
| `drawPathSVG(d)`                                     | Stroke SVG path string                                           |
| `fillPathSVG(d)`                                     | Fill SVG path string (even-odd rule)                             |
| `drawPathSVGColor(d, color)`                         | Stroke SVG path with specific color                              |
| `fillPathSVGColor(d, color)`                         | Fill SVG path with specific color                                |
| `drawPath(commands)`                                 | Stroke from PathCommand array                                    |
| `fillPath(commands)`                                 | Fill from PathCommand array                                      |
| `drawPathCorrected(commands)`                        | Stroke with aspect correction                                    |
| `fillPathCorrected(commands)`                        | Fill with aspect correction                                      |
| `drawText(x, y, text, options?)`                     | Draw text label at pixel coords (terminal chars, not pixels)     |
| `drawTextColor(x, y, text, color, options?)`         | Draw text label with specific color                              |

### drawText / drawTextColor

Draw text labels on the canvas as terminal characters overlaid on top of the pixel content. Unlike pixel drawing methods, text is rendered at terminal cell resolution (not pixel resolution) and appears crisp regardless of canvas mode.

```typescript
// Using current drawing color
canvas.setColor('#ffffff');
canvas.drawText(100, 50, 'Label');

// With explicit color and options
canvas.drawTextColor(100, 50, 'Center', '#ff0', { align: 'center', bg: '#333' });
```

**Parameters:**
- `x`, `y` — pixel coordinates (same space as `drawLine`, `fillCircle`, etc.)
- `text` — string to render
- `color` — foreground color (CSS string or packed RGBA), for `drawTextColor` only
- `options.align` — `'left'` (default), `'center'`, or `'right'` relative to x
- `options.bg` — background color (CSS string or packed RGBA)

Text labels are queued during `onPaint`/`onOverlay` and rendered after the pixel buffer is converted to terminal cells.

### drawImageRegion

Draw a portion of an image to the canvas with scaling:

```typescript
canvas.drawImageRegion(
  image,     // Image data or Uint8Array (PNG/JPEG bytes)
  sx, sy,    // Source rectangle top-left corner
  sw, sh,    // Source rectangle dimensions
  dx, dy,    // Destination position
  dw, dh     // Destination dimensions (scales if different from sw, sh)
);
```

Useful for tile-based rendering where you need to draw portions of larger images.

### Polygon Drawing

```typescript
canvas.fillPoly(points);                   // Fill polygon (scanline, even-odd rule)
canvas.drawPoly(points);                   // Draw polygon outline
canvas.fillPolyColor(points, color);       // Fill polygon with specific color
canvas.drawPolyColor(points, color);       // Draw polygon outline with specific color
canvas.fillCircleCorrectedColor(x, y, r, color);  // Fill aspect-corrected circle with color
```

Where `points` is `number[][]` with each element `[x, y]`.

### SVG Path Drawing

Draw and fill shapes using SVG path syntax (`M`, `L`, `H`, `V`, `Q`, `T`, `C`, `S`, `A`, `Z`). Curves are tessellated to line segments using De Casteljau subdivision (Bezier) and adaptive angle stepping (arcs).

```typescript
// SVG path string API (most convenient)
canvas.drawPathSVG(d);                     // Stroke SVG path string
canvas.fillPathSVG(d);                     // Fill SVG path string (even-odd rule)
canvas.drawPathSVGColor(d, color);         // Stroke with specific color
canvas.fillPathSVGColor(d, color);         // Fill with specific color

// PathCommand array API
canvas.drawPath(commands);                 // Stroke from parsed commands
canvas.fillPath(commands);                 // Fill from parsed commands
canvas.drawPathColor(commands, color);     // Stroke with specific color
canvas.fillPathColor(commands, color);     // Fill with specific color

// Aspect-corrected variants
canvas.drawPathCorrected(commands);        // Stroke with aspect correction
canvas.fillPathCorrected(commands);        // Fill with aspect correction
```

**SVG path commands supported:** `M/m` (moveTo), `L/l` (lineTo), `H/h` (horizontal), `V/v` (vertical), `Q/q` (quadratic Bezier), `T/t` (smooth quadratic), `C/c` (cubic Bezier), `S/s` (smooth cubic), `A/a` (arc), `Z/z` (close). Both absolute (uppercase) and relative (lowercase) coordinates.

**Example — draw a cubic Bezier ellipse:**
```typescript
const kappa = 0.5522847498;
const cx = 80, cy = 36, rx = 30, ry = 20;
const kx = Math.floor(rx * kappa), ky = Math.floor(ry * kappa);
const d = `M ${cx} ${cy - ry} C ${cx + kx} ${cy - ry} ${cx + rx} ${cy - ky} ${cx + rx} ${cy} C ${cx + rx} ${cy + ky} ${cx + kx} ${cy + ry} ${cx} ${cy + ry} C ${cx - kx} ${cy + ry} ${cx - rx} ${cy + ky} ${cx - rx} ${cy} C ${cx - rx} ${cy - ky} ${cx - kx} ${cy - ry} ${cx} ${cy - ry} Z`;
canvas.drawPathSVGColor(d, '#4488FF');
```

Multiple subpaths (separate `M` commands) create holes via even-odd fill rule.

### Canvas SVG Overlay Layers

Named SVG overlay layers draw `<path>` and `<text>` elements on the canvas using pixel coordinates. Useful for annotations, labels, and AI-drawn overlays. Multiple layers can coexist without interfering.

```typescript
const canvas = $melker.getElementById('chart');
canvas.setSvgOverlay('annotations', `
  <path d="M 10 5 L 50 15" stroke="red"/>
  <text x="30" y="10" fill="#fff" text-anchor="middle">Label</text>
`);
canvas.removeSvgOverlay('annotations');       // Remove one layer
canvas.removeSvgOverlaysByPrefix('ai:');      // Remove all AI layers
canvas.clearSvgOverlays();                    // Remove all layers
```

Supported SVG elements:
- `<path d="..." stroke="color" fill="color"/>` — M/L/H/V/C/S/Q/T/A/Z commands
- `<text x="N" y="N" fill="color" bg="color" text-anchor="middle|end">Text</text>`

### Canvas Tooltips

Canvas supports `tooltip` and `onTooltip` for contextual hover information. The `onTooltip` handler receives a `CanvasTooltipContext` with `pixelX`, `pixelY` (buffer pixel coordinates) and `color` (packed RGBA at that pixel).

```xml
<canvas id="chart" width="60" height="20"
  onPaint="$app.draw(event.canvas)"
  onTooltip="$app.chartTooltip(event)"
/>
```

```javascript
export function chartTooltip(event) {
  if (!event.context) return undefined;
  const { pixelX, pixelY, color } = event.context;
  return `**Data** at (${pixelX}, ${pixelY})`;
}
```

## Tile Map Component

The `<tile-map>` component renders an interactive slippy map with Mercator projection. Extends `CanvasElement` — inherits dithering, graphics pipeline, shader support. See [tile-map-architecture.md](tile-map-architecture.md) for full architecture details.

### Usage

```html
<tile-map lat="51.5074" lon="-0.1278" zoom="12" width="100%" height="100%"
          provider="satellite" onOverlay="$app.drawMarkers(event)" />
```

Requires `"map": true` in policy permissions.

### Key Props

| Prop          | Type             | Default           | Description                                |
|---------------|------------------|-------------------|--------------------------------------------|
| `lat`         | `number`         | `51.5074`         | Center latitude                            |
| `lon`         | `number`         | `-0.1278`         | Center longitude                           |
| `zoom`        | `number`         | `5`               | Zoom level (0-20)                          |
| `provider`    | `string`         | `'openstreetmap'` | Tile provider key                          |
| `interactive` | `boolean`        | `true`            | Enable drag/scroll/double-click            |
| `onOverlay`   | `(event) => void`| —                 | Drawing callback with geo transforms       |
| `onTooltip`   | `(event) => void`| —                 | Tooltip callback on hover                  |
| `onMove`      | `(event) => void`| —                 | Fires on position change                   |
| `onZoom`      | `(event) => void`| —                 | Fires on zoom change                       |

### Built-in Providers

`openstreetmap`, `terrain`, `streets`, `voyager`, `voyager-nolabels`, `satellite`

### Programmatic API

```typescript
const map = $melker.getElementById('map');
map.setView(lat, lon, zoom?);    // Navigate
map.getCenter();                  // { lat, lon }
map.getZoom();                    // number
map.getBoundsLatLon();            // { north, south, east, west }
map.latLonToPixel(lat, lon);     // { x, y } | null
map.pixelToLatLon(x, y);        // { lat, lon }
map.zoomIn() / map.zoomOut();
map.panUp() / panDown() / panLeft() / panRight();
```

### SVG Overlay Layers

```typescript
const map = $melker.getElementById('map');
map.setSvgOverlay('routes', `
  <path d="M -0.1 51.5 L 2.3 48.8" stroke="red"/>
  <text lat="51.5" lon="-0.1" fill="#fff" text-anchor="middle">London</text>
`);
map.removeSvgOverlay('routes');
```

Coordinates in `d` attributes are **lat lon** order. `<text>` elements support `lat`, `lon`, `fill`, `bg`, `text-anchor` (`start`/`middle`/`end`) or `align` (`left`/`center`/`right`).

### Overlay Drawing

```typescript
export function drawMarkers(event) {
  const { canvas, geo } = event;
  for (const m of markers) {
    const pos = geo.latLonToPixel(m.lat, m.lon);
    if (!pos) continue;
    canvas.fillCircleCorrectedColor(pos.x, pos.y, 3, 'red');
    canvas.drawTextColor(pos.x, pos.y - 10, m.name, '#fff', { align: 'center' });
  }
}
```

## Slider Component

The `<slider>` component allows numeric value selection within a range using keyboard or mouse.

### Usage

```xml
<!-- Basic slider -->
<slider min="0" max="100" value="50" onChange="$app.handleChange(event)" />

<!-- With step increments -->
<slider min="0" max="10" step="1" value="5" showValue="true" />

<!-- With snap points -->
<slider min="0" max="100" snaps="[0, 25, 50, 75, 100]" value="25" />

<!-- Vertical orientation -->
<slider min="0" max="100" value="50" style="orientation: vertical; height: 8;" />
```

### Props

| Prop          | Type     | Default      | Description                                  |
|---------------|----------|--------------|----------------------------------------------|
| `min`         | number   | 0            | Minimum value                                |
| `max`         | number   | 100          | Maximum value                                |
| `value`       | number   | 0            | Current value                                |
| `step`        | number   | -            | Discrete step size (e.g., 5 = values 0,5,10...) |
| `snaps`       | number[] | -            | Array of specific snap points                |
| `showValue`   | boolean  | false        | Display value label after slider             |
| `onChange`    | function | -            | Called when value changes                    |

### Styles

| Style         | Type     | Default      | Description                                  |
|---------------|----------|--------------|----------------------------------------------|
| `orientation` | string   | 'horizontal' | 'horizontal' or 'vertical'                   |

### Keyboard Navigation

| Key              | Action                                   |
|------------------|------------------------------------------|
| Arrow Left/Down  | Decrease by step (or to previous snap)   |
| Arrow Right/Up   | Increase by step (or to next snap)       |
| Page Down        | Decrease by 10% of range                 |
| Page Up          | Increase by 10% of range                 |
| Home             | Jump to minimum                          |
| End              | Jump to maximum                          |

### Visual

```
▓▓▓▓▓▓▓●────────── 50    (horizontal with showValue)
```

- Focused slider shows thumb with reverse video (inverted colors)
- Theme-aware: Unicode characters for color themes, ASCII for B&W

### Methods

```typescript
const slider = document.getElementById('mySlider');
slider.setValue(75);    // Set value programmatically
slider.getValue();      // Get current value
```

## Separator Component

The `<separator>` component renders a horizontal or vertical line with optional centered label text. Orientation is automatically determined by the parent's flex direction.

### Usage

```xml
<!-- Horizontal separator (default, in column flex) -->
<separator />

<!-- With label -->
<separator label="Settings" />

<!-- With style -->
<separator label="Advanced" style="border-style: double; color: blue;" />

<!-- Vertical separator (automatic in row flex) -->
<container style="flex-direction: row; height: 5;">
  <text>Left</text>
  <separator label="OR" />
  <text>Right</text>
</container>
```

### Props

| Prop    | Type   | Default | Description                        |
|---------|--------|---------|-----------------------------------|
| `label` | string | -       | Optional text centered in the line |
| `style` | Style  | -       | Standard style object              |

### Style Properties

| Property       | Values      | Default | Description                                  |
|----------------|-------------|---------|----------------------------------------------|
| `border-style` | BorderStyle | 'thin'  | Line style: thin, thick, double, dashed, etc. |
| `color`        | ColorInput  | theme   | Line and label color                         |

### Automatic Orientation

The separator detects its parent's flex direction and renders accordingly:

| Parent flex-direction | Separator  | Line char | Size                     |
|-----------------------|------------|-----------|--------------------------|
| `column` (default)    | Horizontal | `─`       | width: fill, height: 1   |
| `row`                 | Vertical   | `│`       | width: 1, height: fill   |

### Visual Examples

**Horizontal (column flex):**
```
───────────────────────────────────────
─────────────── Settings ──────────────
═══════════════ Advanced ══════════════
```

**Vertical (row flex):**
```
Left │ Middle │ Right
     │        O
     │        R
     │        │
```

### Behavior

- Self-closing element (no children allowed)
- Fills available space in its orientation
- Label text renders vertically when separator is vertical
- Inherits theme colors by default

## Split Pane Component

The `<split-pane>` component splits its children horizontally (default) or vertically with draggable, focusable divider bars. Supports N children with N-1 dividers.

### Usage

```xml
<!-- Horizontal split (left/right), equal sizes -->
<split-pane style="width: fill; height: fill;">
  <container><text>Left</text></container>
  <container><text>Right</text></container>
</split-pane>

<!-- Vertical split, custom proportions, styled dividers -->
<split-pane
  sizes="1,2,1"
  dividerTitles="Header,Footer"
  style="width: fill; height: fill; direction: vertical; divider-style: thick; divider-color: cyan;"
>
  <container><text>Top (25%)</text></container>
  <container><text>Middle (50%)</text></container>
  <container><text>Bottom (25%)</text></container>
</split-pane>
```

### Props

| Prop            | Type     | Default        | Description                                              |
|-----------------|----------|----------------|----------------------------------------------------------|
| `sizes`         | `string` | equal          | Comma-separated proportions, e.g. `"1,2,1"`              |
| `dividerTitles` | `string` | -              | Comma-separated divider titles, e.g. `"Nav,Info"`        |
| `onResize`      | handler  | -              | Fired on resize: `{ sizes, dividerIndex, targetId }`     |

### Styles

| Style            | Type          | Default        | Description                                            |
|------------------|---------------|----------------|--------------------------------------------------------|
| `direction`      | `string`      | `'horizontal'` | `'horizontal'` (left/right) or `'vertical'` (top/bottom) |
| `min-pane-size`  | `number`      | `3`            | Minimum pane size in characters                        |
| `divider-style`  | `BorderStyle` | `'thin'`       | Line style: `thin`, `thick`, `double`, `dashed`, etc.  |
| `divider-color`  | `ColorInput`  | inherited      | Divider foreground color                               |

### Interaction

- **Mouse drag**: Drag dividers to resize adjacent panes
- **Keyboard**: Tab to focus a divider, then arrow keys to move it (Left/Right for horizontal, Up/Down for vertical)
- **Focus**: Dividers show reverse video when focused

### Implementation Notes

- Dividers are internal `SplitPaneDivider` elements interleaved between children
- Layout uses flexbox (`flexGrow` proportions on panes, fixed 1-char dividers)
- Nested split-panes work naturally
- See [split-pane-architecture.md](split-pane-architecture.md) for full details

## Connector Component

The `<connector>` component draws a line between two elements identified by their IDs. Uses box-drawing characters for orthogonal routing.

### Usage

```xml
<!-- Basic connector between two elements -->
<container style="flex-direction: row; gap: 4">
  <container id="box-a" style="border: single; padding: 1">Source</container>
  <container id="box-b" style="border: single; padding: 1">Target</container>
  <connector from="box-a" to="box-b" arrow="end" />
</container>

<!-- Vertical connector -->
<container style="flex-direction: column; gap: 2; align-items: center">
  <container id="top" style="border: single; padding: 1">Top</container>
  <container id="bottom" style="border: single; padding: 1">Bottom</container>
  <connector from="top" to="bottom" arrow="both" />
</container>

<!-- With label and custom style -->
<connector from="start" to="end" label="flow" style="color: green; lineStyle: double" />
```

### Props

| Prop       | Type   | Default      | Description                                                        |
|------------|--------|--------------|-------------------------------------------------------------------|
| `from`     | string | **required** | ID of the source element                                           |
| `to`       | string | **required** | ID of the target element                                           |
| `fromSide` | string | `'auto'`     | Side to connect from: `'top'`, `'bottom'`, `'left'`, `'right'`, `'center'`, `'auto'` |
| `toSide`   | string | `'auto'`     | Side to connect to: `'top'`, `'bottom'`, `'left'`, `'right'`, `'center'`, `'auto'` |
| `arrow`    | string | `'end'`      | Arrow style: `'none'`, `'start'`, `'end'`, `'both'`                |
| `label`    | string | -            | Optional label text at midpoint                                    |
| `routing`  | string | `'orthogonal'` | Line routing: `'direct'` or `'orthogonal'`                       |

### Style Properties

| Property    | Values                                | Description        |
|-------------|---------------------------------------|--------------------|
| `color`     | color                                 | Line color         |
| `lineStyle` | `'thin'`, `'thick'`, `'double'`, `'dashed'` | Line drawing style |

### Visual Examples

**Horizontal connector:**
```
┌────────┐          ┌────────┐
│ Source │─────────▶│ Target │
└────────┘          └────────┘
```

**Vertical connector with both arrows:**
```
┌─────┐
│ Top │
└──┬──┘
   ▲
   │
   ▼
┌─────┐
│ Bot │
└─────┘
```

**Orthogonal routing (auto-bends):**
```
┌───┐
│ A │──┐
└───┘  │
       │  ┌───┐
       └─▶│ B │
          └───┘
```

### Behavior

- Connector takes minimal layout space (1x1) but draws based on connected elements' positions
- Elements must have `id` attributes to be referenced
- Side selection defaults to 'auto' which chooses the best side based on relative positions
- Lines are drawn using box-drawing characters (─ │ ┌ ┐ └ ┘)
- Arrow heads use Unicode triangles (▶ ◀ ▲ ▼)
- Labels are automatically clipped to fit within the horizontal span of the line

## Graph Component

The `<graph>` component renders diagrams from Mermaid syntax or JSON input. Supports flowcharts, sequence diagrams, and class diagrams.

### Usage

```xml
<!-- Flowchart -->
<graph>
  flowchart LR
    A[Start] --> B{Decision}
    B -->|Yes| C[Done]
    B -->|No| D[Retry]
</graph>

<!-- Sequence diagram -->
<graph>
  sequenceDiagram
    participant U as User
    participant S as Server
    U->>S: Request
    S-->>U: Response
</graph>

<!-- Class diagram -->
<graph>
  classDiagram
    class Animal {
      +name: String
      +makeSound()
    }
    class Dog
    Animal <|-- Dog
</graph>

<!-- Load from URL -->
<graph src="./diagrams/flow.mmd" />

<!-- JSON input -->
<graph type="json">
  {
    "direction": "TB",
    "nodes": [
      { "id": "a", "label": "Node A", "shape": "rect" },
      { "id": "b", "label": "Node B", "shape": "diamond" }
    ],
    "edges": [
      { "from": "a", "to": "b", "arrow": "end" }
    ]
  }
</graph>
```

### Props

| Prop         | Type    | Default | Description                                               |
|--------------|---------|---------|-----------------------------------------------------------|
| `type`       | string  | auto    | Parser type: 'mermaid' or 'json' (auto-detected from content) |
| `src`        | string  | -       | Load content from URL                                     |
| `text`       | string  | -       | Inline content (alternative to children)                  |
| `style`      | Style   | -       | Container styling; use `overflow: scroll` for scrolling   |

### Content Priority

1. `src` attribute (loads from URL)
2. `text` attribute
3. Children text content

### Diagram Types (Auto-Detected)

| Type      | Mermaid Keywords       |
|-----------|------------------------|
| Flowchart | `flowchart`, `graph`   |
| Sequence  | `sequenceDiagram`      |
| Class     | `classDiagram`         |

### Supported Mermaid Syntax

**Flowcharts:**
- Directions: `TB`, `LR`, `BT`, `RL`
- Node shapes: `[rect]`, `{diamond}`, `((circle))`, `{{hexagon}}`
- Edges: `-->`, `---`, `-.->`, `==>`, with labels `-->|label|`
- Subgraphs: `subgraph name ... end`

**Sequence Diagrams:**
- Participants: `participant A as Alias`
- Messages: `->>`, `-->>`, `-x`, `--x`
- Notes: `Note over A: text`
- Fragments: `alt`, `opt`, `loop`, `par`

**Class Diagrams:**
- Classes: `class Name { +attr: Type; +method() }`
- Relationships: `<|--` (inheritance), `*--` (composition), `o--` (aggregation), `-->` (association)
- Annotations: `<<interface>>`, `<<abstract>>`

### Styling

The graph generates internal CSS classes that can be overridden. Use `style` prop for container-level overrides:

```xml
<graph style="border: none; padding: 2;">
  flowchart LR
    A --> B
</graph>
```

### Implementation Files

| File                                      | Purpose                                  |
|-------------------------------------------|------------------------------------------|
| `src/components/graph/graph.ts`           | GraphElement component                   |
| `src/components/graph/graph-to-melker.ts` | Converts parsed graph to melker elements |
| `src/components/graph/layout.ts`          | Graph layout algorithm                   |
| `src/components/graph/parsers/`           | Mermaid and JSON parsers                 |
| `src/components/graph/types.ts`           | GraphDefinition interfaces               |

## Segment Display Component

The `<segment-display>` component renders LCD/LED-style digits and text using Unicode characters with multiple visual styles.

### Usage

```xml
<!-- Basic clock display -->
<segment-display value="12:45:30" style="height: 5; color: green;" />

<!-- Different renderers -->
<segment-display value="1234567890" renderer="rounded" style="color: cyan;" />
<segment-display value="HELLO" renderer="geometric" style="height: 7; color: yellow;" />

<!-- Pixel renderer (bitmap font glyphs) -->
<segment-display value="Hello World" renderer="pixel" style="height: 5;" />
<segment-display value="Hello World" renderer="pixel" style="height: 7;" />

<!-- With scrolling -->
<segment-display value="HELLO WORLD" scroll="true" scrollSpeed="24" style="width: 50;" />

<!-- LCD style with off-segments visible -->
<segment-display value="88:88" style="color: #00ff00; off-color: #003300;" />

<!-- Pixel renderer with custom characters and colors -->
<segment-display value="MELKER" renderer="pixel" style="height: 7; color: cyan; off-color: #003333; pixel-char: #; off-pixel-char: .;" />
```

### Props

| Prop          | Type    | Default       | Description                                           |
|---------------|---------|---------------|-------------------------------------------------------|
| `value`       | string  | -             | Text to display                                       |
| `renderer`    | string  | 'box-drawing' | Visual style: 'box-drawing', 'rounded', 'geometric', 'pixel' |
| `scroll`      | boolean | false         | Enable horizontal scrolling                           |
| `scrollSpeed` | number  | 24            | Scroll speed in milliseconds                          |
| `scrollGap`   | number  | 3             | Gap between repeated text when scrolling              |

### Style Properties

| Property           | Values    | Description                                                     |
|--------------------|-----------|-----------------------------------------------------------------|
| `height`           | 5, 7      | Display height in rows (only 5 or 7 supported)                  |
| `color`            | any color | Color for "on" segments                                         |
| `off-color`        | any color | Panel background / "off" segment color (dimmed LCD effect)      |
| `background-color` | any color | Background color (overrides off-color if both set)              |
| `pixel-char`       | string    | Custom "on" character for pixel renderer (default: `█`)         |
| `off-pixel-char`   | string    | Custom "off" character for pixel renderer (default: `░`)        |
| `width`            | number    | Width limit for scrolling                                       |

### Renderers

| Renderer      | Characters       | Description                                              |
|---------------|------------------|----------------------------------------------------------|
| `box-drawing` | ━ ┃              | Clean thin lines (default)                               |
| `rounded`     | ╭ ╮ ╰ ╯ ━ ┃     | Rounded corners, modern look                             |
| `geometric`   | ▬ ▮ ▯            | Chunky LCD aesthetic                                     |
| `pixel`       | █ ░              | Bitmap font glyphs (3x5 at height 5, 5x7 at height 7)   |

### Character Support

The component supports the full ASCII character set with best-effort 7-segment approximations:
- **Digits**: 0-9
- **Letters**: A-Z (uppercase and lowercase)
- **Special**: : . , - _ = " ' [ ] ( ) / \ ? ! + * # % & @ ^ < > |
- **Accented**: Full ISO 8859-1 (À-Ö, Ø-ß, etc.) in both 3x5 and 5x7 pixel fonts; Å Ä Ö É È in 7-segment renderers

### 7-Segment Layout

```
   aaaa
  f    b
  f    b
   gggg
  e    c
  e    c
   dddd
```

### Methods

```typescript
const display = document.getElementById('myDisplay');
display.setValue('12:34');  // Set value programmatically
display.getValue();         // Get current value
```

### Visual Example

**"42" with box-drawing renderer (5-row):**
```
      ━━━━
     ┃     ┃
 ━━━━ ━━━━
┃        ┃
 ━━━━
```

## Image Component

The `<img>` component displays images in the terminal using sextant characters (2x3 pixels per cell).

### Usage

```xml
<!-- Fixed dimensions -->
<img src="media/image.png" width="30" height="15" />

<!-- Percentage dimensions (responsive) -->
<img src="media/image.png" width="100%" height="10" />

<!-- With object-fit mode -->
<img src="media/image.png" width="40" height="20" style="object-fit: contain;" />

<!-- Data URL (inline base64-encoded image) -->
<img src="data:image/png;base64,iVBORw0KGgo..." width="20" height="10" />
```

### Props

| Prop             | Type             | Default | Description                                                              |
|------------------|------------------|---------|--------------------------------------------------------------------------|
| `src`            | string           | -       | Image source path or data URL (required)                                 |
| `alt`            | string           | -       | Alternative text for accessibility                                       |
| `width`          | number \| string | 30      | Width in columns or percentage (e.g., "50%")                             |
| `height`         | number \| string | 15      | Height in rows or percentage (e.g., "50%")                               |
| `dither`         | string           | 'auto'  | Dithering mode for limited-color themes                                  |
| `ditherBits`     | number           | -       | Color depth for dithering (1-8)                                          |
| `onLoad`         | function         | -       | Called when image loads successfully                                     |
| `onError`        | function         | -       | Called when image fails to load                                          |
| `onShader`       | function/array   | -       | Per-pixel shader callback or pipeline array (see Shaders section)        |
| `onFilter`       | function/array   | -       | One-time filter callback or pipeline, runs once on image load            |
| `shaderFps`      | number           | 30      | Shader frame rate                                                        |
| `shaderRunTime`  | number           | -       | Stop shader after this many ms, freeze final frame as image              |

### Styles

| Style            | Type             | Default | Description                                                              |
|------------------|------------------|---------|--------------------------------------------------------------------------|
| `object-fit`     | string           | 'fill'  | How image fits: 'contain' (preserve aspect), 'fill' (stretch), 'cover' (crop) |

### Methods

| Method            | Description                                                          |
|-------------------|----------------------------------------------------------------------|
| `setSrc(url)`     | Load image immediately (async, last call wins if called rapidly)     |
| `setSource(url)`  | Set props.src and clear existing image (loads during next render)    |
| `clearImage()`    | Clear the loaded image                                               |
| `loadImage(url)`  | Low-level async load (same as setSrc)                                |
| `refreshImage()`  | Re-render the loaded image (e.g., after resize)                      |
| `setSize(w, h)`   | Resize canvas buffer (updates internal size, not props)              |

```typescript
// Preferred: setSrc loads immediately
const img = $melker.getElementById('my-image');
await img.setSrc('https://example.com/image.png');  // or file path or data URL
```

### Shaders

The `<img>` and `<canvas>` components support per-pixel shader callbacks for animated effects. **Prefer `<img>` over `<canvas>`** for shaders - images scale better on resize.

```xml
<img
  src="image.png"
  width="100%"
  height="100%"
  onShader="$app.myShader"
  shaderFps="30"
  shaderRunTime="5000"
/>
```

The shader callback receives:
- `x, y`: Pixel coordinates
- `time`: Elapsed time in seconds
- `resolution`: `{ width, height, pixelAspect }` - pixelAspect is ~0.5 (pixels are taller than wide)
- `source`: Image accessor with `getPixel(x, y)`, `mouse`, `mouseUV`
- `utils`: Built-in functions: `noise2d`, `simplex2d`, `simplex3d`, `perlin2d`, `perlin3d`, `fbm`, `fbm3d`, `palette`, `smoothstep`, `mix`, `fract`

Returns `[r, g, b]` or `[r, g, b, a]` (0-255 range).

**Shader Pipeline**: `onShader` accepts a single callback or an array of callbacks executed in sequence. Each stage reads from the previous stage's output. Array items may be `null`/`undefined` (skipped), enabling fixed-slot composition:

```javascript
// Fixed-slot pipeline: [mood, overlay, fisheye]. Null slots are no-ops.
const pipeline = [null, null, null];
pipeline[0] = $melker.shaderEffects.rain();  // slot in/out by setting to null
pipeline[2] = $melker.shaderEffects.fisheye();
imgEl.props.onShader = pipeline;
```

**Built-in Shader Effects**: Available via `$melker.shaderEffects`. Each is a factory function that returns a shader callback with optional configuration:

| Effect        | Description                               | Key Options                          |
|---------------|-------------------------------------------|--------------------------------------|
| `fisheye()`   | Mouse-following magnification lens        | `radius`, `zoom`, `exponent`         |
| `lightning()`| Jagged bolt with sky flash                 | `boltDuration`, `flashIntensity`     |
| `rain()`      | Vertical streaks with overcast dimming     | `dim`, `speed`                       |
| `bloom()`     | Warm glow pulsing from bright areas        | `threshold`, `strength`              |
| `sunrays()`   | Radial golden beams from upper corner      | `intensity`, `speed`                 |
| `glitch()`    | Digital distortion, scanlines              | `period`, `displacement`             |
| `fog()`       | Drifting misty overlay                     | `density`                            |
| `fire()`      | Warm flickering glow from below            | `intensity`, `flickerSpeed`          |
| `underwater()`| Blue-green tint with caustic ripples       | `depthFade`                          |
| `snow()`      | Drifting flakes with cold blue tint        | `layers`, `speed`                    |
| `darkness()`  | Heavy vignette, low visibility             | `radius`, `fadeWidth`                |
| `sandstorm()` | Horizontal streaks with orange haze        | `windSpeed`, `haze`                  |
| `magic()`     | Colored splash distortions drifting upward | `distortion`, `driftSpeed`           |
| `heat()`      | Rising shimmer/haze distortion             | `shimmer`, `warmth`                  |

```xml
<!-- Use directly in templates — no script needed -->
<img src="photo.png" onShader="$melker.shaderEffects.rain()" shaderFps="12" />
<!-- Or with custom options -->
<img src="photo.png" onShader="$melker.shaderEffects.rain({speed: 60, dim: 0.7})" shaderFps="12" />
```

**onFilter**: Same signature as `onShader` but runs once when the image loads (time is always 0). Also accepts pipeline arrays. Does **not** require `"shader": true` permission.

```xml
<img src="photo.png" onFilter="$app.grayscale" />
```

**Mouse tracking**: Automatic for elements with `onShader`. The `source.mouse` (pixel coords) and `source.mouseUV` (normalized 0-1) update automatically as the mouse moves over the element. Values are -1 when mouse is not over the element.

**Aspect-correct circles/shapes**: Divide y by `pixelAspect`:
```typescript
const dist = Math.sqrt(dx*dx + (dy/resolution.pixelAspect)**2);
```

**shaderRunTime**: When set, the shader stops after the specified milliseconds and the final frame is preserved as a static image that supports resize.

**Permission**: `onShader` requires `"shader": true` in the app's policy. `onFilter` does not require shader permission.

### Behavior

- Extends `CanvasElement` - inherits all canvas rendering capabilities
- Supports percentage dimensions for responsive sizing (recalculates on container resize)
- Pre-blends semi-transparent pixels with black background during image load
- `object-fit: fill` stretches to fill dimensions (default, like HTML img)
- `object-fit: contain` preserves aspect ratio within bounds
- `object-fit: cover` fills dimensions, cropping as needed
- `dither: 'auto'` applies appropriate dithering based on theme (sierra-stable for B&W/color, none for fullcolor)
- Fixed-dimension images use `flexShrink: 0` to prevent layout compression
- Percentage-dimension images are responsive and shrink with container

### Supported Formats

- **PNG** - Full support including alpha transparency, 16-bit depth, sub-byte indexed color (1/2/4-bit palette), and sub-byte grayscale (fast-png)
- **JPEG** - Full support (jpeg-js)
- **GIF** - Full support including animated GIF playback (omggif). Disable animation with `--no-animate-gif` or `MELKER_NO_ANIMATE_GIF=1`
- **WebP** - Full support via WASM decoder (@jsquash/webp)

PNG, JPEG, and GIF decoders are pure JavaScript. WebP uses a WASM decoder (libwebp). All dependencies are centralized in `src/deps.ts`. For indexed PNGs with bit depth < 8 (common in map tiles), `decodePngImage` unpacks the packed bit data before palette expansion.

### Path Resolution

- Data URLs (`data:image/png;base64,...`) are decoded directly (no file access)
- Absolute paths (starting with `/`) are used as-is
- HTTP/HTTPS URLs are fetched directly
- Relative paths are resolved from the .melker file's directory

### Implementation

Located in `src/components/img.ts`. Subclass of `CanvasElement` that provides an HTML-like API for image display. Image decoding is in `src/components/canvas-image.ts`, with decoders imported from `src/deps.ts`.

## Video Component

Video playback component using FFmpeg for decoding. Extends Canvas.

### Basic Usage

```xml
<video
  src="video.mp4"
  width="80"
  height="40"
  autoplay="true"
/>
```

### Supported Sources

| Source Type  | Example                                       | Notes                                    |
|--------------|-----------------------------------------------|------------------------------------------|
| Local files  | `src="video.mp4"`                             | Relative or absolute paths               |
| HTTP/HTTPS   | `src="https://example.com/video.mp4"`         | Requires `net` permission                |
| RTSP streams | `src="rtsp://192.168.1.191:8080/h264.sdp"`    | Live streaming, requires `net` permission |

### RTSP Streaming Example

```bash
./melker.ts examples/canvas/video/video-demo.melker rtsp://192.168.1.191:8080/h264.sdp
```

RTSP streams work with all themes and graphics modes:
- Themes: `fullcolor-dark`, `gray-std`, `bw-std`, etc.
- Graphics modes: `sextant` (default), `block`, `pattern`, `luma`

### Props

| Prop       | Type    | Description                                              |
|------------|---------|----------------------------------------------------------|
| `src`      | string  | Video source (file path, URL, or RTSP URL)               |
| `width`    | number  | Pixel buffer width                                       |
| `height`   | number  | Pixel buffer height                                      |
| `autoplay` | boolean | Start playing immediately                                |
| `loop`     | boolean | Loop playback                                            |
| `muted`    | boolean | Mute audio                                               |
| `dither`   | string  | Dithering algorithm (`auto`, `none`, `sierra-stable`, etc.) |

### Implementation

Located in `src/components/video.ts`. Uses FFmpeg (`src/video/ffmpeg.ts`) for decoding. Video playback is automatically stopped when the engine exits to prevent render-after-cleanup issues.

## Rendering Pipeline

1. **Layout calculation** → LayoutNode tree
2. **Store bounds** → `_currentLayoutContext` map (id → LayoutNode)
3. **Render pass** → Write to dual buffer
4. **Modal pass** → Render dialogs last
5. **Selection pass** → Apply text selection highlighting
6. **Buffer diff** → Output ANSI sequences

## Opacity

All elements support `opacity` and `background-opacity` CSS properties for transparency effects.

| CSS Property         | Style Key          | Range | Default | Description                                |
|----------------------|--------------------|-------|---------|--------------------------------------------|
| `opacity`            | `opacity`          | 0–1   | 1       | Blends both foreground and background      |
| `background-opacity` | `backgroundOpacity`| 0–1   | 1       | Blends background only (text stays solid)  |

Accepts numbers (`0.5`) or percentages (`50%`). Values below `0.05` skip rendering entirely. Opacity inherits and multiplies with parent (parent 0.5 × child 0.5 = 0.25). Works on all element types including canvas, img, text, containers, and borders.

```xml
<container style="opacity: 0.5; background-opacity: 0.3;">
  <text>Semi-transparent background, half-opacity text</text>
</container>
```

Animatable via `@keyframes` and CSS transitions. See [opacity-support.md](opacity-support.md) for implementation details.

## Style Inheritance

Only these properties inherit from parent:
- `color`
- `backgroundColor`
- `fontWeight`
- `fontStyle`
- `textDecoration`
- `dim`
- `reverse`
- `borderColor`
- `opacity` (multiplies with parent value)

`backgroundOpacity` does **not** inherit.

Layout properties (padding, margin, border widths) do NOT inherit.

## Command Palette Integration

Interactive elements are auto-discovered and surfaced in the command palette (Ctrl+K). The palette can be dragged by its title bar and resets to center when reopened. Qualifying types: `button`, `checkbox`, `radio`, `input`, `textarea`, `slider`, `select`, `combobox`, `tab`, `data-table`, `data-tree`.

When selected from the palette, each element performs its natural action: buttons/checkboxes/radios trigger `onClick`, inputs/textareas/sliders receive focus, selects/comboboxes get focus + open, tabs switch `activeTab`.

### Props

All elements support these props for palette customization:

| Prop               | Type              | Default   | Description                                    |
|--------------------|-------------------|-----------|------------------------------------------------|
| `palette`          | `boolean\|string` | `true`*   | `false` to exclude; string to set custom label |
| `palette-shortcut` | `string`          | undefined | Global keyboard shortcut (e.g., `"Ctrl+S"`)   |
| `palette-group`    | `string`          | auto      | Override default group name                    |

\* Default is `true` for qualifying element types, `undefined` (excluded) for non-qualifying types.

### Examples

```html
<!-- Auto-included with label "Submit" -->
<button label="Submit" onClick=${submit}>

<!-- Excluded from palette -->
<button label="x" palette={false} onClick=${closePanel}>

<!-- Custom label and shortcut -->
<button label="Save" palette="Save Document" palette-shortcut="Ctrl+S" onClick=${save}>

<!-- Input with shortcut to jump to it -->
<input placeholder="Search..." palette-shortcut="Ctrl+F">
```

### Implementation Files

| File                                | Purpose                                       |
|-------------------------------------|-----------------------------------------------|
| `src/command-palette-components.ts` | Element discovery, label resolution, shortcuts |
| `src/engine-system-palette.ts`      | Injection into command palette elements        |

## Command Element

The `<command>` element is a non-visual, declarative replacement for `onKeyPress` switch blocks. Each command binds a keyboard shortcut to an action with structured metadata (key, label, callback), making shortcuts discoverable by the command palette and AI accessibility dialog.

### Usage

```xml
<!-- Focus-scoped commands (fire when parent or descendants have focus) -->
<container id="editor" style="border: thin; padding: 1;">
  <command key="n" label="New File" onExecute="$app.newFile()" />
  <command key="Delete,Backspace" label="Delete" onExecute="$app.del()" />
  <text>Editor content</text>
</container>

<!-- Global command (fires regardless of focus) -->
<command key="Ctrl+S" label="Save" global onExecute="$app.save()" />

<!-- Disabled command -->
<command key="x" label="Danger" disabled onExecute="$app.danger()" />
```

### Props

| Prop        | Type     | Default | Description                                          |
|-------------|----------|---------|------------------------------------------------------|
| `key`       | string   | -       | Keyboard shortcut, comma-separated for multiple keys |
| `label`     | string   | -       | Human-readable command name (shown in palette)       |
| `onExecute` | handler  | -       | Callback when command is triggered                   |
| `group`     | string   | 'Commands' | Palette group name                                |
| `global`    | boolean  | false   | Promote to global shortcut (fires regardless of focus) |
| `disabled`  | boolean  | false   | Temporarily disable the command                      |

### Key Format

The `key` prop supports comma-separated values for multiple bindings:

| Value                  | Meaning                                  |
|------------------------|------------------------------------------|
| `"n"`                  | Single key                               |
| `"Delete,Backspace"`   | Either Delete or Backspace               |
| `"Ctrl+S"`             | Modifier combination                     |
| `","`                  | Literal comma key                        |
| `"a,comma"`            | `a` key or comma key                     |
| `"1"`                  | Numeral key                              |
| `"Space"`              | Space key (alias for `" "`)              |
| `"+"`                  | Plus key                                 |
| `"plus"`               | Alias for plus (avoids modifier ambiguity) |

Letter keys are **case-insensitive**: `"p"`, `"P"`, and `"Shift+P"` all match the same keystroke. Use modifiers (`Ctrl+p`, `Alt+p`) to differentiate. Shift is preserved for non-letter keys (`Shift+ArrowUp`, `Shift+Tab`).

### Behavior

- **Non-visual**: Default style `display: 'none'`, skipped by layout
- **Focus-scoped**: By default, fires when the parent container (or any descendant) has focus. Innermost matching command wins when nested.
- **Global**: With `global` prop, fires via the palette shortcut map (priority 4 in keyboard dispatch). Suppressed when an overlay is open (dialogs, command palette, dev tools) or when the focused element consumes keys (input/textarea, slider, data-table, etc.). Modifier combos (Ctrl+S) pass through focused-element suppression but not overlay suppression.
- **Implicit focusability**: Containers with non-global, non-disabled `<command>` children automatically become focusable via both keyboard (Tab/arrow) and mouse click. Clicking a container's background focuses it; clicking a focusable child (e.g., a button) inside it focuses the child instead.
- **Focus indicator**: Focused command containers show a `*` marker in the upper-right corner and, if bordered, a focus-colored border
- **Palette integration**: All commands appear as a single entry in the command palette, with the original `key` string shown as a hint (e.g., `ArrowLeft, a`). Each key is registered individually in the shortcut map for keyboard dispatch.

### Comparison with Other Mechanisms

| Mechanism              | Scope        | Purpose                                              |
|------------------------|--------------|------------------------------------------------------|
| `palette-shortcut`     | Global       | Shortcut that clicks/focuses an interactive element  |
| `<command>`            | Focus-scoped | Declarative shortcut with custom action              |
| `<command global>`     | Global       | Like `palette-shortcut` but with custom `onExecute`  |
| `onKeyPress`           | Focus-scoped | Opaque callback (not discoverable by palette or AI)  |

### Implementation Files

| File                                | Purpose                                           |
|-------------------------------------|---------------------------------------------------|
| `src/components/command.ts`         | CommandElement class, registration, schema         |
| `src/command-palette-components.ts` | Discovery, key parsing, shortcut map               |
| `src/engine-keyboard-handler.ts`    | Focus-scoped command matching (`findMatchingCommand`) |
| `src/focus-navigation-handler.ts`   | Implicit focusability for command containers       |
| `src/hit-test.ts`                   | `isCommandContainer()` for mouse-click focus       |
| `src/element-click-handler.ts`      | Focuses command containers on click                |
| `src/rendering.ts`                  | Focus indicator (`*` marker, border color)         |

### Example

See [`examples/basics/command-test.melker`](../examples/basics/command-test.melker) for a working demo with multiple panels, scoped and global commands.

## Key Files Reference

| File                      | Responsibility                               |
|---------------------------|----------------------------------------------|
| `src/types.ts`            | Core interfaces: Element, Style, Bounds, Props |
| `src/element.ts`          | createElement, component registry            |
| `src/document.ts`         | Document class, element registry, focus      |
| `src/layout.ts`           | LayoutEngine, flexbox algorithm              |
| `src/sizing.ts`           | SizingModel, box model calculations          |
| `src/rendering.ts`        | RenderingEngine, layout-to-buffer            |
| `src/viewport.ts`         | ViewportManager, scrolling support           |
| `src/viewport-buffer.ts`  | ViewportBufferProxy, ViewportDualBuffer      |
| `src/content-measurer.ts` | ContentMeasurer, intrinsic size calculation  |
| `src/focus.ts`            | FocusManager, tab navigation                 |
| `src/events.ts`           | EventManager, event dispatching              |

## See Also

- [architecture.md](architecture.md) — Core architecture, layout engine
- [filterable-list-architecture.md](filterable-list-architecture.md) — Combobox, select, autocomplete internals
- [file-browser-architecture.md](file-browser-architecture.md) — File browser internals
- [data-table.md](data-table.md) — Data table component
- [data-bars.md](data-bars.md) — Bar chart components
- [data-boxplot-architecture.md](data-boxplot-architecture.md) — Box-and-whisker plots
- [data-heatmap-architecture.md](data-heatmap-architecture.md) — Heatmap with isolines
- [data-tree-architecture.md](data-tree-architecture.md) — Tree view component
- [selection-id-architecture.md](selection-id-architecture.md) — Cross-component selection sync via string IDs
- [bind-selection-architecture.md](bind-selection-architecture.md) — Automatic selection sync via `bind:selection`
- [spinner-architecture.md](spinner-architecture.md) — Spinner component internals
- [toast-architecture.md](toast-architecture.md) — Toast notification system
- [tooltip-architecture.md](tooltip-architecture.md) — Tooltip system
