Skip to content

v0.1: working markupit — local serving + annotation overlay - #1

Merged
seanpk merged 15 commits into
masterfrom
build/v0.1
Jun 16, 2026
Merged

seanpk merged 15 commits into
masterfrom
build/v0.1

Conversation

@seanpk

@seanpk seanpk commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Implements the markupit specification clean-room, test-first. Turns the design-phase repo into a working tool: npx markupit ./dir (or a single .html), open the printed ?markupit URL, click any element to comment / edit / remove, then copy a structured markdown brief for an agent.

Scope

Covered (all MUSTs for local modes):

  • CLI + server — local directory and single-file serving, no-store headers, reserved /__markupit__/ namespace, port fallback, clean shutdown, idempotent overlay injection, activation gating via ?markupit.
  • Overlay — vanilla-JS, Shadow-DOM isolated. Stable element IDs with cascade re-anchoring, selection, comment/edit/remove (all reversible), the queue/overview, localStorage persistence, and a pure markdown export.
  • Design — calm chrome, anchored popover that grows from the click, distinct per-action treatments, prefers-reduced-motion, keyboard fluency.

Deferred to a follow-up (stubbed with a clear notice):

  • Proxy URL mode (SRC-3/4/5/7) — reviewing a live http(s) page.

Constraints held

  • Zero runtime dependencies — Node built-ins only; CI asserts dependencies stays empty.
  • Pure ESM, no build step to use, single npm package.
  • Clean-room from docs/.

Tests

  • 44 unit (node:test + linkedom) · 13 server integration · 16 Playwright.
  • npm run test:ids gates that every MUST requirement has an [ID]-tagged assertion (or a recorded exemption).
  • CI: Node 20/22/24 matrix + a separate Playwright job (chromium/firefox/webkit).

Commit trail

Each commit is one milestone, leaves the suite green, and cites the requirement IDs it satisfies — git log reads as a spec-coverage walk.

Notes

  • 0.1 uses a system-font stack rather than a bundled brand font (avoids shipping a binary / network fetch); bundling DM Sans is a follow-up.
  • npm publish is wired (prepublishOnly runs lint + tests; tarball verified to ship only bin/, src/, assets/overlay/, README, LICENSE) but not yet published — markupit is available on npm.

🤖 Generated with Claude Code

seanpk and others added 10 commits June 14, 2026 22:37
Eight source files covering icon, wordmark, horizontal lockup, and stacked
lockup — each in light and dark background variants. Based on logo concept 6
(nested rounded squares with arrow mark). ViewBox dimensions measured via
Inkscape to eliminate trailing whitespace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure-ESM, zero-runtime-dependency package skeleton for the v0.1 build:
- package.json (bin, exports, files whitelist, engines >=20.10, empty deps)
- ESLint flat config + Prettier
- GitHub Actions CI: node 20/22/24 matrix (lint, unit, server, id-gate,
  zero-dep assertion) + separate Playwright job
- scripts/coverage-ids.mjs: fails CI on any MUST with no [ID]-tagged assertion
- test harness (linkedom DOM loader, server boot + mock-remote helpers) and
  fixtures (static site with varied + non-content nodes, single file, hostile
  stylesheet)
- docs/PLAN-0.1.md

Tests green: [NFR-1] zero runtime deps, [CLI-1] markupit bin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CLI + HTTP server for the two local source modes:
- args.js (pure): flag parsing with validation [CLI-5,6,7]
- source.js: classify dir / file / url, banner labels [CLI-2,3,4]
- server: static serving with traversal guard, MIME table, no-store
  headers, reserved /__markupit__/ namespace checked first [SRC-1,2,6,8]
- net.js: port fallback on EADDRINUSE [CLI-5]
- cli.js: startup banner, --open, clean SIGINT/SIGTERM shutdown [CLI-8]
- bin/markupit.js entry; proxy.js stub returns a friendly "coming soon"

22 tests green (13 unit, 9 server). CLI verified end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- inject.js (pure): insert the overlay module script before the last
  </body>, preserving document casing; idempotent via the injection
  marker so re-serving never doubles it [ACT-1, ACT-4]
