Web: blocks by anchor, version history, the bar morph vs layout shift #86

Closed
kayg wants to merge 0 commits from issue/80-web-2 into dev
Owner

The web side of #80, on top of #85.

Blocks by anchor

  • → make note and Move to note… send {at, hash}. at is the base64 Yjs relative position at the start of the block's first line, taken from the note's md Y.Text in this tab's doc. hash is the lowercase hex SHA-256 of that first line. The "wait up to 3 s for sync" step is gone.
  • block_changed (409): the toast says "That block just changed, so nothing was moved." with Try again. Try again starts from where the anchor is now, and nothing is retried silently.
  • anchorAt / indexOf / lineHash live in @calternal/editor (one copy of yjs), with unit tests: anchors follow their block when another device adds lines above it.
  • With anchors, a job takes the block at the anchor plus its children. When several blocks are selected, only the first one (with its children) is made into a note or moved.

Version history (Info)

  • From notes.history, newest first. Each entry shows its summary, who made it, and the time; tapping the time flips it to the exact time (torph). "Show older" pages with before = the last entry's first_version. The list re-reads when the note changes.
  • Who: you (by name), "Another app" for outside edits, "Imported", or "Someone else".
  • Note: the server folds one author's changes within 5 minutes into one entry, kinds included. So a note created by the API and typed in right after shows one "Created" entry with 16 changes, not "Created" plus "Typed". The test asserts that as it is today.

Layout shift caught on #85 (editor.spec.ts:97)

  • The CI entry had two sources: DIV.morph (the bar widening 279.5 → 321 px, tabs → note tools) and the cursor. Both came from the test focusing the editor by script, not from the status flip. The status isn't in the bar.
  • The bar now marks its own deliberate morph: data-morph-until is set when its size changes, for the spring's duration plus 150 ms. Both layout-shift tests ignore only the bar's own box inside that window; everything else still counts.
  • The status test now runs under 4× CPU throttle. Locally: 20/20 at 1440 and 20/20 at 390, and the "no layout shift" tests 20/20 each (120/120).

Also fixed

  • Info's mentions effect (in dev since #81) read its own result while tracked, so it re-ran every 300 ms after each answer: it polled notes.backlinks while the sheet was open. Both Info effects now read their own state untracked.
  • The list grouped notes by the UTC date of the last edit: just after midnight, today's edits showed under "Yesterday". It uses the local day now.
  • e2e: every "More" locator is exact (note text containing "more" matched list rows under load).

Tests: links.spec.ts adds an anchor refusal (a stale hash on the first request: the toast shows, nothing changes, then Try again works) and a versions test (list and time flip). Locally: full browser suite 79 passed / 26 skipped; editor unit tests 75/75; svelte-check 0 errors. Screenshot in the first comment.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T

