Note editor: arrow cursor over block-selection zones; Cmd-click and Shift-click multi-block selection #1125

Open
opened 2026-10-05 08:24:12 +00:00 by kayg · 11 comments
Owner

Owner report (2026-10-05, Note editor)

  1. Cursor while hovering a block: hovering a block shows the text I-beam even where a click selects the whole block (the block gutter and the block hover zone with the → / clock / ⋯ pill). Show the normal arrow cursor wherever a click selects the block or opens its actions, and the I-beam only over editable text where a click places the caret.
  2. Multi-block selection: once a block is selected, Cmd/Ctrl-click another block toggles it into the selection, and Shift-click selects the contiguous range from the anchor block. Keyboard equivalents: Shift+↑/↓ extends, Cmd/Ctrl+A in block mode selects all blocks, Escape clears. Selected blocks support the existing block actions together (move, delete with Undo, copy link to each, duplicate, convert). Touch: long-press a block to enter selection mode, then tap to toggle.
## Owner report (2026-10-05, Note editor) 1. **Cursor while hovering a block:** hovering a block shows the text I-beam even where a click selects the whole block (the block gutter and the block hover zone with the → / clock / ⋯ pill). Show the normal arrow cursor wherever a click selects the block or opens its actions, and the I-beam only over editable text where a click places the caret. 2. **Multi-block selection:** once a block is selected, Cmd/Ctrl-click another block toggles it into the selection, and Shift-click selects the contiguous range from the anchor block. Keyboard equivalents: Shift+↑/↓ extends, Cmd/Ctrl+A in block mode selects all blocks, Escape clears. Selected blocks support the existing block actions together (move, delete with Undo, copy link to each, duplicate, convert). Touch: long-press a block to enter selection mode, then tap to toggle.
Author
Owner

Starting #1125 on job/blocks-1125, based on dev 3b58bcf532ded0def2348283a6aaa3593ee01d98. I am tracing the existing block selection, actions, and Notes integration before making a focused change. The supplied owner-editor.webp remains review input only.

Starting #1125 on `job/blocks-1125`, based on dev `3b58bcf532ded0def2348283a6aaa3593ee01d98`. I am tracing the existing block selection, actions, and Notes integration before making a focused change. The supplied `owner-editor.webp` remains review input only.
Author
Owner

Owner (2026-10-05): "The drag handle forgets to move with the hover cursor." Screenshot: in a bulleted list, the ⠿ drag handle stays beside the first item (centred on its second line) while the pointer hovers other items. The block handle must follow the hovered block (each list item is its own block), aligned to the first line of that block's text, and update on every hover change and on scroll.

Owner (2026-10-05): "The drag handle forgets to move with the hover cursor." Screenshot: in a bulleted list, the ⠿ drag handle stays beside the first item (centred on its second line) while the pointer hovers other items. The block handle must follow the hovered block (each list item is its own block), aligned to the first line of that block's text, and update on every hover change and on scroll.
Author
Owner

Starting implementation on job/blocks-1125, based on dev 3b58bcf532. The owner report and drag-handle finding are read; I am tracing the existing Note editor selection model and drag-handle positioning before editing.

Starting implementation on job/blocks-1125, based on dev 3b58bcf532ded0def2348283a6aaa3593ee01d98. The owner report and drag-handle finding are read; I am tracing the existing Note editor selection model and drag-handle positioning before editing.
Author
Owner

Finding: packages/editor/src/extensions.ts currently maps every hover to topLevelBlockStart() before choosing the handle DOM, so every list item shares the enclosing list's anchor. NoteEditorSurface.svelte stores selected positions but handles only Shift-click ranges; it has no Cmd/Ctrl toggle, Shift+Arrow extension or block-mode select-all. Multi-block menu plumbing exists, but Copy link is disabled and delete has no scoped Undo toast. I will extend these existing paths.

Finding: packages/editor/src/extensions.ts currently maps every hover to topLevelBlockStart() before choosing the handle DOM, so every list item shares the enclosing list's anchor. NoteEditorSurface.svelte stores selected positions but handles only Shift-click ranges; it has no Cmd/Ctrl toggle, Shift+Arrow extension or block-mode select-all. Multi-block menu plumbing exists, but Copy link is disabled and delete has no scoped Undo toast. I will extend these existing paths.
Author
Owner