- wire injection into HTML responses only; non-HTML passes through
- activation lives in the URL: the dormant overlay is injected into every
  page identically and self-gates on ?markupit, so a path's served bytes
  are the same with or without the flag [ACT-5]

10 new tests green (inject unit + server injection). 32 total.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DOM-simulator-testable heart of the overlay (no browser globals):
- ids.js: composite anchor (authored-id -> structural nth-of-type path ->
  FNV-1a text fingerprint) with cascade re-anchoring; orphans resolve to
  null rather than mis-match [SEL-7]
- selectable.js: content-element predicate, candidate resolution, parent/
  child widening; non-content + chrome excluded [SEL-3,5,6]
- model.js: pure reducer, one entry per element with coexisting comment/
  edit/remove slots, all reversible [ANN-2,4,5]
- export.js: state -> document-ordered markdown brief + raw JSON [EXP-2..7]
- label.js, snippet.js: human labels and content snippets [QUE-2]

Also fixes a sharp test hazard: asserting on a linkedom DOM node deep-
inspects its circular graph (>100MB) on failure and OOMs the process.
candidateAt now resolves text nodes to their element, and tests compare
node identity via a sameNode() helper so a failure only inspects a boolean.

44 unit tests green (up from 19). 57 total.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The full annotation surface, vanilla JS in a single shadow root so it is
style-isolated from the page (UX-10) and our chrome is never selectable as
content (SEL-5):
- shadow.js + styles.js: design-system stylesheet adopted into the root;
  :host inherited resets are !important so a hostile page `*` rule can't
  bleed font/colour into chrome
- selection.js + highlight.js: hover highlight and click-to-select drawn in
  the shadow layer, never touching the page [SEL-1,2]
- popover.js: anchored action popover that grows from the click point;
  comment / edit (focused textarea) / remove, all reversible [ANN-1,2,3,4,6]
- markers.js: in-page edit/remove treatments (owned inline styles) + on-page
  dots [ANN-3, QUE-5]
- queue.js + toolbar.js: sliding overview with human labels, live count,
  copy + reset [QUE-1,2,4, ANN-8, EXP-1]
- persistence.js + anchor.js: localStorage keyed to the page, re-anchoring on
  load, orphans kept not dropped [ANN-7]
- keyboard: c/e/r, [ ] widen, Esc, ⌘/Ctrl+Shift+C export [UX-7]; respects
  prefers-reduced-motion [UX-5]

Note: 0.1 uses a system font stack; bundling the brand font is a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
16 specs against a real markupit server and headless Chromium:
- activation: clean page without the flag, no console errors with it, shadow
  host mounts, page scripts keep working [ACT-2,3, NFR-2,4]
- annotate: hover highlight, click-to-select, add comment, remove treatment,
  persist-across-reload + clipboard export round trip [SEL-1,2, ANN-1,3,7, EXP-1]
- encapsulation: hostile page stylesheet can't bleed into chrome and chrome
  can't leak onto the page; chrome not selectable; reduced motion [UX-10,5, SEL-5]
- queue + motion: queue lists all annotations with human labels, distinct
  per-kind treatments, motion present by default [QUE-1,2, UX-3,4]

Every MUST requirement is now covered by a tagged assertion or recorded as
exempt (proxy mode + subjective UX). Suite: 44 unit, 13 server, 16 browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README: drop the design-phase notice; document usage, options, the
  activation flag, marking-up flow, how-it-works, and the proxy roadmap
- CONTRIBUTING.md: test-first + ID-tracing workflow, the hard constraints
  (zero deps, clean-room, page-disappears, UX bar), and the linkedom/assert
  OOM gotcha
- docs/UX-CHECKLIST.md: human pass for the subjective UX-* qualities
- package.json: prepublishOnly runs lint + tests; verified the publish
  tarball ships only bin/src/overlay/README/LICENSE

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The inline `node -e` contained a colon-space ("found: ") that GitHub's YAML
parser read as a nested mapping, failing the whole workflow at parse time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- test runner: replace `node --test "glob"` (needs Node 21+) with a small
  portable discovery script so test:unit/test:server run on Node 20/22/24
  and on Windows
- fixture: regenerate team.png as a valid PNG (correct zlib stream + CRCs);
  the previous bytes decoded on Chromium but Firefox flagged them, tripping
  the [NFR-2] no-console-errors check
