Links say what happened to their note; # tag picker; link and handle contrast (#448, #449, #450) #464

Closed
kayg wants to merge 3 commits from issue/448-links-app into dev
Owner

Builds kayg's approved links, tags and contrast design (mockup PR #457, links.html) into the app. Do not merge: kayg gives the final go.

Closes #448, closes #449, closes #450.

What it does

Links read like words (#450, chip=icon-accent). No pill and no underline at rest: a small icon for what the link points at (note, day, photo, file, web) and the words in a deep tint of the accent. The pill shows on hover or with the caret inside; its room is always there, so no word moves, and an equal negative margin means nothing sits between a link and the full stop after it.

Links to notes that aren't there (#448).

  • Never written: a note-with-+ icon at 70% ink (icondim). A click, or ⌘↵ / Ctrl+Enter with the caret inside, makes the note at the path the link points to, titled with the link's words, opens it (caret ready under the title on a wide screen; on a phone the keyboard goes, so the toast stays), and toasts "Made “X” · Undo". Undo is the job's undo: the note is deleted for real (not to the Trash) if nobody typed in it, and the link reads as never written again.
  • In the Trash: the trash icon replaces the note icon. A card shows the note's first lines read only, with Restore ⌘↵ (the link works again) and Open (the whole note, in the card; shown when there is more than the first lines).
  • Deleted for good: "“X” was deleted on 12 Oct", with Create a new note ⌘↵ and Remove link (the words stay; Undo puts the link back).
  • Moved outside the app: found by its id, the server repoints the links in the open note, and one toast says "Link updated". Renames in the app already repoint every link (unchanged).
  • Cards and pickers use the shared #319 glass (Glass dense / float-glass-behind dense), and close on Esc (caret back where it was), a click elsewhere, the caret leaving what they complete, and any move to another note or screen. ⌘↵ shows a card at once; a click springs it out of the link.
  • There was no "Linked from … · unlinked mentions" line above the title in the app; a test now keeps it that way.

Tags (#449). # and a letter opens the same glass list as [[ / @: prefix matches first, then loose ones; nested tags one level at a time (Tab on area gives #area/ and its children); each row shows its note count; Enter or Tab takes a row, Space keeps typing, Esc closes; no "create" row; #12 never opens it. Counts come from the note list, worked out once per list change.

Handle and → ⋯ pill (#450, handle=ink). Full-ink icons on the dense floating glass.

How (plugins, small files)

  • Server (crates/notes): history/departed.rs keeps one row per path a note left (renamed, moved outside, sent to the Trash; marked when deleted for good), fed from the notes events as they are recorded. missing.rs answers notes.link-states {paths} (≤200, in order, paths never echoed) with one index lookup per path and one read of that table for the rest: never a walk of the Home. missing/make.rs is notes.make-from-link {path, title} (a one-step job); missing/relink.rs is notes.relink {id, from} (through the room, keyed, so two devices change the text once). Outside renames now carry from in their event. Per user throughout; the isolation gate now also attacks paths with Alice's own file name.
  • Editor (packages/editor): link kinds and a linkState facet the app answers; linkAt; transforms/picker.ts (one autocompletion for [[, @ and #), tags.ts, tag-rank.ts (unit-tested); a surface class option for the margin controls.
  • App (apps/web/src/lib/links/): states.ts (answers kept per path for the page, asked in 30 ms batches, re-asked quietly 400 ms after a note comes, goes or moves), cm-links.ts (drawing, press-release follow, ⌘↵), follow.ts, card.svelte.ts + LinkCard.svelte, relink.ts, peek-lines.ts, resolve.ts. The old regex scan of visible lines on every change (missingLinks) is gone.
  • Docs: ADR 0040, CONTEXT.md, TASTE.md.

Performance

Nothing runs per keystroke: drawing a link reads two maps; the server is asked once per batch of unknown paths, never while typing (a test types into a note with 60 never-written links and counts 0 requests).

Typing cost in a note with 200 links (half to existing notes, half never written), in-page dispatch-to-layout per keystroke, 300 keystrokes, 3 runs each, host load 25–30 on 12 cores:

before p50 / p95 after p50 / p95
Chromium, at the end 6.8–8.3 / 23–31 ms 5.7–6.8 / 18–21 ms
Chromium, mid-note 8.8–9.9 / 31–48 ms 7.6–8.7 / 15–25 ms
WebKit, at the end 7–10 / 37–46 ms 6–8 / 22–54 ms
WebKit, mid-note 7–9 / 30–39 ms 7–8 / 20–24 ms

(The existing keystroke-to-paint @perf test was also run before: on this loaded host it measured 300+ ms p50 either way, so the in-page probe is what's comparable.)

light paper dark paper light photo, page 0% dark photo, page 0%
link words / icon 9.2 / 9.2 7.3 / 7.3 9.0 / 9.1 6.1 / 6.1
never written, words / icon 5.4 / 5.4 7.0 / 7.0 4.9 / 5.6 6.3 / 5.7
in the Trash, words / icon 5.4 / 5.4 7.0 / 7.0 5.0 / 4.9 6.3 / 6.4
handle / → / ⋯ 14.3 / 14.3 / 14.3 13.7 / 13.5 / 13.5 13.4 / 10.0 / 10.3 7.8 / 9.5 / 9.4

One deviation for kayg to judge: over a photo with the page at 0%, the approved no-pill look measured 1.2–1.5:1 at the worst spots (the photo here is busier than the mockup's). On pages under 30% opacity only, a link now sits on a soft wash of paper (no edge, no shadow); on paper nothing changes. The editor's text also gets the faint-page halo it was meant to have (it targeted the mockup's .body, not .cm-content).

Tests

  • New: missing-links.spec.ts (10), links-look.spec.ts (4), tag-picker.spec.ts (4), link-contrast.spec.ts (contrast project), crates/notes/src/tests/missing.rs (7), packages/editor/test/tag-rank.test.ts (5).
  • Rust: cargo fmt --check clean, clippy --workspace --all-targets -D warnings clean, workspace tests 429 passed, 0 failed (incl. the isolation gate, which now also attacks paths). Editor unit tests 117 passed. svelte-check 0 errors; action types match the contract.
  • Browser (--no-deps, chromium-390, webkit-390, chromium-1440, webkit-1440; the new specs plus links, chips, picker, tags, tag-rename, trash, editor, shortcuts, keys-, bar-, glass): full run all green apart from three on a host at load 30–55: trash.spec "a deleted note waits in the Trash" (WebKit 1440, also fails on the base branch: "element is not stable"), and timeouts in bar-focus (WebKit 390) and tag-rename Undo (WebKit 1440), both passing when rerun on this branch. The link specs were rerun on all four projects after the last fixes: 17/17, 17/17, 27/27, 26/27 (the same Trash test).
  • Contrast project: link-contrast.spec.ts passes (table above).
  • Two existing tests changed: bar-stays clicked the middle of a line whose middle is a link (a click now follows a link, so it clicks the line's start), and trash.spec looks for the Trash icon class instead of the old cm-link-missing.

Screenshots and the simulator run are on #448, #449 and #450.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T

Builds kayg's approved links, tags and contrast design (mockup PR #457, `links.html`) into the app. **Do not merge: kayg gives the final go.** Closes #448, closes #449, closes #450. ## What it does **Links read like words (#450, `chip=icon-accent`).** No pill and no underline at rest: a small icon for what the link points at (note, day, photo, file, web) and the words in a deep tint of the accent. The pill shows on hover or with the caret inside; its room is always there, so no word moves, and an equal negative margin means nothing sits between a link and the full stop after it. **Links to notes that aren't there (#448).** - Never written: a note-with-+ icon at 70% ink (`icondim`). A click, or ⌘↵ / Ctrl+Enter with the caret inside, makes the note at the path the link points to, titled with the link's words, opens it (caret ready under the title on a wide screen; on a phone the keyboard goes, so the toast stays), and toasts "Made “X” · Undo". Undo is the job's undo: the note is deleted for real (not to the Trash) if nobody typed in it, and the link reads as never written again. - In the Trash: the trash icon replaces the note icon. A card shows the note's first lines read only, with Restore ⌘↵ (the link works again) and Open (the whole note, in the card; shown when there is more than the first lines). - Deleted for good: "“X” was deleted on 12 Oct", with Create a new note ⌘↵ and Remove link (the words stay; Undo puts the link back). - Moved outside the app: found by its id, the server repoints the links in the open note, and one toast says "Link updated". Renames in the app already repoint every link (unchanged). - Cards and pickers use the shared #319 glass (`Glass dense` / `float-glass-behind dense`), and close on Esc (caret back where it was), a click elsewhere, the caret leaving what they complete, and any move to another note or screen. ⌘↵ shows a card at once; a click springs it out of the link. - There was no "Linked from … · unlinked mentions" line above the title in the app; a test now keeps it that way. **Tags (#449).** `#` and a letter opens the same glass list as `[[` / `@`: prefix matches first, then loose ones; nested tags one level at a time (Tab on `area` gives `#area/` and its children); each row shows its note count; Enter or Tab takes a row, Space keeps typing, Esc closes; no "create" row; `#12` never opens it. Counts come from the note list, worked out once per list change. **Handle and → ⋯ pill (#450, `handle=ink`).** Full-ink icons on the dense floating glass. ## How (plugins, small files) - Server (`crates/notes`): `history/departed.rs` keeps one row per path a note left (renamed, moved outside, sent to the Trash; marked when deleted for good), fed from the notes events as they are recorded. `missing.rs` answers `notes.link-states {paths}` (≤200, in order, paths never echoed) with one index lookup per path and one read of that table for the rest: never a walk of the Home. `missing/make.rs` is `notes.make-from-link {path, title}` (a one-step job); `missing/relink.rs` is `notes.relink {id, from}` (through the room, keyed, so two devices change the text once). Outside renames now carry `from` in their event. Per user throughout; the isolation gate now also attacks `paths` with Alice's own file name. - Editor (`packages/editor`): link kinds and a `linkState` facet the app answers; `linkAt`; `transforms/picker.ts` (one autocompletion for `[[`, `@` and `#`), `tags.ts`, `tag-rank.ts` (unit-tested); a `surface` class option for the margin controls. - App (`apps/web/src/lib/links/`): `states.ts` (answers kept per path for the page, asked in 30 ms batches, re-asked quietly 400 ms after a note comes, goes or moves), `cm-links.ts` (drawing, press-release follow, ⌘↵), `follow.ts`, `card.svelte.ts` + `LinkCard.svelte`, `relink.ts`, `peek-lines.ts`, `resolve.ts`. The old regex scan of visible lines on every change (`missingLinks`) is gone. - Docs: ADR 0040, CONTEXT.md, TASTE.md. ## Performance Nothing runs per keystroke: drawing a link reads two maps; the server is asked once per batch of unknown paths, never while typing (a test types into a note with 60 never-written links and counts 0 requests). Typing cost in a note with 200 links (half to existing notes, half never written), in-page dispatch-to-layout per keystroke, 300 keystrokes, 3 runs each, host load 25–30 on 12 cores: | | before p50 / p95 | after p50 / p95 | |---|---|---| | Chromium, at the end | 6.8–8.3 / 23–31 ms | 5.7–6.8 / 18–21 ms | | Chromium, mid-note | 8.8–9.9 / 31–48 ms | 7.6–8.7 / 15–25 ms | | WebKit, at the end | 7–10 / 37–46 ms | 6–8 / 22–54 ms | | WebKit, mid-note | 7–9 / 30–39 ms | 7–8 / 20–24 ms | (The existing keystroke-to-paint @perf test was also run before: on this loaded host it measured 300+ ms p50 either way, so the in-page probe is what's comparable.) ## Contrast (measured, `e2e/link-contrast.spec.ts`, worst spot) | | light paper | dark paper | light photo, page 0% | dark photo, page 0% | |---|---|---|---|---| | link words / icon | 9.2 / 9.2 | 7.3 / 7.3 | 9.0 / 9.1 | 6.1 / 6.1 | | never written, words / icon | 5.4 / 5.4 | 7.0 / 7.0 | 4.9 / 5.6 | 6.3 / 5.7 | | in the Trash, words / icon | 5.4 / 5.4 | 7.0 / 7.0 | 5.0 / 4.9 | 6.3 / 6.4 | | handle / → / ⋯ | 14.3 / 14.3 / 14.3 | 13.7 / 13.5 / 13.5 | 13.4 / 10.0 / 10.3 | 7.8 / 9.5 / 9.4 | **One deviation for kayg to judge:** over a photo with the page at 0%, the approved no-pill look measured 1.2–1.5:1 at the worst spots (the photo here is busier than the mockup's). On pages under 30% opacity only, a link now sits on a soft wash of paper (no edge, no shadow); on paper nothing changes. The editor's text also gets the faint-page halo it was meant to have (it targeted the mockup's `.body`, not `.cm-content`). ## Tests - New: `missing-links.spec.ts` (10), `links-look.spec.ts` (4), `tag-picker.spec.ts` (4), `link-contrast.spec.ts` (contrast project), `crates/notes/src/tests/missing.rs` (7), `packages/editor/test/tag-rank.test.ts` (5). - Rust: `cargo fmt --check` clean, `clippy --workspace --all-targets -D warnings` clean, workspace tests 429 passed, 0 failed (incl. the isolation gate, which now also attacks `paths`). Editor unit tests 117 passed. `svelte-check` 0 errors; action types match the contract. - Browser (`--no-deps`, chromium-390, webkit-390, chromium-1440, webkit-1440; the new specs plus links, chips, picker, tags, tag-rename, trash, editor, shortcuts, keys-*, bar-*, glass): full run all green apart from three on a host at load 30–55: `trash.spec` "a deleted note waits in the Trash" (WebKit 1440, also fails on the base branch: "element is not stable"), and timeouts in `bar-focus` (WebKit 390) and `tag-rename` Undo (WebKit 1440), both passing when rerun on this branch. The link specs were rerun on all four projects after the last fixes: 17/17, 17/17, 27/27, 26/27 (the same Trash test). - Contrast project: `link-contrast.spec.ts` passes (table above). - Two existing tests changed: `bar-stays` clicked the middle of a line whose middle is a link (a click now follows a link, so it clicks the line's start), and `trash.spec` looks for the Trash icon class instead of the old `cm-link-missing`. Screenshots and the simulator run are on #448, #449 and #450. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
- Links read like words: accent words after a kind icon (note, day, photo, file, web), no pill or
  underline at rest, the pill on hover or with the caret inside, nothing before the full stop.
- A link to a note never written makes it at that path (notes.make-from-link, a job Undo takes
  back); one in the Trash opens a card with its first lines, Restore and Open; one deleted for good
  names the day, with Create a new note and Remove link. ⌘↵ does what a click does.
- History remembers where notes used to live (departed); notes.link-states answers a batch of
  paths with index lookups and one read; notes.relink repoints links to a note moved outside.
- `#` and a letter offers the tags that exist, nested one level at a time, with counts.
- The drag handle and the → ⋯ pill are ink icons on the dense floating glass.
- Faint pages give the editor's text its halo, and links a wash of paper, to keep 4.5:1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
- The caret lands at the end of a note opened to type in (a note made from a link, under its title).
- Answers are asked again 400 ms after a burst of notes coming, going or moving, not per event.
- Tests: toasts found by their words at every width; a day link to a day that exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Links on a phone: the toast comes first and the keyboard goes when a link is followed (#448)
Some checks failed
CI / server (pull_request) Successful in 30s
CI / rust (pull_request) Successful in 2m10s
CI / web (pull_request) Failing after 40m6s
75723cc496
The editor is one view for every note, so a tap on a link kept the caret, and the phone's text bar
swept the "Made" toast and its Undo away. Following a link now lets the keyboard go (except for a
card, which keeps the caret to come back to), and the toast is sent before the move so it rides
along to the new note's bar.

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

Replaced by #465 (review fixes: day links open their day, link looks follow changes made elsewhere). Closing this one.

Replaced by #465 (review fixes: day links open their day, link looks follow changes made elsewhere). Closing this one.
kayg closed this pull request 2026-10-15 23:21:02 +00:00
Some checks failed
CI / server (pull_request) Successful in 30s
CI / rust (pull_request) Successful in 2m10s
CI / web (pull_request) Failing after 40m6s

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!464
No description provided.