# Composure Programming Guide

A comprehensive guide to building user interfaces with Composure, a small ECMAScript-native layout runtime for the web.

## Table of Contents

1. [Introduction](#introduction)
2. [Getting Started](#getting-started)
3. [Core Concepts](#core-concepts)
4. [Building Layouts](#building-layouts)
5. [Styling and Appearance](#styling-and-appearance)
6. [Event Handling](#event-handling)
7. [Custom Layouts](#custom-layouts)
8. [Decoration and SVG Overlays](#decoration-and-svg-overlays)
9. [Virtualized Lists](#virtualized-lists)
10. [Updating the UI](#updating-the-ui)
11. [Debugging](#debugging)
12. [API Reference](#api-reference)
13. [Examples](#examples)

---

## Introduction

Composure is a retained UI tree system that runs explicit layout passes and outputs real, semantic DOM elements. Unlike CSS-based layout, Composure enforces a simple rule: **parents compose children**.

### Key Principles

- **Local ownership**: Each parent explicitly measures and places its direct children
- **Explicit phases**: Build → Layout → Render
- **Bounded iteration**: At most one refinement pass
- **Real DOM output**: Preserves accessibility, native text rendering, and browser events

### What Composure Is Not

Composure is not a full framework like React or Vue. It does not include:
- State management
- Component lifecycle hooks
- Virtual DOM diffing
- Routing
- Build tooling

It focuses solely on layout and rendering, letting you handle application state however you prefer.

---

## Getting Started

### Installation

Composure is a source library with no npm package. Copy the source files into your project:

```
your-project/
├── composure/
│   ├── composure.js    # Core runtime (required)
│   ├── components.js   # vlist, thumbnail (optional)
│   └── debug.js        # Debug overlay (optional)
└── app.js
```

### Basic HTML Setup

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body, #root { width: 100%; height: 100%; }
    body { font-family: system-ui, sans-serif; }
  </style>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="app.js"></script>
</body>
</html>
```

### Hello World

```javascript
import { vstack, text, mount } from './composure/composure.js'

const root = vstack()
  .pad(20)
  .add(
    text('Hello, Composure!', 'h1')
      .style({ fontSize: 24, fontWeight: '700' })
  )

mount(root, document.getElementById('root'))
```

---

## Core Concepts

### The Node Tree

Every UI element in Composure is a `Node`. Nodes form a tree where:
- Parents contain children
- Children describe their intrinsic size
- Parents decide where children are placed

### Constructor Functions

Composure provides five constructor functions:

| Function | Purpose |
|----------|---------|
| `vstack(tag?)` | Vertical stack layout |
| `hstack(tag?)` | Horizontal stack layout |
| `zstack(tag?)` | Overlay/absolute positioning |
| `box(tag?)` | Generic container (no layout) |
| `text(content, tag?)` | Text content |

All functions accept an optional HTML tag (defaults to `'div'` or `'span'` for text).

### Frames

Every node has a `frame` property: `{ x, y, width, height }`.

- Frames are set by the parent during layout
- Children cannot modify their own frame
- Frames determine absolute positioning in the DOM

### Constraints

During measurement, parents pass constraints: `{ maxWidth, maxHeight }`.

- Children measure themselves within these bounds
- Children return `{ width, height }` indicating their actual size
- The returned size does not guarantee the frame size (parent decides)

---

## Building Layouts

### Vertical Stacks (vstack)

Stack children from top to bottom:

```javascript
import { vstack, text } from './composure/composure.js'

const menu = vstack()
  .gap(8)
  .add(
    text('Home'),
    text('About'),
    text('Contact')
  )
```

### Horizontal Stacks (hstack)

Stack children from left to right:

```javascript
import { hstack, box } from './composure/composure.js'

const toolbar = hstack()
  .gap(12)
  .add(
    box('button').add(text('Save')),
    box('button').add(text('Load')),
    box('button').add(text('Export'))
  )
```

### Overlay Stacks (zstack)

Position children on top of each other:

```javascript
import { zstack, box, text } from './composure/composure.js'

const card = zstack()
  .add(
    box().style({ background: '#f0f0f0' }),  // Background layer
    text('Overlay Content').pad(16)          // Content layer
  )
```

### Alignment

Control how children align on the cross-axis:

```javascript
vstack()
  .align('center')  // Center children horizontally
  .add(child1, child2)

hstack()
  .align('end')     // Align children to bottom
  .add(child1, child2)

zstack()
  .align('stretch') // Stretch children to fill (default for zstack)
  .add(child1, child2)
```

Alignment options: `'start'`, `'center'`, `'end'`, `'stretch'`

### Gap Spacing

Add space between children:

```javascript
vstack()
  .gap(16)  // 16px between each child
  .add(child1, child2, child3)
```

### Padding

Add internal spacing around content:

```javascript
// Uniform padding
box().pad(20)

// Per-side padding
box().pad({ top: 10, right: 20, bottom: 10, left: 20 })

// Partial specification (unspecified sides default to 0)
box().pad({ top: 16, bottom: 16 })
```

### Size Hints

Suggest intrinsic dimensions:

```javascript
// Fixed size
box().size(200, 100)

// Width only (height determined by content)
box().size(300, null)

// Height only (width determined by parent)
box().size(null, 50)
```

**Important**: `.size()` provides hints to the layout engine. The parent ultimately decides the final frame dimensions.

### Flexible Space Distribution

Use `.grow()` to distribute remaining space proportionally in stacks:

```javascript
hstack().add(
  box().size(100, null),  // Fixed 100px
  box().grow(1),          // Takes 1/3 of remaining
  box().grow(2),          // Takes 2/3 of remaining
)

vstack().add(
  text('Header'),         // Fixed height
  box().grow(1),          // Fills remaining space
  text('Footer'),         // Fixed height
)
```

**How it works:**
1. Non-growing children are measured first
2. Remaining space is distributed proportionally by grow factor
3. Growing children are then measured within their allocated space

**Note:** `.grow()` only works in `vstack` and `hstack`. There is no shrink factor—this keeps the model simple.

---

## Styling and Appearance

### Visual Styles

Apply CSS visual properties with `.style()`:

```javascript
box()
  .style({
    background: '#3b82f6',
    color: '#ffffff',
    borderRadius: 8,
    boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
    opacity: 0.9
  })
```

### Layout Properties are Rejected

Composure silently ignores layout-related CSS properties because the runtime owns positioning:

```javascript
// These properties are ignored:
box().style({
  position: 'relative',  // Ignored - Composure uses absolute
  width: '100%',         // Ignored - use .size()
  height: 'auto',        // Ignored - use .size()
  margin: 10,            // Ignored - use parent .gap() or custom layout
  display: 'flex',       // Ignored - use vstack/hstack
  padding: 20            // Ignored - use .pad()
})
```

### HTML Attributes

Set attributes with `.attr()`:

```javascript
box('input')
  .attr('type', 'text')
  .attr('placeholder', 'Enter name...')
  .attr('id', 'name-input')
```

### ARIA Roles

Set accessibility roles with `.role()`:

```javascript
box('div')
  .role('button')
  .attr('tabindex', '0')
```

### Compositing Hints

Optimize rendering with `.hint()`:

```javascript
// Promote to own layer for animations
box().hint({ willChange: 'transform' })

// Enable CSS containment
box().hint({ contain: 'layout paint' })
```

---

## Event Handling

### Binding Events

Attach DOM event handlers with `.on()`:

```javascript
box('button')
  .add(text('Click me'))
  .on('click', (e) => {
    console.log('Clicked!', e)
  })
```

### Supported Events

Any valid DOM event works:

```javascript
box('input')
  .on('input', (e) => handleInput(e.target.value))
  .on('keydown', (e) => {
    if (e.key === 'Enter') submit()
  })
  .on('focus', () => console.log('Focused'))
  .on('blur', () => console.log('Blurred'))
```

### Replacing Handlers

Calling `.on()` again replaces the handler without re-binding the DOM listener:

```javascript
// Initial handler
node.on('click', handlerA)

// Later, replace it
node.on('click', handlerB)  // DOM listener unchanged, just calls handlerB now
```

### Removing Handlers

Use `.off()` to remove a handler:

```javascript
node.off('click')  // Remove click handler
```

Or pass `null` to disable without removing:

```javascript
node.on('click', null)  // Click events now ignored
```

---

## Custom Layouts

For layouts beyond vstack/hstack/zstack, define a custom layout function.

### Layout Function Signature

```javascript
node.layout((children, constraints, measure) => {
  // children: Array of child nodes
  // constraints: { maxWidth, maxHeight }
  // measure: (child, constraints) => { width, height }

  // 1. Measure each child
  // 2. Set each child's frame: { x, y, width, height }
  // 3. Return this node's size: { width, height }

  return { width, height }
})
```

### Example: Two-Column Layout

```javascript
box().layout((children, constraints, measure) => {
  const gap = 16
  const colWidth = (constraints.maxWidth - gap) / 2

  // Measure both columns
  const leftSize = measure(children[0], {
    maxWidth: colWidth,
    maxHeight: constraints.maxHeight
  })
  const rightSize = measure(children[1], {
    maxWidth: colWidth,
    maxHeight: constraints.maxHeight
  })

  // Place columns side by side
  const height = Math.max(leftSize.height, rightSize.height)

  children[0].frame = { x: 0, y: 0, width: colWidth, height }
  children[1].frame = { x: colWidth + gap, y: 0, width: colWidth, height }

  return { width: constraints.maxWidth, height }
})
```

### Example: Responsive Grid

```javascript
function responsiveGrid(breakpoints, gap = 16) {
  return (children, constraints, measure) => {
    // Determine column count from breakpoints
    let columns = 1
    for (const [minWidth, cols] of breakpoints) {
      if (constraints.maxWidth >= minWidth) {
        columns = cols
        break
      }
    }

    const colWidth = (constraints.maxWidth - gap * (columns - 1)) / columns
    let y = 0
    let rowStart = 0

    while (rowStart < children.length) {
      const rowEnd = Math.min(rowStart + columns, children.length)
      let rowHeight = 0

      // Measure row items
      for (let i = rowStart; i < rowEnd; i++) {
        const col = i - rowStart
        const size = measure(children[i], {
          maxWidth: colWidth,
          maxHeight: constraints.maxHeight - y
        })
        children[i].frame = {
          x: col * (colWidth + gap),
          y,
          width: colWidth,
          height: size.height
        }
        rowHeight = Math.max(rowHeight, size.height)
      }

      // Equalize row heights
      for (let i = rowStart; i < rowEnd; i++) {
        children[i].frame.height = rowHeight
      }

      y += rowHeight + gap
      rowStart = rowEnd
    }

    return { width: constraints.maxWidth, height: Math.max(0, y - gap) }
  }
}

// Usage
const grid = box()
  .layout(responsiveGrid([
    [1200, 4],  // 4 columns at 1200px+
    [768, 3],   // 3 columns at 768px+
    [0, 2]      // 2 columns below 768px
  ]))
  .add(...items)
```

### Example: Chat Bubble with Avatar

```javascript
function chatBubbleLayout(isFromMe) {
  return (children, constraints, measure) => {
    const avatarChild = children[0]
    const bubbleChild = children[1]
    const gap = 8
    const maxBubbleWidth = constraints.maxWidth * 0.72

    const avatarSize = measure(avatarChild, constraints)
    const bubbleSize = measure(bubbleChild, {
      maxWidth: maxBubbleWidth - avatarSize.width - gap,
      maxHeight: constraints.maxHeight
    })

    const height = Math.max(avatarSize.height, bubbleSize.height)

    if (isFromMe) {
      // Avatar on right
      bubbleChild.frame = {
        x: constraints.maxWidth - avatarSize.width - gap - bubbleSize.width,
        y: 0,
        width: bubbleSize.width,
        height: bubbleSize.height
      }
      avatarChild.frame = {
        x: constraints.maxWidth - avatarSize.width,
        y: height - avatarSize.height,
        width: avatarSize.width,
        height: avatarSize.height
      }
    } else {
      // Avatar on left
      avatarChild.frame = {
        x: 0,
        y: height - avatarSize.height,
        width: avatarSize.width,
        height: avatarSize.height
      }
      bubbleChild.frame = {
        x: avatarSize.width + gap,
        y: 0,
        width: bubbleSize.width,
        height: bubbleSize.height
      }
    }

    return { width: constraints.maxWidth, height }
  }
}
```

---

## Decoration and SVG Overlays

Decoration lets you draw SVG graphics over nodes after layout is complete.

### Basic Decoration

```javascript
box()
  .size(100, 100)
  .decorate(({ svg, frame }) => {
    svg.rect()
      .x(0).y(0)
      .width(frame.width)
      .height(frame.height)
      .fill('none')
      .stroke('#3b82f6')
      .strokeWidth(2)
  })
```

### Decoration Callback Parameters

```javascript
node.decorate(({ el, svg, frame, children }) => {
  // el: The actual DOM element
  // svg: SVG builder for drawing shapes
  // frame: { x, y, width, height } of this node
  // children: Array of child nodes with their frames
})
```

### SVG Builder Methods

| Method | Creates | Chain Methods |
|--------|---------|---------------|
| `svg.rect()` | Rectangle | `.x()`, `.y()`, `.width()`, `.height()` |
| `svg.circle()` | Circle | `.cx()`, `.cy()`, `.r()` |
| `svg.ellipse()` | Ellipse | `.cx()`, `.cy()`, `.rx()`, `.ry()` |
| `svg.line()` | Line | `.x1()`, `.y1()`, `.x2()`, `.y2()` |
| `svg.path()` | Path | `.d()` (path data string) |
| `svg.group()` | Group | `.add()` (nest elements) |

All SVG elements support:
- `.fill(color)`
- `.stroke(color)`
- `.strokeWidth(width)`
- `.strokeLinecap(style)`
- `.opacity(value)`
- `.transform(value)`
- `.attr(key, value)`

### Example: Rounded Chat Bubble Tail

```javascript
const bubble = box()
  .style({ background: '#0b93f6', borderRadius: 16 })
  .pad(12)
  .add(text('Hello!').style({ color: '#fff' }))
  .decorate(({ svg, frame }) => {
    const w = frame.width
    const h = frame.height

    // Draw tail shape
    svg.path()
      .d(`M${w - 4},${h - 10} Q${w + 4},${h + 2} ${w - 2},${h} Q${w - 6},${h - 1} ${w - 10},${h - 4} Z`)
      .fill('#0b93f6')
  })
```

### Decoration Layers

Control whether decoration renders behind or in front of children:

```javascript
// Behind children (default)
node.decorate(drawBackground, { layer: 'behind' })

// In front of children
node.decorate(drawOverlay, { layer: 'front' })
```

### DOM Access in Decoration

Use `el` for direct DOM manipulation:

```javascript
node.decorate(({ el }) => {
  // Custom DOM setup
  el.setAttribute('data-custom', 'value')

  // Load resources
  const img = new Image()
  img.onload = () => { /* ... */ }
  img.src = 'image.png'
})
```

---

## Animation

Composure provides two animation approaches:

1. **CSS Transitions** (`.transition()`) — Simple frame animation
2. **Animatable Values** (`animation.js`) — Programmatic, sequenced animations

### CSS Transitions (Simple)

Use `.transition()` to animate frame changes:

```javascript
const card = box()
  .transition({ duration: 300, easing: 'ease-out' })
  .size(100, 100)
  .style({ background: '#3b82f6' })

// Later, changing size will animate
card.size(200, 200)
app.update()  // Frame animates from 100×100 to 200×200
```

### Animatable Values (Advanced)

For complex, sequenced animations, use the animation module:

```javascript
import {
  animatable, spring,
  easeOutBack, easeOut,
  lerp, parallel, sequence
} from './composure/animation.js'
```

#### Creating Animatable Values

```javascript
// Create with initial value
const scale = animatable(0.6)

// Animate to target
await scale.animateTo(1, { duration: 300, easing: easeOutBack })

// Read current value
console.log(scale.value)  // 1
```

#### Sequencing Animations

```javascript
const foreground = animatable(0)

// Chain with delays and snaps
await foreground.delay(160)
await foreground.snapTo(0.8)  // Instant jump
await foreground.animateTo(1, { duration: 150, easing: easeOutBack })
```

#### Parallel Animations

```javascript
await parallel(
  scale.animateTo(1, { duration: 300 }),
  opacity.animateTo(1, { duration: 200 }),
  rotation.animateTo(0, { duration: 400 })
)
```

#### Spring Physics

```javascript
const position = spring(0, {
  stiffness: 170,  // Spring stiffness
  damping: 26,     // Damping ratio
  mass: 1          // Mass
})

await position.animateTo(100)  // Bouncy animation
```

#### Using with Decorations

```javascript
const lineProgress = animatable(0)
const avatarScale = animatable(0.6)

node.decorate(({ svg, frame }) => {
  // Use animated values to draw
  const progress = lineProgress.value

  const currentBottom = lerp(topY, bottomY, progress)

  svg.path()
    .d(`M${x1},${topY} L${x2},${topY} L${x2},${currentBottom} L${x1},${currentBottom} Z`)
    .fill('#000')
})

// Trigger updates when values change
lineProgress.onUpdate(() => app.update())

// Start animation
lineProgress.animateTo(1, { duration: 500, easing: easeOut })
```

#### Easing Functions

| Function | Description |
|----------|-------------|
| `linear` | Constant speed |
| `easeIn` | Start slow |
| `easeOut` | End slow |
| `easeInOut` | Start and end slow |
| `easeOutBack` | Overshoot then settle (bouncy) |
| `easeOutElastic` | Elastic bounce |
| `easeOutBounce` | Ball bounce effect |
| `cubicBezier(x1, y1, x2, y2)` | Custom curve |

#### Animation Utilities

```javascript
// Linear interpolation
lerp(0, 100, 0.5)  // 50

// Point interpolation
lerpPoint({ x: 0, y: 0 }, { x: 100, y: 100 }, 0.5)  // { x: 50, y: 50 }

// Staggered animations
await stagger([anim1, anim2, anim3], 1, { duration: 300 }, 50)
// Each starts 50ms after previous

// Repeat
await repeat(() => pulse.animateTo(1).then(() => pulse.animateTo(0)), 3)
```

#### Animation Controller

For coordinating multiple related animations:

```javascript
import { animationController } from './composure/animation.js'

const ctrl = animationController()

// Create named animatables
const bg = ctrl.create('backgroundScale', 0.6)
const fg = ctrl.create('foregroundScale', 0)
const line = ctrl.create('lineProgress', 0)

// Access by name
ctrl.valueOf('backgroundScale')  // 0.6

// Stop all
ctrl.stopAll()

// Reset all
ctrl.reset({ backgroundScale: 0.6, foregroundScale: 0, lineProgress: 0 })
```

### Transition Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `duration` | number | 200/300 | Animation duration in milliseconds |
| `easing` | string/function | 'ease-out' | Easing function |

### Disabling Transitions

Use `.noTransition()` to disable CSS transitions:

```javascript
card.noTransition()
card.size(50, 50)
app.update()  // Changes instantly
```

---

## Intrinsic Measurement

For advanced layouts, you may need to know a node's intrinsic size before placing it.

### measureIntrinsic

```javascript
import { measureIntrinsic } from './composure/composure.js'

// Get max-content size (natural width, no constraints)
const maxSize = measureIntrinsic(node, 'max')

// Get min-content size (minimum width that fits content)
const minSize = measureIntrinsic(node, 'min')
```

### Use Cases

```javascript
// Size a container to fit its longest text child
const items = ['Short', 'A much longer item', 'Medium']
const nodes = items.map(t => text(t))

let maxWidth = 0
for (const node of nodes) {
  const size = measureIntrinsic(node, 'max')
  maxWidth = Math.max(maxWidth, size.width)
}

const list = vstack()
  .size(maxWidth + 32, null)  // Fixed width to fit longest
  .pad(16)
  .add(...nodes)
```

**Note:** Intrinsic measurement temporarily marks the node dirty and clears its cache to ensure accurate results.

---

## Virtualized Lists

For large datasets, use `vlist` from `components.js` to render only visible items.

### Basic Usage

```javascript
import { vlist } from './composure/components.js'
import { text, mount } from './composure/composure.js'

const list = vlist({
  count: 10000,        // Total number of items
  itemHeight: 50,      // Fixed height per item
  render: (index) => {
    return text(`Item ${index}`)
      .style({ padding: 16 })
  }
})

mount(list, document.getElementById('root'))
```

### Configuration Options

```javascript
vlist({
  count: 10000,        // Required: total item count
  itemHeight: 80,      // Required: height of each item
  columns: 3,          // Optional: multi-column grid (default: 1)
  gap: 8,              // Optional: spacing between items (default: 0)
  overscan: 5,         // Optional: extra items to render outside viewport (default: 3)
  render: (index) => Node  // Required: function to create each item
})
```

### Lazy-Loading Images

Use `thumbnail` for efficient image loading in lists:

```javascript
import { vlist, thumbnail } from './composure/components.js'
import { vstack, text } from './composure/composure.js'

const gallery = vlist({
  count: images.length,
  itemHeight: 220,
  columns: 4,
  gap: 4,
  render: (index) => {
    const img = images[index]
    return vstack()
      .add(
        thumbnail(img.url, {
          placeholder: '#e5e5e5',  // Color while loading
          error: '#fafafa'          // Color on load error
        }).size(null, 170),
        text(img.title)
      )
  }
})
```

---

## Updating the UI

Composure uses a retained tree. You mutate the tree, then request an update.

### Mount Returns Update Function

```javascript
const { update, destroy } = mount(root, container)

// Later, after mutating the tree:
update()  // Re-run layout and re-render
```

### Font Loading

Custom fonts must be loaded before mounting to ensure correct text measurement. The `mount()` function supports a `fonts` option that waits for specified fonts before rendering:

```javascript
// Wait for fonts before rendering (returns a Promise)
mount(root, container, {
  fonts: ['900 16px "Optima Nova"', '400 14px "Inter"']
}).then(app => {
  // App is ready, fonts are loaded
  app.update()
})

// Or use async/await
const app = await mount(root, container, {
  fonts: ['900 16px "Optima Nova"']
})
```

For fonts that may load after mounting (e.g., fallback scenarios), enable auto re-layout:

```javascript
mount(root, container, {
  relayoutOnFontLoad: true  // Re-layout when any font finishes loading
}).then(app => {
  // ...
})
```

**Note**: Without the `fonts` option, `mount()` returns the app handle directly (not a Promise).

### Tree Mutation Methods

```javascript
// Add children
node.add(child1, child2)

// Remove a specific child
node.remove(child)

// Remove all children
node.clear()
```

### Example: Dynamic List

```javascript
import { vstack, text, box, mount } from './composure/composure.js'

const items = []
const list = vstack().gap(8)

const addButton = box('button')
  .add(text('Add Item'))
  .on('click', () => {
    const item = text(`Item ${items.length + 1}`)
    items.push(item)
    list.add(item)
    app.update()
  })

const root = vstack()
  .pad(20)
  .gap(16)
  .add(addButton, list)

const app = mount(root, document.getElementById('root'))
```

### Example: Updating Text Content

Use the `.text()` method to update text content:

```javascript
let count = 0
const display = text('0', 'span')

box('button')
  .add(text('Increment'))
  .on('click', () => {
    count++
    display.text(count.toString())  // Update text content
    app.update()
  })
```

### Dirty Tracking

Composure automatically tracks which nodes need re-layout. When you modify a node:

- Changing `.size()`, `.pad()`, `.gap()`, `.align()`, `.style()`, `.text()` marks the node and ancestors dirty
- Calling `.add()`, `.remove()`, `.clear()` marks the parent dirty
- Only dirty nodes are re-measured on `update()`

For manual invalidation, use `.dirty()`:

```javascript
node.dirty()  // Force re-layout of this node and ancestors
app.update()
```

### Keyed Nodes

Use `.key()` to maintain stable identity across updates:

```javascript
function renderItem(item) {
  return box()
    .key(item.id)  // Stable identity
    .add(text(item.name))
}
```

Keys are especially important for:
- Items in virtualized lists
- Reorderable lists
- Items that animate in/out

---

## Debugging

### Visual Debug Overlay

Enable colored outlines showing layout structure:

```javascript
import { enableDebug, disableDebug } from './composure/debug.js'

// Turn on
enableDebug(rootNode)

// Turn off
disableDebug(rootNode)

// With options
enableDebug(rootNode, {
  showDirty: true,     // Show dirty state in labels
  showMeasured: true   // Highlight nodes measured this pass
})
```

### Toggle on Keypress

```javascript
import { debugOnKey } from './composure/debug.js'

const cleanup = debugOnKey(rootNode, 'd')  // Press 'd' to toggle

// Later, to stop listening:
cleanup()
```

### Color Coding

| Node Type | Color |
|-----------|-------|
| vstack | Blue |
| hstack | Green |
| zstack | Amber |
| Custom layout | Purple |
| Leaf/box | Gray |
| text | Pink |
| Measured (when tracking) | Bright green |

### Console Logging

Enable detailed logging of the layout process:

```javascript
import {
  enableConstraintLogging,
  enableMeasureLogging,
  logTree
} from './composure/debug.js'

// Log constraints passed to each node during layout
enableConstraintLogging()

// Log when each node is measured
enableMeasureLogging()

// Print tree structure to console
logTree(rootNode)
```

### Measure Tracking

Track which nodes were measured on each update:

```javascript
import {
  enableMeasureTracking,
  debugResetTracking,
  getMeasuredNodes,
  flashMeasuredNodes
} from './composure/debug.js'

enableMeasureTracking()

// Before update, reset tracking
debugResetTracking()
app.update()

// Check what was measured
console.log('Measured:', getMeasuredNodes().size, 'nodes')

// Visual flash on measured nodes
flashMeasuredNodes(300)  // 300ms flash
```

### Export Tree as JSON

```javascript
import { exportTree } from './composure/composure.js'

const json = exportTree(rootNode)
console.log(JSON.stringify(json, null, 2))
```

### Reading Frame Values

After layout, inspect node frames:

```javascript
console.log(node.frame)  // { x: 0, y: 0, width: 200, height: 100 }
```

---

## API Reference

### Constructor Functions

| Function | Description |
|----------|-------------|
| `vstack(tag='div')` | Create vertical stack |
| `hstack(tag='div')` | Create horizontal stack |
| `zstack(tag='div')` | Create overlay stack |
| `box(tag='div')` | Create generic container |
| `text(content, tag='span')` | Create text node |

### Node Methods

#### Tree Mutation

| Method | Description |
|--------|-------------|
| `.add(...children)` | Add child nodes (chainable) |
| `.remove(child)` | Remove a child node |
| `.clear()` | Remove all children |

#### Layout Configuration

| Method | Description |
|--------|-------------|
| `.size(width, height)` | Set intrinsic size hints |
| `.layout(fn)` | Set custom layout function |
| `.gap(pixels)` | Set spacing between children |
| `.pad(value)` | Set padding (number or object) |
| `.align(mode)` | Set cross-axis alignment |
| `.scroll()` | Enable scrolling |
| `.grow(factor=1)` | Set flex grow factor for stacks |

#### Styling

| Method | Description |
|--------|-------------|
| `.style(object)` | Set visual CSS properties |
| `.attr(key, value)` | Set HTML attribute |
| `.role(role)` | Set ARIA role |
| `.hint(object)` | Set compositing hints |

#### Interaction

| Method | Description |
|--------|-------------|
| `.on(event, handler)` | Bind event handler |
| `.off(event)` | Remove event handler |
| `.decorate(fn, opts?)` | Add post-layout decoration |

#### Animation

| Method | Description |
|--------|-------------|
| `.transition(opts?)` | Enable frame animation (`{ duration, easing }`) |
| `.noTransition()` | Disable frame animation |

#### Content

| Method | Description |
|--------|-------------|
| `.text(content)` | Update text content (for text nodes) |

#### Lifecycle

| Method | Description |
|--------|-------------|
| `.dirty()` | Mark node for re-layout |

#### Metadata

| Method | Description |
|--------|-------------|
| `.key(value)` | Set stable identity key |

### Runtime Functions

| Function | Description |
|----------|-------------|
| `layout(node, constraints)` | Measure and place a node tree |
| `mount(node, container, options?)` | Connect to DOM, returns `{ update, destroy }` or Promise |
| `renderInto(node, element)` | Render subtree into DOM element |
| `measureIntrinsic(node, mode)` | Measure intrinsic size (`'min'` or `'max'`) |
| `exportTree(node)` | Export tree as JSON object |

#### Mount Options

| Option | Type | Description |
|--------|------|-------------|
| `fonts` | `string[]` | Font specs to wait for before rendering (e.g., `'900 16px "Font Name"'`) |
| `relayoutOnFontLoad` | `boolean` | Re-layout when any font finishes loading after mount |

When `fonts` is specified, `mount()` returns a `Promise<{ update, destroy }>`.

### Component Functions

| Function | Description |
|----------|-------------|
| `vlist(config)` | Create virtualized list |
| `thumbnail(src, opts?)` | Create lazy-loading image |

### Animation Functions (animation.js)

| Function | Description |
|----------|-------------|
| `animatable(initial)` | Create animatable value |
| `spring(initial, config?)` | Create spring-based animatable |
| `lerp(start, stop, fraction)` | Linear interpolation |
| `lerpPoint(start, stop, fraction)` | Interpolate { x, y } points |
| `lerpObject(start, stop, fraction)` | Interpolate object properties |
| `parallel(...animations)` | Run animations in parallel |
| `sequence(...fns)` | Run animations in sequence |
| `repeat(fn, times?)` | Repeat an animation |
| `stagger(anims, target, spec, ms)` | Staggered animation |
| `animationController()` | Create animation controller |
| `animatedNode(node, updateFn)` | Auto-updating animated node |

#### Animatable Methods

| Method | Description |
|--------|-------------|
| `.value` | Current animated value |
| `.targetValue` | Target value |
| `.isAnimating` | Whether animation is running |
| `.snapTo(value)` | Instant jump to value |
| `.animateTo(value, spec?)` | Animate to value |
| `.delay(ms)` | Delay before continuing |
| `.stop()` | Cancel current animation |
| `.onUpdate(callback)` | Set change callback |

#### Easing Functions

| Function | Description |
|----------|-------------|
| `linear` | Constant speed |
| `easeIn`, `easeOut`, `easeInOut` | Standard curves |
| `easeInCubic`, `easeOutCubic` | Cubic curves |
| `easeInQuart`, `easeOutQuart` | Quartic curves |
| `easeInBack`, `easeOutBack` | Overshoot curves |
| `easeOutElastic` | Elastic effect |
| `easeOutBounce` | Bounce effect |
| `cubicBezier(x1, y1, x2, y2)` | Custom bezier curve |

### Debug Functions

| Function | Description |
|----------|-------------|
| `enableDebug(node, opts?)` | Show debug overlay (opts: `{ showDirty, showMeasured }`) |
| `disableDebug(node)` | Hide debug overlay |
| `debugOnKey(node, key?, opts?)` | Toggle debug on keypress |
| `enableConstraintLogging()` | Log constraints to console |
| `disableConstraintLogging()` | Stop logging constraints |
| `enableMeasureLogging()` | Log measurements to console |
| `disableMeasureLogging()` | Stop logging measurements |
| `enableMeasureTracking()` | Track measured nodes |
| `disableMeasureTracking()` | Stop tracking |
| `debugResetTracking()` | Clear measured nodes set |
| `getMeasuredNodes()` | Get Set of measured nodes |
| `flashMeasuredNodes(ms?)` | Flash measured nodes visually |
| `logTree(node)` | Print tree structure to console |

---

## Examples

### Counter App

```javascript
import { vstack, hstack, text, box, mount } from './composure/composure.js'

let count = 0
const countText = text('0', 'div')
  .style({ fontSize: 48, fontWeight: '700' })

function button(label, onClick) {
  return box('button')
    .style({
      background: '#3b82f6',
      color: '#fff',
      border: 'none',
      borderRadius: 8,
      cursor: 'pointer'
    })
    .pad({ top: 12, bottom: 12, left: 24, right: 24 })
    .add(text(label))
    .on('click', onClick)
}

const root = vstack()
  .align('center')
  .gap(24)
  .pad(40)
  .add(
    countText,
    hstack().gap(12).add(
      button('-', () => {
        count--
        countText._text = count.toString()
        app.update()
      }),
      button('+', () => {
        count++
        countText._text = count.toString()
        app.update()
      })
    )
  )

const app = mount(root, document.getElementById('root'))
```

### Todo List

```javascript
import { vstack, hstack, text, box, mount } from './composure/composure.js'

const todos = []
const todoList = vstack().gap(8)

function createTodoItem(todo) {
  const item = hstack()
    .key(todo.id)
    .gap(12)
    .pad(12)
    .style({ background: '#f5f5f5', borderRadius: 8 })
    .add(
      text(todo.text).style({ flex: 1 }),
      box('button')
        .style({ background: 'transparent', border: 'none', cursor: 'pointer' })
        .add(text('×').style({ fontSize: 20, color: '#ef4444' }))
        .on('click', () => {
          const index = todos.indexOf(todo)
          if (index > -1) {
            todos.splice(index, 1)
            todoList.remove(item)
            app.update()
          }
        })
    )
  return item
}

const input = box('input')
  .attr('type', 'text')
  .attr('placeholder', 'Add a todo...')
  .style({ border: '1px solid #ddd', borderRadius: 8 })
  .pad(12)

const addButton = box('button')
  .style({ background: '#10b981', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer' })
  .pad({ top: 12, bottom: 12, left: 20, right: 20 })
  .add(text('Add'))
  .on('click', () => {
    const el = input._el
    if (el && el.value.trim()) {
      const todo = { id: Date.now(), text: el.value.trim() }
      todos.push(todo)
      todoList.add(createTodoItem(todo))
      el.value = ''
      app.update()
    }
  })

const root = vstack()
  .pad(24)
  .gap(16)
  .size(400, null)
  .add(
    text('Todo List', 'h1').style({ fontSize: 24, fontWeight: '700' }),
    hstack().gap(8).add(input, addButton),
    todoList
  )

const app = mount(root, document.getElementById('root'))
```

### Image Gallery with Virtualization

```javascript
import { vstack, text, mount } from './composure/composure.js'
import { vlist, thumbnail } from './composure/components.js'

// Generate sample data
const images = Array.from({ length: 10000 }, (_, i) => ({
  url: `https://picsum.photos/id/${i % 1000}/300/200`,
  title: `Image ${i + 1}`,
  author: `Photographer ${(i % 50) + 1}`
}))

const gallery = vlist({
  count: images.length,
  itemHeight: 240,
  columns: 4,
  gap: 8,
  overscan: 2,
  render: (index) => {
    const img = images[index]
    return vstack()
      .style({ background: '#fff', borderRadius: 8, overflow: 'hidden' })
      .add(
        thumbnail(img.url, { placeholder: '#f0f0f0' })
          .size(null, 180),
        vstack()
          .pad(12)
          .gap(4)
          .add(
            text(img.title, 'h3').style({ fontSize: 14, fontWeight: '600' }),
            text(img.author, 'span').style({ fontSize: 12, color: '#666' })
          )
      )
  }
})

const root = vstack()
  .pad(16)
  .add(
    text('Gallery', 'h1').style({ fontSize: 24, fontWeight: '700', marginBottom: 16 }),
    gallery.size(null, 600)
  )

mount(root, document.getElementById('root'))
```

---

## Best Practices

### 1. Keep Layout Logic Simple

Custom layouts should be straightforward. If your layout function is complex, consider breaking it into multiple nested containers.

### 2. Use Keys for Dynamic Lists

Always key items that may be reordered, added, or removed:

```javascript
items.map(item =>
  box().key(item.id).add(text(item.name))
)
```

### 3. Prefer Built-in Layouts

Use `vstack`, `hstack`, and `zstack` when possible. Custom layouts are powerful but add complexity.

### 4. Don't Fight the System

- Don't try to use CSS layout properties (they're ignored)
- Don't modify frames directly in decoration
- Don't expect children to influence parent sizing after layout

### 5. Batch Updates

If making multiple tree mutations, make them all before calling `update()`:

```javascript
// Good: single update
list.clear()
newItems.forEach(item => list.add(renderItem(item)))
app.update()

// Avoid: multiple updates
newItems.forEach(item => {
  list.add(renderItem(item))
  app.update()  // Inefficient!
})
```

### 6. Use Virtualization for Large Lists

Any list with more than ~100 items should use `vlist` for smooth scrolling.

### 7. Debug Early and Often

Use `debugOnKey()` during development to visualize layout structure and catch issues early.