- browser tests: clipboard-read/write permissions are Chromium-only in
  Playwright — split [ANN-7] persistence (all browsers) from [EXP-1] clipboard
  export (chromium-only via test.skip)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@seanpk

seanpk commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

UX feedback — comment/edit panel behaviour

Noted while reviewing real content with the overlay:

  • The comment and edit input panels feel too small — the text area especially needs more vertical room to write a proper note without constant scrolling.
  • Clicking outside the panel dismisses it and loses the draft. This is surprising and frustrating; panels should stay open until explicitly confirmed or cancelled.
  • It would be worth making the panels draggable/movable — when you've selected an element near the bottom of the viewport the panel can cover the very thing you're annotating.

@seanpk

seanpk commented Jun 16, 2026 •

Copy link
Copy Markdown
Owner Author

Follow-up: review-note history (instead of destructive clear)

Feedback from dogfooding — the system is working well. Two related asks about the review notes, now scoped into a concrete plan.

1. Keep notes as history instead of discarding on clear

Today doReset() (assets/overlay/app.js:207) is fully destructive: model.resetAll empties annotations and persistence.clear() drops the storage key. Instead, archive on clear:

  • Extend the model state with a history array (core/model.js). On clear, push the current live annotations into history as a timestamped batch rather than dropping them, then empty the live set.
  • liveCount (model.js:123) already counts only live annotations, so history won't inflate counts or re-apply page treatments.
  • load() defaults history = [] for states saved before this change (backward-compat guard at persistence.js:14).

History entries are read-only data — the page treatments (edit/remove) are still reverted on clear, and anchors may go stale on later loads, so we don't try to re-apply them.

2. Confirmation + no undo

  • Replace the native confirm() (app.js:209) with an in-overlay styled confirmation rendered in our shadow DOM — no native browser dialog. We want beautiful, consistent UX. It stays as the safety net before a destructive clear.
  • No undo support in this pass — explicitly out of scope.

History UI / export

  • Render history as timestamped items.
  • Support individual deletion of a history item and a total clear of all history.
  • Render the history comments into a single block of text that can be copied/pasted out, with a copy button on the entry (reuses the clipboard path from doExport).

Out of scope for now: undo, re-applying archived treatments, history size caps.

seanpk and others added 2 commits June 16, 2026 11:52
Reset was destructive — clearing the page wiped every annotation and
removed the localStorage key. Make it non-destructive: each clear is
archived as a timestamped history batch (one entry per clear) that
persists across reloads, and the queue panel grows a collapsible
History section to review, copy, delete, or clear past notes.

- model: history[] state with archiveAndReset / deleteHistoryBatch /
  clearHistory / historyBatches; summarizeKinds (liveCount reuses it).
- persistence: default missing history on load (back-compat); reset now
  saves the archived state instead of removing the storage key.
- queue: History section with per-entry copy (full notes brief via the
  existing markdown export) and delete, plus a total clear.

ANN-8 is now non-destructive; adds QUE-6 (history) and QUE-7
(copy/delete/clear). Confirmation still uses the native dialog — the
styled replacement follows in the next commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The browser's window.confirm is jarring and out of place inside the
shadow-DOM overlay. Add a styled, promise-based confirmation rendered in
the overlay itself (danger variant, Esc / backdrop to cancel, focus on
the primary button) and use it for both Reset and Clear history.

- confirm.js: createConfirm(root) factory matching the other UI modules.
- app: await the styled dialog instead of window.confirm.
- styles: dialog + backdrop, reusing the design tokens and mk-grow.
- browser tests cover the styled (non-native) confirm and the full
  history flow: archive-on-reset, cancel, persistence, copy, delete,
  and total clear.

Adds UX-11 (no native dialogs); covers QUE-6/QUE-7 in the browser suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@seanpk

seanpk commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

Implemented ✅

Both pieces of feedback are in, on build/v0.1, as two commits that follow the same story:

  • 919cfd3 — retain cleared review notes as history. Reset is now non-destructive: each clear is archived as a timestamped history batch (one entry per clear) that persists across reloads. The queue panel grows a collapsible History section to copy (full notes brief, via the existing markdown export), delete, or clear past notes. Model gains archiveAndReset / deleteHistoryBatch / clearHistory / historyBatches / summarizeKinds; load() is back-compat for pre-history states.
  • be6a88a — styled in-overlay confirmation. The native window.confirm is replaced with a promise-based dialog rendered in the shadow DOM (danger variant, Esc / backdrop to cancel, focus on the primary button), used for both Reset and Clear history.

