redlining

Annotation format

Save writes up to three files into .redlining/. The Markdown is the primary, human- and agent-readable spec; the JSON is the same session for tools and tests; the PNG is optional.

annotations.md

# Redlining — /dashboard  (2026-09-06 14:12 · viewport 1440×900)

Styling: Tailwind 4 (detected: --spacing and --text-* tokens)

Screenshot: .redlining/screenshot.png (pins numbered as below)

## 1 · CHANGE — MainNav
- Anchor: `<nav>` · app/(app)/layout.tsx:42 · owners: RootLayout › Header › MainNav
- Text: "Dashboard · Reports · Settings"
- Note: Replace the dropdown with a horizontal top nav. Same items, same order.
  Active item underlined.

## 2 · ADD — inside FilterPanel
- Container: `<section>` · app/(app)/dashboard/page.tsx:87 · owners: DashboardPage › FilterPanel
- Position: after child 2 · full width · ≈ 220 px tall
- Note: Sortable table. Columns: Name, Status, Updated. Status filter chips above it.
  Reuse our existing DataTable if present.

## 3 · REMOVE — "Export CSV" button
- Anchor: `<button>` · components/toolbar.tsx:31 · owners: DashboardPage › Toolbar
- Text: "Export CSV"
- Note: Remove; the action moves into the new table's row menu.

## 4 · MOVE — QuickStats
- From: `<aside>` · components/sidebar.tsx:12 · owners: DashboardPage › Sidebar › QuickStats
- To: before `<section>` · app/(app)/dashboard/page.tsx:60 · owners: DashboardPage › Main
- Note: Show quick stats above the main content on this page only.

---
Apply in order. Reuse existing components and design tokens. Do not touch anything not listed.

The header names the route, the local time of the save and the viewport. Styling: names the idiom the values map to (detected from the page or set by hand); the footer speaks in that idiom. The screenshot line appears only when a screenshot was included. Blocks are numbered in the order of the list panel. When other routes in the same browser carry annotations, their blocks follow under their own # Redlining — /route heading, and the footer comes once at the end.

Block shapes

Action Lines Title
CHANGE Anchor, Text (if any), Note innermost owner, else "text" tag, else <tag>
REMOVE as CHANGE prefers "text" tag when the text is short
ADD Container, Position, Note inside Owner
MOVE From, To (with before / after / inside), Note innermost owner of the source

Every location is `<tag>` · file:line · owners: A › B › C; the owners part is omitted for server-only subtrees. When the element comes from a reusable component, the location also names its usage site: instance 2 of 3 in `<section>` · app/page.tsx:41 is the nearest decorated ancestor from another file and this branch's position among its children, which is how three cards rendered by one card.tsx:33 are told apart. The To line of a move adds the target's text for the same reason. When the element comes from a reusable component, the location also names its usage site: instance 2 of 3 in `<section>` · app/page.tsx:41 is the nearest decorated ancestor from another file and this branch's position among its children, which is how three cards rendered by one card.tsx:33 are told apart. The To line of a move adds the target's text for the same reason. A multi-select annotation replaces Anchor with a numbered Anchors: list and suffixes the title with (+N). Fallback rungs add a Resolved: line, for example nearest decorated ancestor; locate the child by selector `main > div > span`, or the location itself reads unresolved.

Position for ADD is at start, after child N or at end, followed by full width or an approximate width, and an approximate height, all taken from the box you drew relative to the container.

Tweaks: Classes, Changes, Applies at

## 3 · CHANGE — Toolbar
- Anchor: `<button>` · components/toolbar.tsx:31 · owners: Toolbar
- Text: "Export CSV"
- Classes: `btn btn-primary`
- Changes:
  - text: "Export CSV" → "Download CSV"
  - font-size: 14px → 16px (class text-sm → text-base)
  - line-height: 20px → 24px (from var(--leading) via class btn)
  - width: auto (96px, laid out by the parent flex) → 128px (≈ 33 % of parent) — set by its content and the parent's flex sizing, not by this element; prefer changing the layout
  - padding-left, padding-right: 12px → 16px (from class btn)
  - background-color: rgb(243, 244, 246) → rgb(37, 99, 235) (from class btn; token --accent)
  - visual nudge: +8px right, −4px up — previewed with a transform; implement as spacing or alignment, never ship a transform
- Applies at: ≤ 768px (made in a 768px device frame)
- Reference: .redlining/ref-3-1.jpg — match this; it shows the intended result
- Crop: .redlining/crop-3.png — this element as it looks now

Classes is the element's class list, so a value can be mapped to a utility class. Each change is computed before → after; sizes add their share of the parent, colours the matching :root token when one exists, and equal paired sides collapse into one line. Each change also says where the old value came from: the winning class or selector, the var() it reads, or the ancestor it inherits from; when the new value equals a single-class rule in the project's stylesheets the line reads class A → B. A size nobody sets prints as auto (Npx, laid out by the parent grid|flex|block) and ends with a warning to change the layout instead. A nudge is always labelled as visual. When any block has changes, the footer adds: values are computed px at the stated viewport; implement them in this project's own idiom (utility classes, tokens), not as inline styles. A tweak may have no Note line. Reference names mockups pasted into the note and Crop the element cut from the screenshot; both appear on any action, with the files next to the export.

reply.md

Written by the agent after a /redline run; the overlay reads it to verify. One heading per annotation, in the export's numbering, with a route heading first when the export held several routes:

# /dashboard
## 1 · done — text-sm → text-lg (components/toolbar.tsx:31)
## 2 · partial — moved the card; the gap still needs a token
## 3 · skipped — the element no longer exists

annotations.json

interface Session {
  route: string            // "/dashboard"
  url: string
  viewport: { w: number; h: number }
  annotations: Annotation[]
  screenshot?: string      // data URL, only in the request; the file is written separately
}

interface Annotation {
  id: string
  index: number            // 1-based, the pin number
  action: 'change' | 'add' | 'remove' | 'move'
  anchor: Anchor
  anchors?: Anchor[]       // multi-select: every anchor, `anchor` first
  target?: Anchor & { position: 'before' | 'after' | 'inside' }   // move
  box?: { x: number; y: number; w: number; h: number; childIndex?: number } // add, relative to anchor.rect
  changes?: Change[]       // tweak deltas, see below
  appliesAt?: number       // device-frame width the annotation was made in
  note: string
  createdAt: string        // ISO
}

interface Change {
  kind: 'style' | 'text' | 'nudge' | 'visibility'
  property: string         // CSS longhand, 'text', 'transform' or 'display'
  from: string             // computed before
  to: string               // computed after
  input?: string           // what was typed or dragged
  token?: string           // :root custom property equal to `to`
  relative?: string        // e.g. "≈ 33 % of parent"
}

interface Anchor {
  file?: string            // project-relative; absent when selector-only
  line?: number
  column?: number
  tag: string              // 'nav' | 'form' | 'button' …
  owners: string[]         // outermost first; [] for server-only subtrees
  selector: string         // fallback CSS path
  text?: string            // trimmed textContent ≤ 80 chars
  rect: { x: number; y: number; w: number; h: number }   // viewport px at capture
  resolved: 'exact' | 'ancestor' | 'selector-only'
  classes?: string[]       // the element's class list (≤ 20)
  display?: string         // computed display at capture
}

childIndex means "insert before child N"; a value equal to the number of children means "at end".

Stability

The file format is unstable before 1.0. Field additions such as anchors land in minor releases and are listed in the changelog. Tools that parse the JSON should ignore unknown fields.