KQ-1755 · SCREENSHOT/MARKUP
Screenshot Markup
An image annotation editor: arrows, boxes, labels and redaction over a screenshot, every mark kept in the image's own pixels. Drawing an arrow, the shaft bows behind the hand — a curve whose control point trails the pointer on springs.glide — and on release it pulls taut while the head springs into place on springs.recoil with two visible bounces. The redact brush pixelates whatever passes under it: cells of a grid fill with the real mosaic of the image beneath, popping in as separate tiles on springs.flick, and when the stroke ends they settle — gaps closing and corners squaring on springs.snap in the order they were laid — into one solid mosaic. Tools have single keys, the canvas has a keyboard cursor so Enter, arrows and Enter draw any mark, [ and ] select marks, arrows nudge them and Delete removes them, and undo and redo walk a history.
Basinworks · levels, 14 Oct
4 marks · 1 redaction
Tools: V select, A arrow, B box, T text, R redact. Arrow keys move the cursor; Enter starts a mark there, arrows extend it and Enter ends it. Left and right brackets select marks; arrows nudge the selected mark, Shift for ten pixels; Delete removes it; Escape cancels.
arrow · 4 marks · 1 redaction
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/screenshot-markup
Props
- strokeWidth
- number
- default 3
- Stroke of arrows and boxes in image px; it also sizes arrowheads, labels and the redact brush (six times the stroke).
- toolPalette
- "rail" | "float" | "strip"
- default "rail"
- Where the tools live: a rail down the left, a pill floating over the image, or a strip under the header. Below a 36rem container they dock at the bottom whatever this is.
- accent
- string
- default "var(--danger)"
- The markup colour, taken at a fixed lightness so marks read the same over either theme.
- image
- MarkupImage
- default defaultMarkupImage
- The screenshot as { width, height, alt, src?, content? }: a raster src, or SVG content drawn in the image's coordinates (the default Basinworks dashboard).
- annotations / defaultAnnotations
- MarkupAnnotation[]
- default — / defaultMarkupAnnotations
- Marks in image pixels: arrow { x1, y1, x2, y2 }, box { x, y, w, h }, text { x, y, text } and redact { size, cells: [column, row][] }.
- onChange
- (annotations) => void
- default —
- Fires with every mark after one is drawn, moved, nudged, edited, removed, undone or redone.
- tool / defaultTool
- "select" | "arrow" | "box" | "text" | "redact"
- default — / "arrow"
- The active tool.
- onToolChange
- (tool) => void
- default —
- Fires from the button or single key (V, A, B, T, R) that picked a tool.
- selected / defaultSelected
- string | null
- default — / null
- The selected mark's id.
- onSelectedChange
- (id) => void
- default —
- Fires when a click, a bracket key or a new mark changes the selection.
- onUndo
- (annotations) => void
- default —
- Undo was pressed or Command/Control+Z typed, with the marks it went back to.
- onRedo
- (annotations) => void
- default —
- Redo was pressed or Shift+Command/Control+Z typed, with the marks it restored.
- onExport
- (annotations, image) => void | Promise
- default —
- Export was pressed. Return a promise to show it exporting; a rejection offers Retry.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the screenshot has arrived: loading shows a ruled frame and a progress bar, error offers Try again.
- progress
- number
- default —
- How much of a large screenshot has loaded, 0 to 1; omit it for an indeterminate bar.
- onRetry
- () => void
- default —
- Try again was pressed after the screenshot failed to load.
- title
- string
- default "Basinworks · levels, 14 Oct"
- The editor's heading.
- cellSize
- number
- default 16
- The redaction grid in image px: the size of each pixelated block.
- height
- number
- default 540
- The editor's tallest height in px; the image letterboxes inside.
- disabled
- boolean
- default false
- Marks can be viewed but not drawn, moved, removed or exported.
- className
- string
- default —
- Classes for the editor's outer box.
| Prop | Type | Default | Description |
|---|---|---|---|
| strokeWidth | number | 3 | Stroke of arrows and boxes in image px; it also sizes arrowheads, labels and the redact brush (six times the stroke). |
| toolPalette | "rail" | "float" | "strip" | "rail" | Where the tools live: a rail down the left, a pill floating over the image, or a strip under the header. Below a 36rem container they dock at the bottom whatever this is. |
| accent | string | "var(--danger)" | The markup colour, taken at a fixed lightness so marks read the same over either theme. |
| image | MarkupImage | defaultMarkupImage | The screenshot as { width, height, alt, src?, content? }: a raster src, or SVG content drawn in the image's coordinates (the default Basinworks dashboard). |
| annotations / defaultAnnotations | MarkupAnnotation[] | — / defaultMarkupAnnotations | Marks in image pixels: arrow { x1, y1, x2, y2 }, box { x, y, w, h }, text { x, y, text } and redact { size, cells: [column, row][] }. |
| onChange | (annotations) => void | — | Fires with every mark after one is drawn, moved, nudged, edited, removed, undone or redone. |
| tool / defaultTool | "select" | "arrow" | "box" | "text" | "redact" | — / "arrow" | The active tool. |
| onToolChange | (tool) => void | — | Fires from the button or single key (V, A, B, T, R) that picked a tool. |
| selected / defaultSelected | string | null | — / null | The selected mark's id. |
| onSelectedChange | (id) => void | — | Fires when a click, a bracket key or a new mark changes the selection. |
| onUndo | (annotations) => void | — | Undo was pressed or Command/Control+Z typed, with the marks it went back to. |
| onRedo | (annotations) => void | — | Redo was pressed or Shift+Command/Control+Z typed, with the marks it restored. |
| onExport | (annotations, image) => void | Promise | — | Export was pressed. Return a promise to show it exporting; a rejection offers Retry. |
| status | "ready" | "loading" | "error" | "ready" | Whether the screenshot has arrived: loading shows a ruled frame and a progress bar, error offers Try again. |
| progress | number | — | How much of a large screenshot has loaded, 0 to 1; omit it for an indeterminate bar. |
| onRetry | () => void | — | Try again was pressed after the screenshot failed to load. |
| title | string | "Basinworks · levels, 14 Oct" | The editor's heading. |
| cellSize | number | 16 | The redaction grid in image px: the size of each pixelated block. |
| height | number | 540 | The editor's tallest height in px; the image letterboxes inside. |
| disabled | boolean | false | Marks can be viewed but not drawn, moved, removed or exported. |
| className | string | — | Classes for the editor's outer box. |