Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,33 @@ jobs:
test -f dist/app/index.html || { echo "::error::dist/app/index.html missing — app shell gone"; exit 1; }
test -f dist/404.html || { echo "::error::dist/404.html missing — SPA deep links would 404"; exit 1; }
grep -q "Read code. Annotate it." dist/index.html || { echo "::error::dist/index.html is not the landing page"; exit 1; }

# In a real browser against the dev server: ink that stays on its line while the document
# scrolls, and the flows that cross the editor, the database and the UI. A separate job so
# that `verify` stays fast and its result arrives first. Not in deploy.yml: a push to main
# has already passed this in review.
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- run: npm ci

- name: Install Chromium
run: npx playwright install --with-deps chromium

- name: End to end
run: npm run test:e2e

- name: Keep the traces of anything that failed
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces
path: test-results/
retention-days: 7
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ dist/
.vite/
*.local
.DS_Store
/test-results/
/playwright-report/
12 changes: 11 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Other scripts:
```bash
npm run typecheck # tsc --noEmit
npm test # vitest
npm run test:e2e # playwright, in a real browser (first run: npx playwright install chromium)
npm run build # typecheck + production build
```

Expand All @@ -66,7 +67,8 @@ npm run build # typecheck + production build
npm run typecheck && npm test && npm run build
```

CI runs exactly these, so running them locally is the fastest way to find out.
CI runs exactly these, plus the end-to-end suite in a separate job, so running them locally
is the fastest way to find out.

### Tests

Expand All @@ -78,6 +80,14 @@ assertion per claim, and a name that states the claim rather than the mechanism.
Anything touching anchoring deserves more tests than you think. Getting it wrong misplaces
somebody's handwriting.

### End to end

`e2e/` drives the app in Chromium against the dev server. Each test gets a fresh browser
context, seeds IndexedDB with exactly what it needs through the helpers in `e2e/app.ts`, and
opens it from the recents list - no network, no GitHub. Add one when a change crosses the
editor, the database and the UI in a way Vitest cannot see; `e2e/ink-scroll.spec.ts` is the
one that guards the plan's highest risk, ink drifting off its line.

### Things CI cannot check

Stylus feel and ink/scroll behaviour have no substitute for a real device. If your change
Expand Down
189 changes: 189 additions & 0 deletions PHASE_4_ORGANISATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Phase 4 — Organisation

*Companion to `IMPLEMENTATION_PLAN.md` §4, §7, §11. That document says what Phase 4 is —
layers, bookmarks, typed notes, an annotation index. This one says how, against the code
that exists after Phase 3, and records the calls that were made without a conversation so
they can be reversed cheaply if they are wrong.*

**Done when** (from the plan): *you can find a note you wrote last week by typing three
words.*

---

## 0. The one idea

Nothing in this phase needs a new coordinate system, a new store in IndexedDB, or a schema
bump. The data model in §4 already has `Layer`, `Bookmark`, `kind: 'note'` and
`Annotation.layerId`; Phases 2 and 3 just never exercised them. Phase 4 is UI over records
that already exist, plus three rules about how those records move:

1. **A typed note is an annotation like a stroke.** It sits in the same `placed` list, so it
is carried across an edit by the same `lineMapper`, re-anchored by the same save, and
lands in the same displaced tray when the ladder gives up. No second anchoring path.
2. **A bookmark is anchored, not numbered.** It carries an `Anchor` already. It resolves
through `resolveAnchor` when its file opens and moves with edits exactly as ink does.
3. **Visibility is a render-time filter, never a reload.** Hiding a layer repaints; it does
not touch the database or the undo stacks.

---

## 1. Layers

### Where they live in the UI

In the palette's `⋯` overflow, which §7 already reserved for "Layers, Code Space toggle,
export". Today `⋯` collapses the palette; that moves into the same menu as "Hide tools".

```
┌───────────────────────────┐
│ Layers │
│ ◉ 👁 Notes 12 │ ← active: new ink and notes go here
│ ○ 👁 Questions 3 │
│ ○ ⌀ Exam revision 0 │ ← hidden
│ + New layer │
├───────────────────────────┤
│ Export annotations │
│ Hide tools │
└───────────────────────────┘
```

### Rules

- **One active layer per source**, persisted in `meta` as `activeLayer:<sourceId>`. Every
new stroke and note is written into it. `ensureDefaultLayer` stays — a source with no
layers still gets one, silently, the first time it is opened.
- **Hidden layers are not drawn, not erasable, and their notes are not shown.** The eraser
hit-tests only what is visible, because erasing ink you cannot see is a destructive act
with no feedback.
- **Drawing on a hidden active layer shows it.** The alternatives — forbidding the hide, or
silently drawing invisible ink — are both worse than the layer reappearing under the pen.
- **Rename** in place. **Delete** asks once, inline (never `window.confirm`), and offers to
*move* the layer's annotations into another layer instead of deleting them. That is the
plan's "reassign": it works at layer granularity because selecting individual strokes
needs a lasso, which §13 defers.
- **The last layer cannot be deleted.** Every annotation needs a home.
- Deleting or merging a layer **clears the ink undo stack** for the open file. An undo entry
holds whole annotations, and replaying one into a layer that no longer exists would
resurrect it somewhere the user just removed.

---

## 2. Bookmarks

- **Toggle from the toolbar** (a ribbon button), and from a narrow **bookmark gutter** left
of the line numbers — click a line's gutter to set or clear a bookmark there. The gutter is
the tablet-friendly path; the button is the obvious one.
- **Which line the button means:** the cursor's line when it is on screen, otherwise the top
line of the viewport. A reader who scrolled without clicking means "here", not "line 1".
- **Default name** is the bookmarked line's own text, trimmed and clipped — `export async
function runJob(job) {` is a better label than "Bookmark 3", and costs the user nothing.
Rename inline in the list. Colour from the palette's swatches; amber by default (the
swatch list already calls it "bookmark amber").
- **Resolved when their file opens**, against the text as it stands. An unresolved bookmark
is shown in the list with a warning rather than pointed at a line it no longer describes.
- **Carried across edits** with the same line mapper as ink, and re-anchored on save.
- `Bookmark` gains an optional `updatedAt`. §3 of the plan says every record has one; this
one was missed, and Phase 6 sync will need it.

---

## 3. Typed notes

- **A fourth palette tool, "Note."** Arm it, tap a line, type. Tapping an existing note
with the tool armed — or with no tool armed at all — opens it for editing. Keeping note
creation in the palette means it works the same way with a stylus as everything else.
- **Rendered at the end of their line**, as a CodeMirror widget decoration: a quiet chip,
one line, clipped, with the note's colour as a hairline on its left edge. Because it is a
CodeMirror decoration it scrolls *with* the text by construction — there is nothing to
keep in sync. No line wrapping is configured, so a chip never changes a line's height and
ink anchored nearby is unaffected.
- **Edited in a small card** that opens next to the line: a textarea, *Done*, *Delete*.
`Escape` cancels, `Mod-Enter` saves. Saving an empty note deletes it.
- Notes are **not** on the ink undo stack. Undo there means "the last stroke", and a note
has its own explicit delete.
- The displaced tray shows a note's **text**, not just the code it was written against —
for a typed note the words are the thing worth keeping.

---

## 4. Annotation index

- The sidebar gets three views: **Files**, **Bookmarks**, **Search**. A PDF reader's
sidebar works the same way, and it keeps the toolbar from growing.
- **Search** covers every note and bookmark in the open source, whichever file it is in.
Every word typed must appear (any order, case- and accent-insensitive); the file path
counts towards a match, so `runner webcontainer` finds the WebContainer note in
`runner.ts`. With nothing typed, the view *is* the index: every note, grouped by file.
- **Click to jump.** Jumping opens the file if needed and centres the target line. For a
note, the line is corrected after the file opens to wherever the anchor ladder actually
placed it, so a note in a file edited elsewhere is still found.
- Notes in **hidden layers are still searchable**, and say so. Search is how you find
things you are not currently looking at.
- Matching is pure and synchronous over strings (`src/organise/search.ts`) and tested
table-style, like the anchor ladder.

---

## 5. Navigation

One function, `goTo(fileId, line)`, owned by the session store and used by bookmarks,
search, and anything later. It reuses the reading-position plumbing (`pendingLine`) but
asks CodeView to **centre** the line and put the cursor on it, so the active-line highlight
shows where you landed. Restoring a reading position still lands at the top, as before.

---

## 6. Export

`exportSource()` has existed since Phase 0 with nothing calling it. It gets a menu item:
one JSON file with the source, its files, layers, annotations and bookmarks. The plan calls
it the backstop for every storage risk; it should not stay unreachable. Import is not in
this phase.

---

## 7. Commit order

1. This document.
2. `goTo` and line centring — the shared navigation primitive.
3. Layers: data helpers, store, ink integration, the overflow menu.
4. Export.
5. Bookmarks: store, gutter, toolbar toggle, sidebar views.
6. Typed notes: the tool, the widgets, the card, the tray.
7. Search: the matcher and the sidebar view.

Each commit builds, typechecks and passes the suite on its own.

---

## 8. Testing

| What | How |
|---|---|
| Moving and counting a layer's annotations | Vitest + fake-indexeddb, alongside `db.test.ts` |
| Which annotations are visible / erasable | Pure function, table-driven |
| Bookmark default names, resolution, shifting | Pure functions over `Anchor` |
| Search matching, ranking, snippets | Pure functions, table-driven |
| The whole flow | Headless Chrome against the dev server: draw, add a note, hide its layer, bookmark a line, search three words, jump |

Stylus feel is unchanged by this phase — no ink path is touched except the visibility
filter — but the note tool is new input handling and should be tried on a tablet.

---

## 9. Decided without asking — reverse if wrong

| Call | Why | Cheapest reversal |
|---|---|---|
| Layers live in the palette overflow, not the toolbar | §7 reserved the overflow for them; the toolbar stays minimal | Move the panel component |
| Note is a palette tool | Same gesture as every other annotation; works with a stylus | Replace with a toolbar button |
| Notes render at the end of their line | Free scroll sync; no Code Space needed | Phase 5 can move them into bands |
| Sidebar tabs instead of more toolbar buttons | PDF-reader convention; keeps the toolbar quiet | Swap the tab header for buttons |
| Hidden-layer notes still appear in search | Search is for finding what you are not looking at | One filter in the search view |

## 10. Deliberately not in this phase

- **Lasso / selection** of individual annotations — §13. Reassignment is per layer.
- **Layer reordering** by drag. `order` is stored; nothing reads it except list order.
- **Handwriting OCR** — §13. Typed notes are what search covers.
- **Import** of an exported file.
Loading
Loading