Skip to content

feat(favorites): favorites, recently opened pages and a light theme (B.3) - #117

Merged
joszz merged 2 commits into
mainfrom
feat/b3-favorites-theme
Nov 9, 2026
Merged

joszz merged 2 commits into
mainfrom
feat/b3-favorites-theme

Conversation

@joszz

@joszz joszz commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

Run B.3 (plan: docs/runs/B.3.md, with its handoff).

What a person gets

  • Favorites. A star in the page header (a toggle button, aria-pressed) and "Add to / Remove from favorites" in a row's menu. Starred pages are listed above the tree in the sidebar (and the phone's drawer), in the order they were starred, with titles from the tree so a rename shows at once.
  • Recently opened. The start page lists the 10 pages opened last, newest first, each with its parent's title and when it was opened ("5 minutes ago", in English or Dutch; the times move on while the page stays open).
  • Shared by the phone and the laptop. Both lists are kept on the server per user and read again when the tab comes back into view. A page in the trash is in neither list and is back in both when restored; a page deleted for good takes its rows with it.
  • A light theme. The app is light when the system is light and dark when it is dark, from the first paint with no flash. "Theme: System, Light, Dark" in the user menu chooses per device, and the phone's browser bar follows.

Design decisions worth a look

  • The tokens hold both themes: each colour in @theme is light-dark(<light>, <dark>), with color-scheme: light dark on the root. The strict CSP leaves no room for a script in index.html, so the system setting has to work from CSS alone. A choice in the menu sets data-theme on <html> before the app mounts. No component changed for the theme, and no dark: variant is used.
  • Private, live, and quiet: every query is filtered by the signed-in user, even for the workspace's owner. The lists read live_pages, as the tree and search do. Nothing touches a page's version, updated_at or the tree version, and no audit line is written.
  • One person's changes run one at a time: a star and a visit lock that user's own row (for update) for their transaction. Two stars from two devices cannot pass the limit of 200, and two visits never prune the same rows at once. Different people never wait for each other.
  • Raw SQL, a little beyond the tree rule: the two lists are one query each joined on live_pages, and the lock is the same for update pattern TagService and the compactor use. Starring checks only the page and the pages above it (TreeService.IsLiveAsync), not the workspace's whole tree.
  • Phase 2: the lists filter by the workspace role only. When page shares arrive (2.3) they must go through the permission service; this is written into the architecture's permission section.

Tests

  • Server: FavoriteEndpointTests (17): idempotent star and unstar, a viewer may star, trash and restore, the 200 limit (also under parallel stars), privacy, stranger and anonymous, cascade on delete, no version or audit change, visit order, the 51st visit, parallel visits of one page and of many. The deny-by-default policy test covers the five endpoints; the migration's column test has the two tables.
  • Vitest: theme.ts (blocked storage, data-theme, the browser bar), the contrast of both palettes and a check that every colour token has two values, the browser bar colours equal surface-panel, the stores (rollback of only the refused change, reads that wait for visits), the star, the sidebar list, the row menu item, the start page, the relative times, the visibility helper.
  • Playwright: favorites.spec.ts and theme.spec.ts on desktop and both phones; a11y.spec.ts runs axe on every state in both themes on all four projects, with the sidebar with favorites and the start page with recents added.
  • Full pnpm check (Postgres up): web 1462, sidecar 152, server 1986 with the timing tests, Playwright 413, all passed.

Found on the way

  • The CSRF check answers 415 to a PUT or DELETE without a body: it reads a request without a content type as a form. The web client always sends {}; the server tests do the same.
  • The light values of the plan needed no changes: the contrast test and axe passed on them in both themes on every scanned screen.
  • syncClient.ts keeps its own visibility listener: it sends a heartbeat, not a reload, and the shared helper is for reloads.

Review fixes (/code-review, Opus 5.5, 10 findings, all fixed)

Rollback of only the refused toggle; the browser bar following a chosen theme, its colours tied to the token by a test; the favorites limit and the visit pruning safe under parallel requests (user row lock); the page just opened on the start page at once; relative times that move on; a Set for the sidebar's lookups; fewer and cheaper queries; one helper for "read again when the tab is back"; a shared isTheme, a corrected test name, a repeated assertion removed. Details in the handoff of docs/runs/B.3.md.

For the test pass

Added to docs/test-pass.md under "B.3": light and dark following the system on the phone (also the browser bar) and a chosen theme surviving a reload; reading in both themes by day and night; a star and an open crossing from one device to the other; the star and the recent list with a screen reader.

🤖 Generated with Claude Code

joszz and others added 2 commits November 9, 2026 14:36
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jos Nienhuis <6952249+joszz@users.noreply.github.com>
…B.3)

Favorites and recently opened pages, kept per user on the server so the
phone and the laptop share them: a star in the page header and the row
menu, a favorites list above the sidebar tree, and "Recently opened" on
the start page. Two tables (favorites, page_visits) and five endpoints,
live pages only, private to the user.

A light theme from the same tokens: every colour is light-dark(light,
dark), so the system setting applies with no script and no flash, and
System, Light and Dark in the user menu choose per device. The contrast
test and the axe scans check both palettes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jos Nienhuis <6952249+joszz@users.noreply.github.com>
@joszz
joszz merged commit 6a4afcf into main Nov 9, 2026
13 checks passed
@joszz
joszz deleted the feat/b3-favorites-theme branch November 9, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant