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
anchors land in minor releases and are listed in the changelog. Tools that
parse the JSON should ignore unknown fields.