Files
pad/web/src
xarmian 3463c83bf7 feat(web): contextual browser-tab titles (IDEA-592) (#136)
* feat(web): add page title store and wire root layout (TASK-602)

Foundation for contextual browser-tab titles (IDEA-592 / PLAN-601).

Introduces a centralized rune store at web/src/lib/stores/title.svelte.ts
that composes titles as `{item|section} · {workspace} · Pad` with the most
specific label first (browsers truncate from the right). The root layout's
<svelte:head> renders `<title>{titleStore.title}</title>` reactively.

The store exposes `setPageTitle({ workspace?, section?, item? })` with
per-key merge semantics: omitted keys preserve, `null` clears, strings set.
This lets a layout set the workspace once while leaf pages contribute only
their own section or item ref without clobbering context.

With no route wired yet (TASK-603), all pages continue to render `Pad` —
identical behavior to before, now served via the store.

OG meta tags are unchanged on purpose; this only affects the browser tab.

* feat(web): wire contextual titles for big-four routes (TASK-603)

Completes contextual browser-tab titles for IDEA-592 / PLAN-601.

Each workspace-area route now calls `titleStore.setPageTitle(...)` from
a `$effect` to contribute its slice of context:

- `[username]/[workspace]/+layout.svelte` — sets `workspace` from
  `workspaceStore.current?.name`; clears on destroy so leaving the
  workspace area resets the tab to bare `Pad`.
- `[username]/[workspace]/+page.svelte` (workspace home) — clears
  section/item so only the layout-owned workspace name shows.
- `[username]/[workspace]/[collection]/+page.svelte` — section from
  the loaded collection's display name.
- `[username]/[workspace]/[collection]/[slug]/+page.svelte` — item
  from `formatItemRef(item)` (e.g. `IDEA-592`); section cleared so
  the format reads `{REF} · {Workspace} · Pad` (the ref prefix
  already encodes the collection).
- `[username]/[workspace]/activity/+page.svelte` — static section
  `Activity`; removes the old ad-hoc `<svelte:head><title>` block
  that conflicted with the store-driven root <title>.

Results:
- `/` → `Pad`
- `/{user}/{ws}` → `{Workspace} · Pad`
- `/{user}/{ws}/{collection}` → `{Collection} · {Workspace} · Pad`
- `/{user}/{ws}/{collection}/{ref}` → `{REF} · {Workspace} · Pad`
- `/{user}/{ws}/activity` → `Activity · {Workspace} · Pad`

Niche routes (settings, roles, console, billing) are unchanged and
continue to fall back to `Pad` — they can migrate to the store
incrementally.

* fix(web): clear stale title parts on route change in workspace layout

Addresses Codex P1 on PR #136: `setPageTitle` preserves omitted keys by
design, so navigating from a wired route (item detail, collection list,
activity) to an unwired route (settings, roles, dashboard, library,
playbooks, conventions) left the previous section/item in the tab title.

The workspace layout's title effect now reads `page.url.pathname` so it
re-runs on every SPA navigation and clears `section`/`item` alongside
the `workspace` set. Leaf pages that want to contribute their own parts
continue to do so in their own `$effect`s, which run after this one per
Svelte 5's parent-before-child effect ordering. Unwired routes inherit
the cleared state and correctly fall back to `{Workspace} · Pad`.

* fix(web): re-run activity title effect on pathname change

Addresses Codex P2 on PR #136. The activity page's title `$effect` set
`section: 'Activity'` with no reactive dependencies, so it only fired on
first mount. Because SvelteKit reuses the page component when navigating
between `/{user}/{ws1}/activity` and `/{user}/{ws2}/activity`, and the
workspace layout now clears `section` on every pathname change, the tab
title dropped to `{Workspace} · Pad` after cross-workspace navigation
until a full remount.

Reading `page.url.pathname` at the top of the effect gives it a dep that
changes on every SPA navigation, so the activity section is re-asserted
after the layout's clear.

The other wired leaf pages (workspace home, collection list, item detail)
are not affected: the home page sets only nulls (matches the layout's
clear), and the collection/item effects already depend on reactive state
(`collection?.name`, `formatItemRef(item)`) that gets refreshed on
navigation.

* fix(web): split workspace-name sync from section/item clear in layout

Addresses Codex P1 on PR #136 (third round). The previous combined
effect in the workspace layout depended on both `page.url.pathname` and
`workspaceStore.current?.name`, so every async resolution of the
workspace name would clear `section`/`item` in addition to updating
`workspace`. If a leaf page (e.g. activity) had already set its section
before the workspace resolved, the layout's rerun would wipe it.

Splitting the single effect into two:

1. Workspace-name sync — depends only on `workspaceStore.current`. Only
   touches the `workspace` slot. Safe to fire asynchronously after the
   leaf has set its context.
2. Route-change clear — depends only on `page.url.pathname`. Fires
   exactly once per SPA navigation, clearing `section`/`item`. Leaf
   `$effect`s run after (parent-before-child ordering) and re-assert
   their parts.

Unwired routes still correctly fall back to `{Workspace} · Pad`, and
the activity page retains `Activity · {Workspace} · Pad` after the
workspace-name resolution.
2026-04-17 16:21:08 -04:00
..
2026-03-26 01:52:36 +00:00