port: contacts app tab and list (approved mockup, 1/4) #841

Closed
kayg wants to merge 2 commits from port/contacts into dev
Owner

Slice 1 of 4 of the Contacts port (approved mockup apps/mockup/src/contacts/* on review/calendar-2). Stacked: 2 = the card, 3 = edit fields, 4 = duplicates + merge.

What it is

  • App tab: Contacts is a built app (lib/apps.ts). Phone: tab in the first-screen pill of Notes and Contacts. Desktop: an app button beside Settings in the Notes sidebar foot; Contacts has its own foot (chrome/AppFoot.svelte, shared, with the tabs). openApp() is the one place a tab goes to an address.
  • The list (#/contacts, ?list=<tag>, ?q=<words>): A to Z under letter headings, # last; your own card on top; Pinned under it; Lists (the cards' tags) as filters in the sidebar, never sections; search that filters as you type and lives in the address; phone: sidebar -> list stack and the A to Z strip.
  • No new backend call. A contact is the note Contacts/<Name>.md; the list is read from the notes the app already holds (store.notes, path directly in Contacts/).
  • Pure, tested rules: contacts/card.ts reads the card (frontmatter, H1, photo, the first bulleted list with groups and sub-bullets, ## Notes to the end) and writes edits back as a rewrite of only the changed line; serializeCard(parseCard(t)) === t for every note (CRLF, BOM, no final newline, odd bullets, unicode). The layout is that of crates/contacts note_io (checked line by line); there was no TS parser on dev or in feat/contacts-companion-pure (Rust only). names.ts (initials, sort, sections, search) is the mockup's logic.ts; people.ts is the list rules.
  • Own chunk ContactsScreen-* (lazy, like Tasks); nothing on the first screen imports it.

Decisions (nobody to ask; noted)

  • The decisions memory file (calternal-decisions-contacts.md) does not exist on the control host (the memory dir is empty), so C1-C8 / U1-U7 were not available. I built from the mockup source, the task's key summary and the Rust format code. Anything those rounds changed that the mockup does not show may need a follow-up.
  • "Your own card" is not defined in the note format. Rule used: the contact named as the signed-in account (first match only; none when the name is You). Easy to replace once the decision is known (people.ts, one function).
  • The notes list says nothing of a note's format, so the folder makes a contact (direct child of Contacts/). Notes dropped there that are not cards still list; the card slice checks format: calternal-contact when it opens one.
  • Import entry: not drawn (backend PRs open). No "soon" labels. Birthdays "Upcoming", "Tidy up" (duplicates) and the company building icon need card bodies for every row; they come with slices 2 and 4.
  • Desktop text is 14 px (product tokens); phone sizes are the mockup's.

Gates (CI=1 --retries=0, through ~/bin/slot)

  • bun run check (svelte-check): 0 errors.
  • bunx vitest run in apps/web: all pass (card 22, names 5, people 4 + the existing 78).
  • e2e/contacts-list.spec.ts (list A-Z + address, Lists filter, search, Notes<->Contacts tabs, phone index strip): chromium-1440, chromium-390 on dev-01 and webkit-1440, webkit-390 on dev-02: 8 passed, 2 skipped (the phone-only and desktop-only cases) on each pair.

Merge notes

  • feat/tasks-ui-s1-s2 touches the same lines (lazy-ui-plugin.ts allow-list and rewrite count, lazy-ui.svelte.ts, App.svelte layers, apps.ts appTabs(also), bundle-size.mjs). Whichever lands second takes a trivial conflict (count becomes 19). AppFoot.svelte is the generic version of Tasks' SideFoot.svelte.

Bundle numbers and screenshots: in the stack's last PR report.


Budget (per-app gate, PR #866)

perf/web.sh --bundle-only --gate on this branch with perf/budgets-per-app merged in (uncommitted, in a scratch worktree on dev-01):

  • First-paint JS (chunk.index): 239.917 KB vs 240 (was 240.048 on the first version; dev is 239.517). The tab, the route layer and the lazy-import entry are the +0.40 KB; nothing else of Contacts is in the entry chunk.
  • chunk.ContactsScreen: 5.31 KB (allowance 14.5).
  • What was trimmed to get there: the loop over the apps in the Notes sidebar (now one button), the unused registerApp and the appTabs(also) parameter, a separate effect for the lazy load, and my old Contacts bucket in bundle-size.mjs (per-chunk gating replaces it; removing it also avoids a conflict when #866 lands).
Slice 1 of 4 of the Contacts port (approved mockup `apps/mockup/src/contacts/*` on `review/calendar-2`). Stacked: 2 = the card, 3 = edit fields, 4 = duplicates + merge. ## What it is - **App tab**: Contacts is a built app (`lib/apps.ts`). Phone: tab in the first-screen pill of Notes and Contacts. Desktop: an app button beside Settings in the Notes sidebar foot; Contacts has its own foot (`chrome/AppFoot.svelte`, shared, with the tabs). `openApp()` is the one place a tab goes to an address. - **The list** (`#/contacts`, `?list=<tag>`, `?q=<words>`): A to Z under letter headings, `#` last; your own card on top; Pinned under it; Lists (the cards' tags) as filters in the sidebar, never sections; search that filters as you type and lives in the address; phone: sidebar -> list stack and the A to Z strip. - **No new backend call.** A contact is the note `Contacts/<Name>.md`; the list is read from the notes the app already holds (`store.notes`, path directly in `Contacts/`). - **Pure, tested rules**: `contacts/card.ts` reads the card (frontmatter, H1, photo, the first bulleted list with groups and sub-bullets, `## Notes` to the end) and writes edits back as a rewrite of only the changed line; `serializeCard(parseCard(t)) === t` for every note (CRLF, BOM, no final newline, odd bullets, unicode). The layout is that of `crates/contacts` `note_io` (checked line by line); there was no TS parser on dev or in `feat/contacts-companion-pure` (Rust only). `names.ts` (initials, sort, sections, search) is the mockup's `logic.ts`; `people.ts` is the list rules. - Own chunk `ContactsScreen-*` (lazy, like Tasks); nothing on the first screen imports it. ## Decisions (nobody to ask; noted) - **The decisions memory file (`calternal-decisions-contacts.md`) does not exist on the control host** (the memory dir is empty), so C1-C8 / U1-U7 were not available. I built from the mockup source, the task's key summary and the Rust format code. Anything those rounds changed that the mockup does not show may need a follow-up. - **"Your own card"** is not defined in the note format. Rule used: the contact named as the signed-in account (first match only; none when the name is `You`). Easy to replace once the decision is known (`people.ts`, one function). - The notes list says nothing of a note's format, so the folder makes a contact (direct child of `Contacts/`). Notes dropped there that are not cards still list; the card slice checks `format: calternal-contact` when it opens one. - Import entry: not drawn (backend PRs open). No "soon" labels. Birthdays "Upcoming", "Tidy up" (duplicates) and the company building icon need card bodies for every row; they come with slices 2 and 4. - Desktop text is 14 px (product tokens); phone sizes are the mockup's. ## Gates (CI=1 --retries=0, through ~/bin/slot) - `bun run check` (svelte-check): 0 errors. - `bunx vitest run` in apps/web: all pass (card 22, names 5, people 4 + the existing 78). - `e2e/contacts-list.spec.ts` (list A-Z + address, Lists filter, search, Notes<->Contacts tabs, phone index strip): chromium-1440, chromium-390 on dev-01 and webkit-1440, webkit-390 on dev-02: 8 passed, 2 skipped (the phone-only and desktop-only cases) on each pair. ## Merge notes - `feat/tasks-ui-s1-s2` touches the same lines (`lazy-ui-plugin.ts` allow-list and rewrite count, `lazy-ui.svelte.ts`, `App.svelte` layers, `apps.ts` `appTabs(also)`, `bundle-size.mjs`). Whichever lands second takes a trivial conflict (count becomes 19). `AppFoot.svelte` is the generic version of Tasks' `SideFoot.svelte`. Bundle numbers and screenshots: in the stack's last PR report. --- ## Budget (per-app gate, PR #866) `perf/web.sh --bundle-only --gate` on this branch with `perf/budgets-per-app` merged in (uncommitted, in a scratch worktree on dev-01): - First-paint JS (`chunk.index`): **239.917 KB** vs 240 (was 240.048 on the first version; dev is 239.517). The tab, the route layer and the lazy-import entry are the +0.40 KB; nothing else of Contacts is in the entry chunk. - `chunk.ContactsScreen`: 5.31 KB (allowance 14.5). - What was trimmed to get there: the loop over the apps in the Notes sidebar (now one button), the unused `registerApp` and the `appTabs(also)` parameter, a separate effect for the lazy load, and my old Contacts bucket in `bundle-size.mjs` (per-chunk gating replaces it; removing it also avoids a conflict when #866 lands).
A contact is the note Contacts/<Name>.md. The list reads those notes from the notes the app already holds (no new call): A to Z
sections, your own card on top (the contact named as you are), Pinned, Lists as filters (the cards' tags), search, the phone's index
strip, the sidebar and the app tabs (phone foot, desktop sidebar foot). The card parser/serializer (byte-exact round trip) and the
list rules are pure and unit tested.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Contacts: icons at the product's sizes
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
ef15767cd3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Owner

First-paint diet (BM15, #947). Head d36491eb (unchanged by me).

First-paint bytes are brotli q11 from apps/web/scripts/bundle-size.mjs (JS of the first-paint closure = the index chunk; CSS of the first stylesheet). Dev baseline 41f2efa3: 238,797 B JS, 19,250 B CSS. A second figure, "hash-normalised", is the same chunk with the 8-character chunk-file hashes replaced by a fixed string: the hashes are random, and they move the real brotli number by ±300 B between two builds of identical code, so a few-hundred-byte claim is only meaningful in the normalised figure (dev: 238,804). Chunk budgets: every new lazy chunk's line is taken from the first-script line or the idle-screens line so the total stays 404.2; perf/web.sh --bundle-only --gate passes on this head.

first JS first CSS lazy JS
dev 238,797 19,250 122,936
this PR 239,152 (+355; normalised 239,038 = +234) 19,250 (0) 128,409

The Contacts screen is its own lazy chunk (ContactsScreen, 5.3 KB). First-screen cost: the route wiring in App.svelte, the app tab button in Sidebar.svelte, openApp. Same registry note as #832: merged with Tasks, the wiring is one appScreens object. Not ≤ 0 on its own; covered by #875.

contacts-list.spec: 16 passed, 4 skipped on all four projects. No budget change here (its ContactsScreen line already exists).

**First-paint diet (BM15, #947).** Head `d36491eb` (unchanged by me). First-paint bytes are brotli q11 from `apps/web/scripts/bundle-size.mjs` (JS of the first-paint closure = the `index` chunk; CSS of the first stylesheet). Dev baseline `41f2efa3`: **238,797 B JS, 19,250 B CSS**. A second figure, "hash-normalised", is the same chunk with the 8-character chunk-file hashes replaced by a fixed string: the hashes are random, and they move the real brotli number by ±300 B between two builds of identical code, so a few-hundred-byte claim is only meaningful in the normalised figure (dev: 238,804). Chunk budgets: every new lazy chunk's line is taken from the first-script line or the idle-screens line so the total stays 404.2; `perf/web.sh --bundle-only --gate` passes on this head. | | first JS | first CSS | lazy JS | |---|---|---|---| | dev | 238,797 | 19,250 | 122,936 | | this PR | 239,152 (**+355**; normalised 239,038 = **+234**) | 19,250 (0) | 128,409 | The Contacts screen is its own lazy chunk (`ContactsScreen`, 5.3 KB). First-screen cost: the route wiring in `App.svelte`, the app tab button in `Sidebar.svelte`, `openApp`. Same registry note as #832: merged with Tasks, the wiring is one `appScreens` object. Not ≤ 0 on its own; covered by #875. `contacts-list.spec`: **16 passed, 4 skipped** on all four projects. No budget change here (its `ContactsScreen` line already exists).
Author
Owner

#841 per-PR gates at 63faf541: BLOCKED on two non-spec gates

Head = 40adc307 + merge of origin/dev b5e8dcb2 (no conflicts).

gate result
lint / svelte-check / vitest / ui primitives pass / 2 errors, 16 warnings = dev's / pass
Chromium + WebKit 1440+390: contacts-list, phone-foot Chromium 12 passed, 6 skipped, 0 failed; WebKit 12 passed, 6 skipped, 0 failed
perf/web.sh --bundle-only --gate rc 0 against its budgets, but first-paint JS brotli 232.941 KB vs dev 232.862 (+79 B); first CSS 18.571 (=) vs dev 18.571
check-ui-primitives (from #941, run on this tree) NEW finding vs dev: contacts/ContactsScreen.svelte:90 (hand-made glass pill: use <Pill alone>); the fix needs #941's Pill, so it follows #941 into dev
Rust perf n/a

To go GREEN: (1) cut ≥ 79 B from the first script or get an explicit OK; (2) move the pill to <Pill alone> once #941 is in dev.

## #841 per-PR gates at `63faf541`: BLOCKED on two non-spec gates Head = `40adc307` + merge of origin/dev `b5e8dcb2` (no conflicts). | gate | result | |---|---| | lint / svelte-check / vitest / ui primitives | pass / 2 errors, 16 warnings = dev's / pass | | Chromium + WebKit 1440+390: contacts-list, phone-foot | Chromium 12 passed, 6 skipped, 0 failed; WebKit 12 passed, 6 skipped, 0 failed | | `perf/web.sh --bundle-only --gate` | rc 0 against its budgets, **but first-paint JS brotli 232.941 KB vs dev 232.862 (+79 B)**; first CSS 18.571 (=) vs dev 18.571 | | `check-ui-primitives` (from #941, run on this tree) | **NEW finding vs dev: `contacts/ContactsScreen.svelte:90`** (hand-made glass pill: use `<Pill alone>`); the fix needs #941's Pill, so it follows #941 into dev | | Rust perf | n/a | To go GREEN: (1) cut ≥ 79 B from the first script or get an explicit OK; (2) move the pill to `<Pill alone>` once #941 is in dev.
Author
Owner

#841 after #941: Pill/press done, specs green, first-paint bytes NOT yet ≤ dev

Head 52a7444d8, dev 850c23a9 merged in. The phone's back is <Pill alone h="var(--tier-top)">; local .pill / .round CSS gone.

gate result
lint / svelte-check / vitest pass / 2 errors, 16 warnings = dev's / pass
check-ui-primitives clean
Chromium (dev-01) + WebKit (dev-02), 1440+390: contacts-list, phone-foot, pill-press all pass (18 passed each, 0 failed)
first-paint JS brotli (dev 232.565 KB, first CSS 17.952 = dev) 232.720: +155 B; the bundle gate is also RED: idle-screens 51.320 > its 51.3 line

Not GREEN until the first-paint bytes are cut by the amount above.

## #841 after #941: Pill/press done, specs green, first-paint bytes NOT yet ≤ dev Head `52a7444d8`, dev `850c23a9` merged in. The phone's back is `<Pill alone h="var(--tier-top)">`; local `.pill` / `.round` CSS gone. | gate | result | |---|---| | lint / svelte-check / vitest | pass / 2 errors, 16 warnings = dev's / pass | | `check-ui-primitives` | **clean** | | Chromium (dev-01) + WebKit (dev-02), 1440+390: contacts-list, phone-foot, pill-press | all pass (18 passed each, 0 failed) | | first-paint JS brotli (dev 232.565 KB, first CSS 17.952 = dev) | **232.720: **+155 B**; the bundle gate is also RED: `idle-screens` 51.320 > its 51.3 line** | Not GREEN until the first-paint bytes are cut by the amount above.
Author
Owner

#841 icons move: done, specs and screenshots clean, first paint still +124 B over dev (not GREEN)

Head 788aa772 (dev 02cdf878 merged). The camera, scan, indent, outdent, redo and wikilink icons moved from Icon.svelte to icons-extra (history stays: the palette's filter chips draw it before the idle screens load; pencil and close are drawn by first-paint code too). The typing bar's Strip.svelte now calls registerExtraIcons() (the attach menu, Insert, Info, Settings and Style already did).

Screenshots (Chromium 1440 and 390, before = 52a7444d, after = this change; the screens that can draw them: the phone's typing bar strip at both ends, the Attach menu, the desktop Insert sheet, the desktop typing bar, Settings → Editor): every icon is drawn after; the 390 strip's tool list shows indent, outdent, redo and the note-link tool with the same SVG as before, Attach shows Camera and Scan, nothing falls back to the notes icon. (Settings → Editor has no toolbar editor in this build, so nothing there draws them.)

first-paint JS brotli, real build 232.532 KB at d42f498c (vs dev then 232.565)
after merging #971 232.741 vs dev 02cdf878 232.617: +124 B; hash-normalised (chunk hashes stripped) index +145 B (the icons removed 474 raw bytes, the registration code adds 1.1 KB raw of App/Sidebar/apps/lazy-ui)
idle-screens 51.500 on its line 51.5 (moved 0.2 KB from index, total 404.2); note dev itself measures 51.334 on 51.3
check-ui-primitives, lint, svelte-check (0 errors), vitest clean / pass
Chromium (dev-01) + WebKit (dev-02), 1440 + 390: contacts-list, phone-foot, pill-press, note-bars, bar-rest, bar-focus, bar-taps, note-bars-fit, sheet-geometry, preload, lazy-ui, lazy-styled, a11y-high Chromium 136 passed, 0 failed; WebKit 132 passed, 3 failed (a11y-high Style x1 and palette x1 at 390, note-bars-fit 390). --repeat-each=10 on the branch: 1 failure in 239 (note-bars-fit 1440 dark); on dev: 2 in 238 (the known a11y-high Style at 390). --repeat-each=30 of note-bars-fit 1440 dark on the branch: 60/60. Not reproduced, not blocking (flaky lane)

Remaining cut needed: ≈ 150 B. The only other first-paint icons are history, pencil, close, all used before idle. Waiting on kayg's morning answer for how app registration is handled; no further cuts tonight.

## #841 icons move: done, specs and screenshots clean, first paint still +124 B over dev (not GREEN) Head `788aa772` (dev `02cdf878` merged). The `camera`, `scan`, `indent`, `outdent`, `redo` and `wikilink` icons moved from `Icon.svelte` to `icons-extra` (`history` stays: the palette's filter chips draw it before the idle screens load; `pencil` and `close` are drawn by first-paint code too). The typing bar's `Strip.svelte` now calls `registerExtraIcons()` (the attach menu, Insert, Info, Settings and Style already did). **Screenshots** (Chromium 1440 and 390, before = `52a7444d`, after = this change; the screens that can draw them: the phone's typing bar strip at both ends, the Attach menu, the desktop Insert sheet, the desktop typing bar, Settings → Editor): every icon is drawn after; the 390 strip's tool list shows `indent`, `outdent`, `redo` and the note-link tool with the same SVG as before, Attach shows Camera and Scan, nothing falls back to the notes icon. (Settings → Editor has no toolbar editor in this build, so nothing there draws them.) | | | |---|---| | first-paint JS brotli, real build | 232.532 KB at `d42f498c` (vs dev then 232.565) | | after merging #971 | 232.741 vs dev `02cdf878` 232.617: **+124 B**; hash-normalised (chunk hashes stripped) index +145 B (the icons removed 474 raw bytes, the registration code adds 1.1 KB raw of App/Sidebar/apps/lazy-ui) | | `idle-screens` | 51.500 on its line 51.5 (moved 0.2 KB from `index`, total 404.2); note dev itself measures 51.334 on 51.3 | | `check-ui-primitives`, lint, svelte-check (0 errors), vitest | clean / pass | | Chromium (dev-01) + WebKit (dev-02), 1440 + 390: contacts-list, phone-foot, pill-press, note-bars, bar-rest, bar-focus, bar-taps, note-bars-fit, sheet-geometry, preload, lazy-ui, lazy-styled, a11y-high | Chromium 136 passed, 0 failed; WebKit 132 passed, 3 failed (a11y-high Style x1 and palette x1 at 390, note-bars-fit 390). `--repeat-each=10` on the branch: 1 failure in 239 (note-bars-fit 1440 dark); on dev: 2 in 238 (the known a11y-high Style at 390). `--repeat-each=30` of note-bars-fit 1440 dark on the branch: 60/60. Not reproduced, not blocking (flaky lane) | Remaining cut needed: ≈ 150 B. The only other first-paint icons are `history`, `pencil`, `close`, all used before idle. Waiting on kayg's morning answer for how app registration is handled; no further cuts tonight.
Author
Owner

#841 re-gated on dev c808ef67: GREEN at 84fd36c1

Merged dev (the Pill/press fixes and the icons move are kept). perf/budgets.json: 3-way merge of both sides' line moves (index 229.0, idle-screens 51.8, picker-list 9.7 from #975; total 404.2).

gate result
lint / svelte-check / vitest web / ui primitives / check-ui-primitives pass / 0 errors, 16 warnings (= dev) / 109 / 59 / clean
first-paint JS brotli (this PR vs dev c808ef67 225.407 KB vs prod 71827028 233.040 KB) 225.500 KB: +93 B vs dev, −7.54 KB vs prod (first CSS 17.952 = dev, prod 18.376). Option A: dev after this merge stays ≤ prod.
perf/web.sh --bundle-only --gate rc 0. idle-screens 51.478
Chromium (dev-01) 1440+390: contacts-list, phone-foot, pill-press, note-bars, bar-rest, bar-focus, bar-taps, note-bars-fit, sheet-geometry, preload, lazy-ui, lazy-styled, a11y-high, toast-bar 154 passed, 0 failed
WebKit (dev-02) same 153 passed, 0 failed
Rust n/a
## #841 re-gated on dev `c808ef67`: GREEN at `84fd36c1` Merged dev (the Pill/press fixes and the icons move are kept). `perf/budgets.json`: 3-way merge of both sides' line moves (index 229.0, idle-screens 51.8, picker-list 9.7 from #975; total 404.2). | gate | result | |---|---| | lint / svelte-check / vitest web / ui primitives / `check-ui-primitives` | pass / 0 errors, 16 warnings (= dev) / 109 / 59 / clean | | **first-paint JS brotli (this PR vs dev `c808ef67` 225.407 KB vs prod `71827028` 233.040 KB)** | **225.500 KB: +93 B vs dev, −7.54 KB vs prod** (first CSS 17.952 = dev, prod 18.376). Option A: dev after this merge stays ≤ prod. | | `perf/web.sh --bundle-only --gate` | rc 0. idle-screens 51.478 | | Chromium (dev-01) 1440+390: contacts-list, phone-foot, pill-press, note-bars, bar-rest, bar-focus, bar-taps, note-bars-fit, sheet-geometry, preload, lazy-ui, lazy-styled, a11y-high, toast-bar | 154 passed, 0 failed | | WebKit (dev-02) same | 153 passed, 0 failed | | Rust | n/a |
Author
Owner

Merged into dev by the orchestrator (U1 GREEN, credit bank).

Merged into dev by the orchestrator (U1 GREEN, credit bank).
kayg closed this pull request 2026-11-06 11:41:25 +00:00
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled

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