Finding: node --check apps/web/e2e/notes.mjs failed before running Notes e2e with SyntaxError: Identifier 'remoteImage' has already been declared at line 1857. The same const name was declared earlier at line 1756 in the same async flow. I renamed the proxy-image locator so the requested Notes e2e can parse and run.

Finding: `node --check apps/web/e2e/notes.mjs` failed before running Notes e2e with `SyntaxError: Identifier 'remoteImage' has already been declared` at line 1857. The same `const` name was declared earlier at line 1756 in the same async flow. I renamed the proxy-image locator so the requested Notes e2e can parse and run.
Author
Owner

Finding during production verification: deleting three consecutive selected blocks succeeded in the live ProseMirror view and created one scoped Yjs Undo item, but Yjs inverse replay restored two anchored blocks twice (model content grew from 855 to 1009 characters). The server API and rendered editor each held one copy before deletion. The Undo action now restores immutable selected-block snapshots at Yjs-relative positions and still marks the Yjs update as local history for the collaboration server. This keeps unrelated edits in place and avoids the inverse replay duplication. Verifying with the production browser run now.

Finding during production verification: deleting three consecutive selected blocks succeeded in the live ProseMirror view and created one scoped Yjs Undo item, but Yjs inverse replay restored two anchored blocks twice (model content grew from 855 to 1009 characters). The server API and rendered editor each held one copy before deletion. The Undo action now restores immutable selected-block snapshots at Yjs-relative positions and still marks the Yjs update as local history for the collaboration server. This keeps unrelated edits in place and avoids the inverse replay duplication. Verifying with the production browser run now.
Author
Owner

Additional production finding: the selected-block callback received positions [15, 154, 169] and built a changed ProseMirror transaction (document content size became 649; one scoped Undo item was recorded), but the rendered .cal-prose still showed all three blocks after 30 seconds. The test caught a model/view mismatch, so I am restoring the single-range transform used for contiguous selections and checking that the dispatched view and visible surface are the same instance.

Additional production finding: the selected-block callback received positions [15, 154, 169] and built a changed ProseMirror transaction (document content size became 649; one scoped Undo item was recorded), but the rendered `.cal-prose` still showed all three blocks after 30 seconds. The test caught a model/view mismatch, so I am restoring the single-range transform used for contiguous selections and checking that the dispatched view and visible surface are the same instance.
Author
Owner

Root cause evidence: the delete callback sends a Yjs update (26 bytes) and the server sends a second update (460 bytes) within 1.5 s. The local ProseMirror view briefly omits the three blocks, then the server update restores them; the Note API still has the original Markdown. The initial delete used a Symbol origin, so it did not carry the existing local-history marker. The collaboration conflict-shadow path then reconciled it against the older Markdown baseline. I am marking the scoped delete and its snapshot restore with the scoped UndoManager origin so the server keeps the live pre-delete state and does not replay the stale baseline.

Root cause evidence: the delete callback sends a Yjs update (26 bytes) and the server sends a second update (460 bytes) within 1.5 s. The local ProseMirror view briefly omits the three blocks, then the server update restores them; the Note API still has the original Markdown. The initial delete used a Symbol origin, so it did not carry the existing local-history marker. The collaboration conflict-shadow path then reconciled it against the older Markdown baseline. I am marking the scoped delete and its snapshot restore with the scoped UndoManager origin so the server keeps the live pre-delete state and does not replay the stale baseline.
Author
Owner

Finding on #1125: the production focused E2E showed that Backspace removed the selected blocks but no Undo action appeared. The delete handler still called a temporary record(...) diagnostic after its helper was removed. That unbound call interrupted the handler before the Undo toast. I removed the call. I am rebuilding and rerunning the focused production E2E now.

Finding on #1125: the production focused E2E showed that Backspace removed the selected blocks but no Undo action appeared. The delete handler still called a temporary `record(...)` diagnostic after its helper was removed. That unbound call interrupted the handler before the Undo toast. I removed the call. I am rebuilding and rerunning the focused production E2E now.
Author
Owner

Finding: after a heading received its first stable block ID, touch Copy Link announced the heading as Goals ^<id> (observed in the focused production E2E). The heading widget used raw ProseMirror text for its title. It now strips the trailing storage token with the existing withoutTrailingBlockAnchor helper. A regression test checks that the action keeps the authored heading name after ID assignment; the focused Vitest suite passed 5 files and 19 tests, and the #1125 production E2E passed.