Per the discussion: no undo this pass; history entries are individually deletable with a total clear; each entry copies out as a single text block via its Copy button.

Requirements

ANN-8 is now non-destructive; added QUE-6 (history), QUE-7 (copy/delete/clear), and UX-11 (no native dialogs).

Verification

  • npm run lint clean; npm test (48 unit + 13 server) green; MUST-coverage gate passes.
  • npx playwright test — 21/21, including 4 new history specs: archive-on-reset via the styled (non-native) confirm, cancel leaves notes intact, persistence across reload, per-entry copy of the full brief, and individual delete + total clear.

seanpk and others added 3 commits June 16, 2026 12:29
Polish from review feedback on the confirm/history flow:

- Reset dialog: clearer two-line prompt ("Clear all annotations for this
  page / and reset for a new review?"), primary button reads "Reset", and
  a third action "Copy notes and Clear" copies the brief then clears in
  one step.
- Deleting a history entry now asks for confirmation (no accidental loss)
  and the control uses a trash-can icon instead of a bare ×.
- createConfirm generalised to resolve 'confirm' | 'cancel' | 'extra',
  supporting an optional third button and multi-line messages.

Updates ANN-8 (copy-and-clear); browser tests cover the new dialog
actions and the delete confirmation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…omposing

Two popover fixes from review feedback:

- The selected-element outline gains a white contrast ring so it stays
  legible on dark page backgrounds, where the ink border alone vanished.
  It remains drawn while the popover is open, including in comment/edit
  mode (ANN-9).
- While composing a comment or edit, a stray click elsewhere on the page
  no longer swaps or dismisses the popover or discards the in-progress
  text (ANN-10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…views

Record the larger ideas raised in review for a future release: moving the
popover aside while composing, annotating content that only appears after a
page-driven interaction, and collecting feedback across multiple pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@seanpk

seanpk commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

Round 2 — review-note UX refinements ✅

Pushed to build/v0.1 as three commits:

  • a252c9f — confirmation & history UX. Reset dialog now reads "Clear all annotations for this page / and reset for a new review?" with a primary Reset button and a third Copy notes and Clear action (copies the brief, then clears). Deleting a history entry now asks for confirmation and uses a trash-can icon instead of ×. createConfirm was generalized to resolve 'confirm' | 'cancel' | 'extra' with multi-line messages.
  • 97cc3b1 — popover focus + composing. The selected-element outline gains a white contrast ring so it stays legible on dark page backgrounds (addresses the missing highlight in screenshot Proxy URL mode: review a live http(s) page #2) and persists in comment/edit mode. While composing a comment/edit, a stray click on the page no longer dismisses the popover or loses your text.
  • d626b95 — roadmap. Recorded the bigger ideas for a future release (below).

Mapped to: #1 trash icon ✓, #2 delete confirm ✓, #3 reset wording + "Reset" label ✓, #4 "Copy notes and Clear" ✓, #5 focus highlight ✓, #6 no-dismiss while composing ✓.

Verification

npm run lint clean · npm test 48 unit + 13 server, MUST-gate passes · npx playwright test 25/25 (added: copy-and-clear, delete-confirm + cancel, no-dismiss-while-composing, focus-highlight). New requirements: ANN-8 (copy-and-clear), ANN-9 (focus highlight), ANN-10 (no dismiss while composing).

Backlog (recorded in README roadmap)

  • Per-element comment-count badges (QUE-5) #7 Draggable popover — move the box aside to read surrounding context while composing.
  • Annotate interactive content — trigger the page's own actions (menus, accordions) and annotate elements that only appear after a JS-driven interaction, rather than intercepting every click.
  • Multi-page reviews — follow links and collect annotations across pages into one brief.

@seanpk
seanpk merged commit 102c5eb into master Jun 16, 2026
4 checks passed
@seanpk
seanpk deleted the build/v0.1 branch June 16, 2026 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant