Links say what happened to their note; day links open their day; # tag picker; contrast (#448, #449, #450) #465

Closed
kayg wants to merge 4 commits from issue/448-links-app-2 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. Replaces #464 (same work, plus the review fixes below).

Review fixes since #464

  • Day links: a link to a day with no note yet keeps its day icon and opens that day through daily.open (the Journal's way, id day-YYYY-MM-DD), never an ordinary note named after the date. Weeks, months and years keep their icon too.
  • Live looks: a path asked about again while a request for it was out is asked once more when the answer comes back (that answer could be from before the change), and a link whose note leaves the list asks at once. New test: a link follows made → trashed → restored → deleted for good, all done through the API as another device would (repeated 4× in Chromium and WebKit, green).
  • The simulator review showed stale looks from two causes: my earlier session there had restored "Old budget" and made "Calternal ideas", and Safari's service worker was still serving the previous build. After a fresh server, a reseed and clearing the service worker, every state shows correctly (shot on #448).
  • Soft paper wash on pages under 30% kept as it is, for kayg to decide.

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 after the review fixes: link specs (missing-links, links-look, tag-picker, links, chips, picker, trash) 24/24 chromium-390, 24/24 webkit-390, 34/34 chromium-1440, 34/34 webkit-1440; contrast project 1/1.
  • Earlier full run (--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.** Replaces #464 (same work, plus the review fixes below). ## Review fixes since #464 - **Day links:** a link to a day with no note yet keeps its day icon and opens that day through `daily.open` (the Journal's way, id `day-YYYY-MM-DD`), never an ordinary note named after the date. Weeks, months and years keep their icon too. - **Live looks:** a path asked about again while a request for it was out is asked once more when the answer comes back (that answer could be from before the change), and a link whose note leaves the list asks at once. New test: a link follows made → trashed → restored → deleted for good, all done through the API as another device would (repeated 4× in Chromium and WebKit, green). - The simulator review showed stale looks from two causes: my earlier session there had restored "Old budget" and made "Calternal ideas", and Safari's service worker was still serving the previous build. After a fresh server, a reseed and clearing the service worker, every state shows correctly (shot on #448). - Soft paper wash on pages under 30% kept as it is, for kayg to decide. 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 after the review fixes: link specs (missing-links, links-look, tag-picker, links, chips, picker, trash) 24/24 chromium-390, 24/24 webkit-390, 34/34 chromium-1440, 34/34 webkit-1440; contrast project 1/1. - Earlier full run (`--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
Links: day links open their day; link looks follow changes made elsewhere (#448)
Some checks failed
CI / server (pull_request) Successful in 22s
CI / rust (pull_request) Successful in 1m46s
CI / web (pull_request) Failing after 48m32s
d472e6a4a6
- A link to a day with no note yet keeps its day icon and opens that day through daily.open (the
  Journal's way), never an ordinary note named after the date. Weeks, months and years keep their
  icon too.
- An answer asked for again while a request for the same path was out is asked once more when it
  comes back (its answer could be from before the change), and a link whose note leaves the list
  asks at once: made, trashed, restored or deleted for good on another device, the look follows.

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

Replaced by #472 (branch issue/448-links-app-3): no paper wash, card buttons side by side.

Replaced by #472 (branch issue/448-links-app-3): no paper wash, card buttons side by side.
kayg closed this pull request 2026-10-16 04:20:52 +00:00
Some checks failed
CI / server (pull_request) Successful in 22s
CI / rust (pull_request) Successful in 1m46s
CI / web (pull_request) Failing after 48m32s

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