← Cycles
Design · c106

Sticky Title

archivedcreated 2026-10-02· last activity 2026-10-02· mode: evolution (c104's band grows rows and modes) with formula (the iOS large-title pattern)

The page title (layer `UxPageTitle` / Fable `PageHeader`, not the actor headers) joins c104's sticky band, which becomes two rows with scroll-direction modes.

New in this cycle: The page title joins the sticky band: the real UxPageTitle pins on row 1 beside the Filter and sub navs pin in row 2; scrolling down compacts title (15px), Filter (values only) and pills, scrolling up grows them to an in-between set (title 20px); a two-layer shadow lifts the band; nothing in the band moves the page (every mode flip Δ0)

Why

c104 pins the global filter and jump pills as one band. The page title still scrolls away, so a long page loses its name. Mark wants the title in the band: compact while scrolling down ("tidy, out of the way"), with room under it for sticky sub navs, and an in-between set while scrolling up.

Objective

Row 1 = the compact title (crumbs + title) and the filter; row 2 = any sub nav that registers. Modes rest · down · up by scroll direction. Nothing in the band may move page content.

Scope

  • Layer: useStickyBand rows + mode; UxStickyBand row 1; UxPageTitle and NavJumpPills join a mounted band automatically (opt-out); a compact Filter look.
  • Fable: no page changes beyond the layer; SessionReading's switch joins row 2.
  • Not the actor/hero headers.

Key Changes

Phase 0 mock (2026-10-02): /pages/sticky-title, design-local (useStickyTitleBand, components/sticky-title/). Verified headless: detail band 91 px down · 129 px up, listing 53 · 81, headers pin at the band's height in every mode, jumps land under the compact band, every mode flip Δpage 0 · Δcontent 0. Mark's mid-build question (no toggle — detect sub navs) → registration, and components auto-join a mounted band in Phase 1. Details in plan.md § Phase 0.

Phase 0 rework (2026-10-02): the real UxPageTitle pins instead of a teleported copy (Mark: use the actual title, as the Filter is used): a zero-height sticky box with a bottom margin reserving its rest height, restyled by mode. Re-verified, every flip Δ0 (after fixing a mid-animation rest measurement that moved the page 8 px). Fable's PageHeader wrapper needs flattening in the port. plan.md § Phase 0 rework.

Mock refinements + cleanup (2026-10-02):

  • Rest title: at Fable PageHeader size (28px, modelled on :3032/inventory/harnesses), on both page kinds.
  • No load jump: the title box is an ordinary block at rest and freezes its own height only while pinned; the first paint (no JS) matches the loaded page to the pixel.
  • Band shadow (Mark): two gradients off the backing's bottom edge, 5px at 0.18 and 20px at 0.08, black 100% → 0% alpha.
  • Code cleanup:
    • shared geometry constants in the composable;
    • the Band owns publishing (attach);
    • the Filter plumbing moved to StickyTitleFilterBar (a replica of Fable's GlobalFilterBar);
    • names say what they do (titlePinned, reportTitle, showBacking);
    • each file documents its role and its Phase 1 counterpart.
  • Re-verified headless after a dev-server restart (its watcher had stopped tracking the directory): every flip Δ0, headers under the band, jumps 16px under the compact band, no console errors.

Design refinements + documentation (2026-10-02):

  • Hairline kept while pinned (Mark: "keep the hairline on the title … during scroll"); its padding tightens to 6px to fit the row.
  • Motion (Mark: "slow down the transitions, especially when scrolling up … quint-in-out"; then "slow to 1s, 2s"): one easing, cubic-bezier(0.83, 0, 0.17, 1). Compacting takes 1s and growing takes 2s, published as --st-band-dur / --st-band-ease and read by the backing, title, pills and table headers. Sampled headless: the band goes 129 → 91px over ~1s scrolling down, and 93 → 129px over ~2s scrolling up.
  • Captures: scripts/shot-programs/c106-sticky-title.json (thread Phase 0 mock, subject page/sticky-title), 8 states: detail rest / down / up / jump / filter-compact off; listing rest / down / up. Files are public/cycles/design/c106-*.png.
  • Archive: 2026-10-02-c106-sticky-title.

Phase 1 + 2 — the layer and its docs (2026-10-02):

  • Layer 6283ed9 (q-nuxt-layer, local, unreleased): useStickyBand is rebuilt around roles (claimTitle / registerFilter / registerNav) and two layouts.
    • c104 (default): the filter and the pills side by side. Current consumers are unchanged, apart from the new shadow and the 400ms fade.
    • c106 (<UxStickyBand title>): the two-column band, with the scroll-direction mode and STICKY_BAND.upRestyles = false.
    • UxPageTitle: pins, freezes, restyles and settles; new sticky (opt-out) and rowMinHeight (replaces wrapper boxes).
    • NavJumpPills: joins any mounted band; docks under the title and compacts.
    • Filter: bandCompact.
    • UxTableListHeader: eases with the band.
    • STICKY_BAND: holds geometry and motion.
  • The band chooses the layout, not the pages. The Fable app reads the layer live; with title-joining tied to the band's title prop, its pages keep the c104 band until the Phase 4 port adds title to its layout and flattens PageHeader.
  • Mock on the layer (design 87e0c1ad): /pages/sticky-title now composes the layer pieces exactly as the Fable shell will. The design-local replica is deleted. Same numbers verified: detail 81 → 90px (pills docked 48–80), listing 81 → 53px, up draws as down, jumps 16px under the band, every flip Δ0, no errors.
  • Docs (design 13a56842): UxStickyBand is now the sticky-band guide — a live framed demo, setup, layouts, members, scrolling, consumer rules, variables and STICKY_BAND. UxPageTitle, NavJumpPills, Filter and UxTableListHeader document their band behaviour.
  • Captures: scripts/shot-programs/c106-sticky-title-p1.json (thread Phase 1 — on the layer), 8 mock states (c106-p1-*) plus the band guide (component/ux-sticky-band). The Phase 0 captures stay as the earlier thread.
  • Dev-server lesson: a fresh page load can be served stale while open tabs get hot updates. Removing a layer composable export needs a restart (Nuxt's auto-import list goes stale). Recorded in memory.
  • Not yet: Phase 3 (release 0.9.26) and Phase 4 (Fable port; :3032 was down at the time, so Fable is unverified against the new layer — its dev server will need a restart).

Phase 3 + 4 — release and the qdash port (2026-10-02):

  • q-nuxt-layer 0.9.26 (d52aa1c, tag pushed, publish green; migration notes in the layer CLAUDE.md). Lockfiles at 0.9.26: design 5d34901d, bob-wire cc9daed, app.qpoint.io a7a231a, www 28fbce3, qflow e53ab0c, qcontrol ui 2dacfeaa.
  • Port target moved: c107 made the Fable app qcontrol/crates/qdash/ui (feat/new-pages, PR #713, :3030). Port d1f2e00d, local:
    • <UxStickyBand title> in the layout;
    • PageHeader a fragment (the title a direct child of the page; rowMinHeight 44; an empty mb-10 keeps the spacing);
    • 13 detail pages drop :titleSize="15" (full size at rest).
  • c108 was live in the same tree. My one-line layout change went in beside its uncommitted banner, and c108 committed first (f76fa091), so the port sits on top of it.
  • Verified on a temp server (an APFS clone at ../.c106tmp on :3033, the real layer, the :7882 seed — :3030 served stale modules and isn't this session's):
    • listing pinned band 53px;
    • Team detail 90px, pills docked 48–80, table header at 90;
    • a jump lands 16px under the band; the page never moves; no errors;
    • npm test 566 + bible-check clean.
  • Caught on the way: a scripted PageHeader rewrite split the file at the inner #right </template> and left invalid Vue. Tests and bible-check don't compile it; the temp server did, and an SFC compile check now runs over PageHeader, the layout and the 42 pages that use it.
  • Open:
    • Detail pages now show the entity name twice at full size — the page title and the actor header's hero. Mark to rule.
    • Push the qcontrol and design commits; deploy the design site; restart :3030 to see the port in the live app.

Polish on the live app — 0.9.27 (2026-10-02): Mark's pass on the ported band, tuned on the :3033 temp server.

  • The backing fades in at its height instead of sliding down from the top; in a titled band it waits for the title, and its height animates only once fully in (layer 6f5b95c).
  • The title shrinks on its own clock: Mark tried slower (2s), then faster; 500ms (STICKY_BAND.motion.title, --q-band-title). Settling back to rest stays 1s.
  • Look: shadow 7px at 0.11 + 28px at 0.08 (taller, then lighter); backing surface-sunken-subtle, a tick off the page (lighter in light mode, darker in dark); 16px under docked pills (navBottomPad, was 12; band 94px with pills, 54px without). Layer 04e4406.
  • q-nuxt-layer 0.9.27 (be148b3, tag pushed, publish green). Lockfiles refreshed in all six consumers.

Outcome

Completed 2026-10-02. The page title joins the sticky band. <UxStickyBand title> turns on the two-column band: the real UxPageTitle pins beside the filter at 15px with its hairline, the jump pills dock under it as an outlined compact bar, and the filter drops its labels. Nothing in the band moves the page. It shipped in q-nuxt-layer 0.9.26, with Mark's polish on the live app in 0.9.27. qdash's PageHeader is a fragment, so the title is a direct child of the page and pins on every page (d1f2e00d, pushed to feat/new-pages). The UxStickyBand docs page is the guide, with the mock running live in a frame.

  • Motion: quint-in-out, 1s to compact; the title shrinks on its own 500ms clock; the backing fades in over 400ms at its height and only then animates; the pills dock in 200ms. Scrolling up draws the same as scrolling down (STICKY_BAND.upRestyles, kept off).
  • Look: the backing a tick off the page (surface-sunken-subtle); a two-layer shadow (7px at 0.11, 28px at 0.08); 16px under the docked pills; listing band 54px, with pills 94px.
  • Ruled: detail pages show the entity name twice at full size (page title and actor header). Mark: fine.
  • Captures: Phase 0 (design-local mock), Phase 1 (on the layer, plus the guide) and Phase 4 (in qdash at 0.9.27, c106-qdash-*).

Learnings:

  • A sticky element must not change its own flow height. Whatever pins freezes its rest height and restyles inside it; the filter is a zero-height box; the backing is out of flow. Every mode flip was measured with Δpage 0.
  • Use the real component. A teleported copy of the title fought the original. The filter's technique, a sticky box that never moves the flow, worked for the title too.
  • Docking beats sizing ahead. The backing grows when the pills arrive, not before, so it never covers them on the way in.
  • Fresh loads can be served stale while open tabs hot-update. Restart before verifying; a removed auto-import export needs a restart. For a tree another session is using, verify on an APFS clone that shares the real layer (cp -Rc; a symlinked layer path fails Vite's fs.allow).
  • Split a scripted Vue rewrite on the root's closing tag, not the first </template>. Tests and the bible check don't compile SFCs, so add a compile check.

Path change (c107, 2026-10-02). The Fable app is now ../qcontrol/crates/qdash/ui on branch feat/new-pages. Its dev server is :3030 (launch config qdash-app) and the seed backend is :7882 (qdash-seed). The qcontrol-c99-fable worktree, its c99/claude-code-fable-5.1 working branch and :3032 are gone. Read every apps/claude-code-fable-5.1/… path in this cycle as crates/qdash/ui/…, the same files moved. The c99 rule that kept src/ read-only applied to the parallel apps and ended with the cutover. Commit to feat/new-pages, never main. See c107.

Visual record

23 captures — heroes, page slices, and full-page renders. Click to view.

Phase 4 — in qdash (0.9.27)

2026-10-02· 5 captureslatest
qdash /inventory/harnesses at rest: Inventory / Harnesses at 28px on the sidebar's first hairline, the global filter on the title row; no backing
qdash /inventory/harnesses at rest: Inventory / Harnesses at 28px on the sidebar's first hairline, the global filter on the title row; no backing history · 6 moments →
Scrolled down: Harnesses pinned at 15px beside the compact filter (it shrank in 500ms), a 54px band on surface-sunken-subtle with the 7px + 28px shadow; the page scrolls under it
Scrolled down: Harnesses pinned at 15px beside the compact filter (it shrank in 500ms), a 54px band on surface-sunken-subtle with the 7px + 28px shadow; the page scrolls under it history · 6 moments →
qdash harness detail (Claude Code) at rest: the page title at its full 28px (detail pages dropped their 15px), the actor header and the jump pills below
qdash harness detail (Claude Code) at rest: the page title at its full 28px (detail pages dropped their 15px), the actor header and the jump pills below history · 4 moments →
Scrolled down: the title pinned at 15px, the pills docked under it as the outlined compact bar with 16px below them — a 94px band (the Installs table sits in a horizontal scroll box, so its header scrolls rather than pins)
Scrolled down: the title pinned at 15px, the pills docked under it as the outlined compact bar with 16px below them — a 94px band (the Installs table sits in a horizontal scroll box, so its header scrolls rather than pins) history · 4 moments →
Pill jump to Surface: the band holds compact through the smooth scroll and the heading lands 16px under it
Pill jump to Surface: the band holds compact through the smooth scroll and the heading lands 16px under it history · 4 moments →

Phase 1 — on the layer

2026-10-02· 10 captures
Detail page at rest: Organization / Teams / Payments at its full 28px (PageHeader's 19px top and 44px row via rowMinHeight), the Filter on the title row, the actor header and the jump pills below; no band backing
Detail page at rest: Organization / Teams / Payments at its full 28px (PageHeader's 19px top and 44px row via rowMinHeight), the Filter on the title row, the actor header and the jump pills below; no band backing open page →history · 2 moments →
Scrolling down: the title pins beside the Filter at 15px with its hairline, the Filter shows values only, the pills dock under the title — grey-350 outline, black semibold, the active pill purple bold with a 2px ring — and the shadowed band is 90px; the Installs header pins right under it
Scrolling down: the title pins beside the Filter at 15px with its hairline, the Filter shows values only, the pills dock under the title — grey-350 outline, black semibold, the active pill purple bold with a 2px ring — and the shadowed band is 90px; the Installs header pins right under it open page →history · 2 moments →
Scrolling back up: nothing changes — the band tracks the direction (the panel reads up, looks down) but draws it as down while STICKY_BAND.upRestyles is off
Scrolling back up: nothing changes — the band tracks the direction (the panel reads up, looks down) but draws it as down while STICKY_BAND.upRestyles is off open page →history · 2 moments →
Pill jump to Sessions: the band holds compact through the smooth scroll and the heading lands 16px under it; the Sessions pill takes the ring in 200ms
Pill jump to Sessions: the band holds compact through the smooth scroll and the heading lands 16px under it; the Sessions pill takes the ring in 200ms open page →history · 2 moments →
Listing page at rest, modelled on :3032/inventory/harnesses: Inventory / Harnesses at 28px; no sub nav, so the band is one row
Listing page at rest, modelled on :3032/inventory/harnesses: Inventory / Harnesses at 28px; no sub nav, so the band is one row open page →history · 2 moments →
Listing, scrolling down: Harnesses pinned at 15px beside the compact Filter, a 53px band, the Recent Work header pinned under it
Listing, scrolling down: Harnesses pinned at 15px beside the compact Filter, a 53px band, the Recent Work header pinned under it open page →history · 2 moments →
Listing, scrolling up: unchanged — 53px, the same as scrolling down
Listing, scrolling up: unchanged — 53px, the same as scrolling down open page →history · 2 moments →
The UxStickyBand docs page: the guide opens with the mock running live in a frame (scroll inside it), then setup, layouts, members, scrolling, rules and reference
The UxStickyBand docs page: the guide opens with the mock running live in a frame (scroll inside it), then setup, layouts, members, scrolling, rules and reference open page →
Filter bandCompact off: the Filter keeps its labels while pinned, so the right column (8 + 65 + 8) sets the band instead of the title and pills
Filter bandCompact off: the Filter keeps its labels while pinned, so the right column (8 + 65 + 8) sets the band instead of the title and pills open page →history · 2 moments →
c106 full
open page →

Phase 0 mock

2026-10-02· 8 captures
Detail page at rest: Organization / Teams / Payments at its full 28px (Fable PageHeader), the Filter on the title row, the actor header and the jump pills below; no band backing
Detail page at rest: Organization / Teams / Payments at its full 28px (Fable PageHeader), the Filter on the title row, the actor header and the jump pills below; no band backing open page →history · 2 moments →
Scrolling down: the title pins beside the Filter at 15px, the Filter drops its labels, the pills tighten in row 2, and the shadowed band is 91px; the Installs header pins right under it
Scrolling down: the title pins beside the Filter at 15px, the Filter drops its labels, the pills tighten in row 2, and the shadowed band is 91px; the Installs header pins right under it open page →history · 2 moments →
Scrolling back up 80px: the in-between set — title 20px, Filter labels back, pills full size; the band grows to 129px and the header follows
Scrolling back up 80px: the in-between set — title 20px, Filter labels back, pills full size; the band grows to 129px and the header follows open page →history · 2 moments →
Pill jump to Sessions: the band lands compact (91px) and the heading sits 16px under it
Pill jump to Sessions: the band lands compact (91px) and the heading sits 16px under it open page →history · 2 moments →
Listing page at rest, modelled on :3032/inventory/harnesses: Inventory / Harnesses at 28px; no sub nav, so the band will be one row
Listing page at rest, modelled on :3032/inventory/harnesses: Inventory / Harnesses at 28px; no sub nav, so the band will be one row open page →history · 2 moments →
Listing, scrolling down: one row — Harnesses at 15px beside the compact Filter, a 53px band, the Recent Work header pinned under it
Listing, scrolling down: one row — Harnesses at 15px beside the compact Filter, a 53px band, the Recent Work header pinned under it open page →history · 2 moments →
Listing, scrolling up: title 20px and the Filter with its labels; the band grows to 81px
Listing, scrolling up: title 20px and the Filter with its labels; the band grows to 81px open page →history · 2 moments →
The Q3 lever off: scrolling down with the Filter keeping its labels — row 1 stays as tall as the full Filter, which is what the compact look saves
The Q3 lever off: scrolling down with the Filter keeping its labels — row 1 stays as tall as the full Filter, which is what the compact look saves open page →history · 2 moments →

In the archive

Artifacts

/pages/sticky-title/components/layout/ux-sticky-band/inventory/harnesses/detail?adapterId=claude-cli (local dev)/inventory/harnesses (local dev)

Qpoint Brand Style Guide