Finding: after a heading received its first stable block ID, touch Copy Link announced the heading as `Goals ^<id>` (observed in the focused production E2E). The heading widget used raw ProseMirror text for its title. It now strips the trailing storage token with the existing `withoutTrailingBlockAnchor` helper. A regression test checks that the action keeps the authored heading name after ID assignment; the focused Vitest suite passed 5 files and 19 tests, and the #1125 production E2E passed.
Author
Owner

Finished

Implemented Note block selection and hover-handle behavior for #1125.

READY FOR MERGE: no. bun run check stops on a stale perf-lint exception in apps/web/src/lib/components/app-sidebar.svelte, outside this change. Direct Svelte checking and the focused suites pass.

Head SHA: 6cc162e864195fae628996900b39328d537a194e

Built

  • Cmd/Ctrl-click toggles blocks; Shift-click selects sibling ranges; Shift+Arrow extends; Cmd/Ctrl+A selects all; Escape clears. Touch long-press enters selection mode.
  • Block zones use an arrow cursor and editable text uses an I-beam. The handle follows each selectable block, aligns to its first line, and updates during scroll under a stationary pointer.
  • The action bar and menu operate on the whole selection. Multi-block Copy link copies one stable URL per block. Delete Undo restores only the deleted blocks at their Yjs relative positions.
  • Copying a heading keeps selection active. Its accessible name and announcement omit the stored ^block-id token.

Files

packages/editor/src/blockSelection.ts, packages/editor/src/blockSelection.test.ts, packages/editor/src/extensions.ts, packages/editor/src/extensions.dragHandle.test.ts; apps/web/src/lib/editor/blockMenu.ts, blockMenu.test.ts, blockToolbarActions.ts, blockToolbarActions.test.ts; apps/web/src/lib/notes/NoteEditorSurface.svelte, NoteView.svelte, anchors.ts, anchors.svelte.test.ts, headingLinks.ts, headingWidgets.svelte.test.ts; apps/web/e2e/notes.mjs; contracts/perf/exceptions.json; bench/notes-editor-series.mjs; docs/perf/baseline.json.

UX gaps closed / left

Closed: selected actions keep the selection; Escape clears it; touch selection works; Copy link speaks the authored heading title; delete has Undo; handle position follows hover and scroll. No UX gap remained in the focused #1125 flow.

Left: the full Notes E2E and full web Vitest suite are deferred to the merge round under the shared verification policy.

Decisions not stated in DESIGN

  • Nested range selection stays within the same parent. Each list item remains an independent block.
  • Undo uses a scoped Yjs origin and restores saved nodes at relative positions. The normal inverse for nested deletes duplicated anchors and could replay an older Markdown baseline.
  • Multi-block Copy link writes newline-separated stable URLs as plain text, because the single-link helper accepts one URL.

Performance

Extended bench/notes-editor-series.mjs and recorded the profile in docs/perf/baseline.json. The local run used 64 blocks / 4 sections and 900 blocks / 18 sections. No like-for-like selection baseline exists. Host load was high (before: 11.99, 20.74, 24.52; after: 13.88, 19.70, 23.91); treat these as local measurements, not a regression comparison. The perf VM was not used.

Average Note: Shift-click 16 blocks p50/p95 283.3/396.4 ms; Control-click toggle 151.5/267.1 ms; Shift+Arrow 27.9/42.9 ms. Server mean/peak RSS was 323,806,822/575,139,840 bytes and mean/peak CPU was 60.28/228.01%.

Worst Note: Shift-click 50 blocks p50/p95 232.7/397.1 ms; Control-click 159.1/234.9 ms; Shift+Arrow 50/115.8 ms. A 200-event pointer burst took 15,556.3 ms. Server mean/peak RSS was 584,590,086/596,832,256 bytes and mean/peak CPU was 4.86/75.94%.

Verification

bun run check failed before Svelte diagnostics. Verbatim output:

$ ../../scripts/perf-lint --check && node scripts/check-user-storage.mjs && node scripts/check-glass-tokens.mjs && node scripts/check-type-tokens.mjs && node scripts/check-focus-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
perf-lint: parse Rust product sources
perf-lint: parse browser product sources
perf-lint: validate coverage and architecture
perf-lint: INVALID: ('render.bound', 'apps/web/src/lib/components/app-sidebar.svelte', 'apps/web/src/lib/components/app-sidebar.svelte#each:10a1aaf3137a2426:1'): unused or changed exception

Other outputs:

  • Direct web check: svelte-check found 0 errors and 4 warnings in 3 files.
  • Editor check: svelte-check found 0 errors and 0 warnings.
  • Editor tests: Test Files 21 passed (21); Tests 437 passed (437).
  • Focused web Vitest: Test Files 5 passed (5); Tests 19 passed (19).
  • Production E2E (CALTERNAL_E2E_MAC=1 NOTES_E2E_BLOCK_SELECTION_ONLY=1): PASS Cmd-click toggle, Shift-click range, Shift+Arrow extension and scoped Undo for three blocks; PASS Note drag handle follows hovered list items, aligns to their first line and updates under a stationary pointer after scroll; notes #1125 block selection e2e: multi-select, keyboard, cursor, handle and touch checks passed; CSP REPORTS notes: 0 across 3 pages.
  • cargo clean: Removed 8626 files, 7.2GiB total.

Screenshots

Captured from the production build with macOS platform emulation. These are attached to this issue.

Selection: 390 light, 820 light, 1440 light, 390 dark, 820 dark, 1440 dark.

Actions: 850px edge, light, 850px edge, dark, 1440px tooltip, light, 1440px tooltip, dark, 1440px menu, light, 1440px menu, dark.

For the merge round

  • cd apps/web && bun run check — rerun after the stale app-sidebar.svelte perf exception is reconciled.
  • cd apps/web && bun run test — run the full web Vitest suite.
  • cd apps/web && bun e2e/notes.mjs — run the full Notes E2E suite; this job ran the focused #1125 flow.
## Finished Implemented Note block selection and hover-handle behavior for #1125. **READY FOR MERGE: no.** `bun run check` stops on a stale perf-lint exception in `apps/web/src/lib/components/app-sidebar.svelte`, outside this change. Direct Svelte checking and the focused suites pass. Head SHA: `6cc162e864195fae628996900b39328d537a194e` ### Built - Cmd/Ctrl-click toggles blocks; Shift-click selects sibling ranges; Shift+Arrow extends; Cmd/Ctrl+A selects all; Escape clears. Touch long-press enters selection mode. - Block zones use an arrow cursor and editable text uses an I-beam. The handle follows each selectable block, aligns to its first line, and updates during scroll under a stationary pointer. - The action bar and menu operate on the whole selection. Multi-block Copy link copies one stable URL per block. Delete Undo restores only the deleted blocks at their Yjs relative positions. - Copying a heading keeps selection active. Its accessible name and announcement omit the stored `^block-id` token. ### Files `packages/editor/src/blockSelection.ts`, `packages/editor/src/blockSelection.test.ts`, `packages/editor/src/extensions.ts`, `packages/editor/src/extensions.dragHandle.test.ts`; `apps/web/src/lib/editor/blockMenu.ts`, `blockMenu.test.ts`, `blockToolbarActions.ts`, `blockToolbarActions.test.ts`; `apps/web/src/lib/notes/NoteEditorSurface.svelte`, `NoteView.svelte`, `anchors.ts`, `anchors.svelte.test.ts`, `headingLinks.ts`, `headingWidgets.svelte.test.ts`; `apps/web/e2e/notes.mjs`; `contracts/perf/exceptions.json`; `bench/notes-editor-series.mjs`; `docs/perf/baseline.json`. ### UX gaps closed / left Closed: selected actions keep the selection; Escape clears it; touch selection works; Copy link speaks the authored heading title; delete has Undo; handle position follows hover and scroll. No UX gap remained in the focused #1125 flow. Left: the full Notes E2E and full web Vitest suite are deferred to the merge round under the shared verification policy. ### Decisions not stated in DESIGN - Nested range selection stays within the same parent. Each list item remains an independent block. - Undo uses a scoped Yjs origin and restores saved nodes at relative positions. The normal inverse for nested deletes duplicated anchors and could replay an older Markdown baseline. - Multi-block Copy link writes newline-separated stable URLs as plain text, because the single-link helper accepts one URL. ### Performance Extended `bench/notes-editor-series.mjs` and recorded the profile in `docs/perf/baseline.json`. The local run used 64 blocks / 4 sections and 900 blocks / 18 sections. No like-for-like selection baseline exists. Host load was high (before: 11.99, 20.74, 24.52; after: 13.88, 19.70, 23.91); treat these as local measurements, not a regression comparison. The perf VM was not used. Average Note: Shift-click 16 blocks p50/p95 283.3/396.4 ms; Control-click toggle 151.5/267.1 ms; Shift+Arrow 27.9/42.9 ms. Server mean/peak RSS was 323,806,822/575,139,840 bytes and mean/peak CPU was 60.28/228.01%. Worst Note: Shift-click 50 blocks p50/p95 232.7/397.1 ms; Control-click 159.1/234.9 ms; Shift+Arrow 50/115.8 ms. A 200-event pointer burst took 15,556.3 ms. Server mean/peak RSS was 584,590,086/596,832,256 bytes and mean/peak CPU was 4.86/75.94%. ### Verification `bun run check` failed before Svelte diagnostics. Verbatim output: ```text $ ../../scripts/perf-lint --check && node scripts/check-user-storage.mjs && node scripts/check-glass-tokens.mjs && node scripts/check-type-tokens.mjs && node scripts/check-focus-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json perf-lint: parse Rust product sources perf-lint: parse browser product sources perf-lint: validate coverage and architecture perf-lint: INVALID: ('render.bound', 'apps/web/src/lib/components/app-sidebar.svelte', 'apps/web/src/lib/components/app-sidebar.svelte#each:10a1aaf3137a2426:1'): unused or changed exception ``` Other outputs: - Direct web check: `svelte-check found 0 errors and 4 warnings in 3 files`. - Editor check: `svelte-check found 0 errors and 0 warnings`. - Editor tests: `Test Files 21 passed (21)`; `Tests 437 passed (437)`. - Focused web Vitest: `Test Files 5 passed (5)`; `Tests 19 passed (19)`. - Production E2E (`CALTERNAL_E2E_MAC=1 NOTES_E2E_BLOCK_SELECTION_ONLY=1`): `PASS Cmd-click toggle, Shift-click range, Shift+Arrow extension and scoped Undo for three blocks`; `PASS Note drag handle follows hovered list items, aligns to their first line and updates under a stationary pointer after scroll`; `notes #1125 block selection e2e: multi-select, keyboard, cursor, handle and touch checks passed`; `CSP REPORTS notes: 0 across 3 pages`. - `cargo clean`: `Removed 8626 files, 7.2GiB total`. ### Screenshots Captured from the production build with macOS platform emulation. These are attached to this issue. Selection: [390 light](https://git.kayg.org/attachments/03779777-cb0a-4c0d-9039-1d8e24e41c86), [820 light](https://git.kayg.org/attachments/e9571f3d-f3b4-4945-99ce-8bb8bffea1a2), [1440 light](https://git.kayg.org/attachments/3f99f28a-da32-4e9b-86c4-4f1b4c8ac012), [390 dark](https://git.kayg.org/attachments/3c428824-5da5-44fd-be2e-65b72d0f209e), [820 dark](https://git.kayg.org/attachments/32b1cf58-f63e-4905-9240-95546131914b), [1440 dark](https://git.kayg.org/attachments/4fc902b1-8f88-4ae9-abc8-ecadfdaff214). Actions: [850px edge, light](https://git.kayg.org/attachments/be23422a-44e9-49f6-bafe-c8322dc2e745), [850px edge, dark](https://git.kayg.org/attachments/731b2cd5-92f9-4fb2-9bfc-d9f66785368e), [1440px tooltip, light](https://git.kayg.org/attachments/d6957dba-d744-443b-89df-e3269ea51d7d), [1440px tooltip, dark](https://git.kayg.org/attachments/2d1dfd0d-6726-486c-99a8-fa1f0634703f), [1440px menu, light](https://git.kayg.org/attachments/6549bcff-9c9e-4a40-a333-299588491796), [1440px menu, dark](https://git.kayg.org/attachments/1023af63-736a-4f4b-9760-f83a2d18fd61). ### For the merge round - `cd apps/web && bun run check` — rerun after the stale `app-sidebar.svelte` perf exception is reconciled. - `cd apps/web && bun run test` — run the full web Vitest suite. - `cd apps/web && bun e2e/notes.mjs` — run the full Notes E2E suite; this job ran the focused #1125 flow.
Sign in to join this conversation.
No labels
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#1125
No description provided.