Mischief

27 / Documents

Annotation Layer

Notes attached to regions of a page. Drag to add one, select to read it, and the coordinates stay relative to the page rather than the screen.

Master Services Agreement, page 1

Drag on the page to add a note.

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/annotation-layer
import { AnnotationLayer } from "mischief-ui/annotation-layer"
Also installs
  • lucide-react

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/annotation-layer/annotation-layer.tsx
"use client" import * as React from "react"import { MessageSquare, Trash2 } from "lucide-react"import { cn } from "@/lib/utils" export type Annotation = {  id: string  x: number  y: number  width: number  height: number  note?: string  author?: string

Usage

export function Review({ page }: { page: string }) {
  return (
    <AnnotationLayer
      src={page}
      alt="Contract, page 2"
      annotations={notes}
      onCreate={(rect) => addNote(rect)}
    />
  )
}

Coordinate system

Annotations are stored as fractions of the image rather than pixels: x and y are the top-left corner, width and height run from there, and everything sits between 0 and 1. A note therefore stays on the same words when the image is resized, zoomed, or rendered on a denser screen, and the same numbers survive being stored and read back at another size.

// A note over the middle of the page, whatever it renders at.
const annotation = {
  id: "clause-4",
  x: 0.25,
  y: 0.4,
  width: 0.5,
  height: 0.08,
  note: "Check this against the master agreement",
  author: "Aman",
}

Drawing and storing

The component holds no list of its own. Dragging on the image calls onCreate with the rectangle, and it is yours to store, give an id, and pass back in. Nothing appears until you do, which is what lets you await a save and show a failure instead of a note that was never kept.

async function onCreate(rect) {
  const saved = await api.annotate({ ...rect, note: await ask() })
  setAnnotations((current) => [...current, saved])
}

readOnly keeps the notes visible and stops new ones being drawn, which is the right mode for anyone without permission to comment. minSize discards a stray click that would leave an annotation too small to find again.

API

src, altstringThe page image and its description.
annotationsAnnotation[]Id, note, author, and x, y, width, height as fractions of the page.
onCreate(rect: AnnotationRect) => voidRuns with a new region when someone drags one out. Omit it to disable drawing.
onDelete(id: string) => voidShows a delete control when given.
activeId, defaultActiveId, onActiveChangestring | nullThe selected note, controlled or uncontrolled.
minSizenumberSmallest drag that counts as a region. Defaults to 0.01 of the page.

Annotation

idstringUnique within the set. Drives selection.
x, ynumberTop-left corner as a fraction of the image, from 0 to 1.
width, heightnumberSize as a fraction of the image, from 0 to 1.
notestringThe comment itself.
authorstringWho left it.

Accessibility

Regions are toggle buttons carrying their note as an accessible name, so notes can be reached and read without a pointer. The note itself appears in a polite live region rather than a hover card. A drag that never moved is treated as a deselect instead of creating an unusably small region. Coordinates are fractions of the page, so they survive zoom and a change of screen.