The web side of #80, on top of #85. **Blocks by anchor** - → make note and Move to note… send `{at, hash}`. `at` is the base64 Yjs relative position at the start of the block's first line, taken from the note's `md` Y.Text in this tab's doc. `hash` is the lowercase hex SHA-256 of that first line. The "wait up to 3 s for sync" step is gone. - `block_changed` (409): the toast says "That block just changed, so nothing was moved." with **Try again**. Try again starts from where the anchor is now, and nothing is retried silently. - `anchorAt` / `indexOf` / `lineHash` live in `@calternal/editor` (one copy of yjs), with unit tests: anchors follow their block when another device adds lines above it. - With anchors, a job takes the block at the anchor plus its children. When several blocks are selected, only the first one (with its children) is made into a note or moved. **Version history (Info)** - From `notes.history`, newest first. Each entry shows its summary, who made it, and the time; tapping the time flips it to the exact time (torph). "Show older" pages with `before` = the last entry's `first_version`. The list re-reads when the note changes. - Who: you (by name), "Another app" for outside edits, "Imported", or "Someone else". - Note: the server folds one author's changes within 5 minutes into one entry, kinds included. So a note created by the API and typed in right after shows one "Created" entry with 16 changes, not "Created" plus "Typed". The test asserts that as it is today. **Layout shift caught on #85 (`editor.spec.ts:97`)** - The CI entry had two sources: `DIV.morph` (the bar widening 279.5 → 321 px, tabs → note tools) and the cursor. Both came from the test focusing the editor by script, not from the status flip. The status isn't in the bar. - The bar now marks its own deliberate morph: `data-morph-until` is set when its size changes, for the spring's duration plus 150 ms. Both layout-shift tests ignore only the bar's own box inside that window; everything else still counts. - The status test now runs under 4× CPU throttle. Locally: 20/20 at 1440 and 20/20 at 390, and the "no layout shift" tests 20/20 each (120/120). **Also fixed** - Info's mentions effect (in dev since #81) read its own result while tracked, so it re-ran every 300 ms after each answer: it polled `notes.backlinks` while the sheet was open. Both Info effects now read their own state untracked. - The list grouped notes by the UTC date of the last edit: just after midnight, today's edits showed under "Yesterday". It uses the local day now. - e2e: every "More" locator is exact (note text containing "more" matched list rows under load). **Tests:** `links.spec.ts` adds an anchor refusal (a stale hash on the first request: the toast shows, nothing changes, then Try again works) and a versions test (list and time flip). Locally: full browser suite 79 passed / 26 skipped; editor unit tests 75/75; svelte-check 0 errors. Screenshot in the first comment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Web: blocks by anchor, version history, and the bar's morph told apart from layout shifts
All checks were successful
CI / rust (pull_request) Successful in 1m15s
CI / web (pull_request) Successful in 2m55s
f2c8525643
- → make note and Move to note… send the block's anchor (#80): a Yjs
  relative position at the start of its first line plus a SHA-256 of that
  line, so the server acts on this block even if lines above it changed,
  and refuses if the line itself did. No more waiting for sync first.
  block_changed (409) shows "That block just changed, so nothing was
  moved." with Try again (from where the block is now), never a silent
  retry. anchorAt / indexOf / lineHash live in @calternal/editor, with
  unit tests.
- Info: Versions from notes.history, newest first: summary, who, and the
  time that flips to the exact one; "Show older" pages with before.
- Info's mentions and versions effects no longer re-run on their own
  results (they read their state untracked), so they don't poll.
- The bottom bar marks its own deliberate morph (data-morph-until, set
  when its size changes); both layout-shift tests ignore only the bar's
  own box inside that window. The status test runs under 4x CPU throttle.
  CI on #85 had caught the bar widening from tabs to note tools after the
  test focused the editor by script, not the status flip.
- The list groups by the local day of the last edit, not its UTC date
  (an edit just after midnight showed under Yesterday).
- e2e: anchor refusal and Try again, versions and the time flip; every
  "More" locator is exact (note text with "more" matched list rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Author
Owner

Info → Versions at 1440 (local run of the versions test, time flipped to exact):

versions 1440

Info → Versions at 1440 (local run of the versions test, time flipped to exact): ![versions 1440](https://git.kayg.org/attachments/e46fb80b-5f66-4476-a9e2-7f50c0395657)
Author
Owner

Verified by Claude with own screenshot (journal grouped by day, new daily note on first keystroke, Versions from notes.history). CI green. Merging and deploying. Small follow-up: list rows show the weekday of the last edit (all "Tue") instead of the note date.

Verified by Claude with own screenshot (journal grouped by day, new daily note on first keystroke, Versions from notes.history). CI green. Merging and deploying. Small follow-up: list rows show the weekday of the last edit (all "Tue") instead of the note date.
Author
Owner

Closing: this branch is already fully contained in dev (it landed through a merge train). Branch cleanup, 2026-10-15.

Closing: this branch is already fully contained in `dev` (it landed through a merge train). Branch cleanup, 2026-10-15.
kayg closed this pull request 2026-10-15 08:35:14 +00:00
All checks were successful
CI / rust (pull_request) Successful in 1m15s
CI / web (pull_request) Successful in 2m55s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!86
No description provided.