← Cycles
Design · c104

Sticky Band

archivedcreated 2026-10-01· last activity 2026-10-01· mode: evolution, with archaeology (app.qpoint.io's page-level sideways scroll was considered and rejected)

One sticky band for qdash detail pages: `NavJumpPills` and the global filter pin together, and table headers pin directly under them

New in this cycle: One sticky band: the global Filter and NavJumpPills stay sticky where they sit and register with useStickyBand, which publishes --q-sticky-top; UxStickyBand backs them once pinned, table headers pin directly under the band, pill jumps land below it, and UxOverflowX lets only a too-wide table scroll sideways

Why

DataTable's always-on overflow-x-auto wrapper stopped every UxTableList header from pinning. Fixing it exposed what that bug had been hiding: three independent stickies (the filter at z-40, the pills at z-30, the table header at z-2), each with its own hard-coded offset, and nothing that knows how they stack. Long ledgers on detail pages now pin their header under the pills, which cover the first two or three column labels.

Objective

Detail pages pin one band: pills on the left, filter on the right, a solid backing only once pinned. The band publishes its height as --q-sticky-top, and table headers, jump targets and the scroll-spy all read it. Tables fit by default; the rare one that doesn't scrolls sideways through one layer primitive. Done = plan.md §Done means.

Scope

  • Layer: useStickyBand, UxStickyBand, UxOverflowX; NavJumpPills (sticky prop, var-driven offset and scroll-margin); Filter (registers with the band); UxTableListHeader (var top); DataTable (onto UxOverflowX); UxModal (resets the band offset).
  • Design site: /pages/sticky-band mock + registry entry; component docs; replica table wrappers.
  • Fable app: layouts/default.vue; 11 detail pages with pills; pill-target scroll-mt-20; UsersRoster.vue (+ DeviceCard.vue if it wraps a table).
  • Out: a stuck chip (evaluation option C); qcontrol ui/ :3030; non-table overflow-x-auto boxes.

Key Changes

kick-off (2026-10-01): the six evaluation §7 decisions answered → evaluation.md §8. Band only when pinned; backing only when pinned; no stopgap, the header fix waits for the band's release; tables fit by default + one overflow primitive (built as UxOverflowX) (app.qpoint.io's #page-scroll-window approach rejected: the whole page slides sideways and every offset must target the custom scroller); c104 owns the fix and the 0.9.24 release; design mock first, then the Fable app :3032 (Mark, correcting :3030). Since both controls already pin at 8px, "only when pinned" makes the band a coordinator over stickies that stay in place, not one container they move into (plan.md §Architecture). Plan decisions P1–P6 confirmed by Mark as drafted (2026-10-01): var on the document root, NavJumpPills owns its pinning, band height counts every band control on the page, background over the content column only, NavJumpPills sets its targets' scroll-margin, stuck chip out.

header fix committed (2026-10-01): layer 11e80a9 DataTable: arm overflow-x only while the table overflows, so the thead pins. Local and unpushed. The Fable app resolves the sibling layer checkout, so it is already live on :3032 in dev, collision included; its package fallback is layer main, so layer main stays unpushed until 0.9.24.

Phase 1 layer pieces + Phase 2 mock (2026-10-01): layer 37ec2c8 (local, unpushed, on 11e80a9), alongside another session's uncommitted HistoryGraph/DataMachine work in the same tree (not staged). New composables/useStickyBand.ts (useStickyBand → { stuck }, useStickyBandMember), UxStickyBand (backing), UxOverflowX (a wrapper component, not the planned composable: consumers swap their div for it; measures scrollWidth because UxTableList puts a full-width div between box and table, the first try missed it). UxTableListHeader pins at var(--q-sticky-top, 0px); UxModal and an armed UxOverflowX reset the var to 0, since a header inside a scroll box would otherwise be pushed down by the band height. Both memberships opt-in (Filter band, NavJumpPills sticky): the Fable app reads the sibling layer live while c103 runs, so nothing joins a band there until Phase 4. Layer CLAUDE.md: catalog rows + "unreleased, ships as v0.9.24" migration notes. Mock: design /pages/sticky-band (qcontrol layout, Fable shell replica; Band/Before switch, Filter/Pills toggles, 14-column wide ledger, live --q-sticky-top readout) + page-registry.ts entry. Verified in the pane at 1440×900: band height 81px with the filter (8 + 65 + 8), 58px pills only, unset with neither; band backing at opacity 1 once pinned; ledger headers pin at 81 and the second pushes the first off; every pill jump lands at 97 (band + 16) with its pill lit (Notes, last, lights by the bottom rule); Before mode reproduces the collision (header at 0 under the pills and filter). Wide-table arming checked on :3032 instead (Activity at 1024: 1132 in 748 → armed, var 0px inside; at 1700 disarmed, header pins at 0) because the design dev server (:3000, another session's) kept serving the first OverflowX.vue — its watcher missed every later edit to that one file; restart it to see the wide ledger arm in the mock. Fable /inventory/skills/detail unchanged (old wrapper pins at 8, no console errors). Not done yet: /document-components docs for the new pieces.

release (2026-10-01, by the c103 session): while Phase 2 was being checked, c103's close released layer 0.9.24 (1bea0a7, tag pushed), which includes c104's 11e80a9 + 37ec2c8. So decision 8.3 (hold the release) was overtaken: the header fix and the opt-in band shipped before the Fable port. Lockfiles were refreshed in design (af1c7512), qcontrol ui/ (c5fb5520) and Fable (3d557e53). Layer CLAUDE.md's "unreleased" migration-notes heading relabelled to v0.9.24.

Phase 4, Fable port (2026-10-01): Fable 579b512c on c99/claude-code-fable-5.1 (local, unpushed; c103 was closed and the tree clean). layouts/default.vue mounts <UxStickyBand class="-mx-8"> in the relative wrapper ahead of GlobalFilterBar, whose Filter gains band. 11 detail pages swap <div class="sticky top-2 z-30 mb-6"><NavJumpPills> for <NavJumpPills sticky class="mb-6">; 28 components drop scroll-mt-20 from their sec-* pill targets (SessionReading's transcript/raw/surface anchors kept: not pill targets). UsersRoster onto UxOverflowX (tableEl.$el); DeviceCard's overflow-x-auto is a grid, not a table, left alone. npm test 553 passed + bible-check clean. Checked on /inventory/providers/detail?provider=anthropic: band 81px, jumps land at 97, the Harnesses header pins at 81. The pane was hidden for part of this (no rAF, smooth scroll stalls), so jumps were checked with instant scrolls.

Phase 3 + docs (2026-10-01): design b9f4c633. Six hand-rolled overflow-x-auto wrappers around UxTableList onto UxOverflowX (replica DataTable, qcontrol/teams, pages/control-center, phase1/{tools,files,plugins}; tableEl refs read $el). /document-components: UxOverflowX + UxStickyBand registry entries (layout group) and demos; NavJumpPills sticky/stickyTop (demo exposes them), Filter band, UxTableListHeader description. Layer 49e90f0 (pushed): the template comment above each new root moved inside it, because in dev a root-level comment makes the component a fragment and ref.$el becomes the comment node (the UxOverflowX docs demo 500'd on $el.classList); production strips comments, so 0.9.24 builds are unaffected. The design dev server's stale OverflowX.vue caught up by itself; the mock's wide ledger now arms on :3000.

Captures (2026-10-01): design 54b769d6. Shot programs c104-sticky-band.json (6 mock states: rest, pinned, jump, wide, filter only, before) and c104-sticky-band-fable.json (provider + skill detail on :3032), thread band; only the c104 manifest entries staged (the c100 session's manifest edits left in the tree).

Outcome

The filter and the jump pills now pin as one band, and table headers pin directly under it instead of disappearing (the original bug) or sliding under the pills (the bug the fix exposed). useStickyBand coordinates controls that stay sticky where they sit, rather than moving them into one container, because both already pinned at 8px. Membership is opt-in (Filter band, NavJumpPills sticky), the band publishes --q-sticky-top, and UxStickyBand backs it only when pinned. Tables are meant to fit; UxOverflowX lets the rare one that doesn't scroll sideways and gives up its pinned header only then. Shipped in layer 0.9.24, ported to all 11 Fable detail pages, with a before/after mock at /pages/sticky-band.

Learnings:

  • A fix that makes something work can expose a collision it was hiding. The header fix was right, but it needed the band before it could ship cleanly.
  • Hold-the-release decisions don't survive a shared layer with parallel sessions. c103 released our unreleased commits. Opt-in membership is what kept that safe; the header fix itself did reach apps that use pills and haven't ported (ui/ :3030).
  • In dev, a root-level template comment turns a component into a fragment, so $el becomes the comment node. Keep comments inside the root element.
  • A hidden Browser pane has no requestAnimationFrame: smooth scrolling and rAF-driven state stall. Verify geometry with instant scrolls, or capture headless.

Follow-ons:

  • Port qcontrol ui/ (:3030) Dropped 2026-10-01 (Mark): the Fable branch is the active app and :3030 is deprecated. The Fable branch is pushed with the port (579b512c, origin at 97d59f8d).
  • SessionReading's Transcript ⇄ Surface switch is a third sticky top-2 z-30 control and isn't a band member.
  • The stuck chip (evaluation option C) is still open.
  • Any other vertical scroll box that holds a non-compact table must reset --q-sticky-top.

Visual record

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

At rest: filter on the title row, pills below the tiles, no backing; the readout shows --q-sticky-top 81px (8 + 65px filter + 8)
At rest: filter on the title row, pills below the tiles, no backing; the readout shows --q-sticky-top 81px (8 + 65px filter + 8) open page →
Fable /inventory/providers/detail (Anthropic): pills and filter pinned as one band, the Harnesses ledger header pinned under it
Fable /inventory/providers/detail (Anthropic): pills and filter pinned as one band, the Harnesses ledger header pinned under it history · 4 moments →
Pinned: filter and pills side by side at 8px over the band backing; the Installs ledger header pins directly under it at 81px
Pinned: filter and pills side by side at 8px over the band backing; the Installs ledger header pins directly under it at 81px open page →
Fable /inventory/skills/detail (deploy): the band over the Installs and Sessions ledgers, header pinned under it
Fable /inventory/skills/detail (deploy): the band over the Installs and Sessions ledgers, header pinned under it history · 5 moments →
Pill jump to Sessions: the heading lands 16px below the band (97px) and its pill lights
Pill jump to Sessions: the heading lands 16px below the band (97px) and its pill lights open page →
Wide ledger (14 columns): UxOverflowX arms horizontal scroll, so this header gives up pinning while the ledgers that fit keep it
Wide ledger (14 columns): UxOverflowX arms horizontal scroll, so this header gives up pinning while the ledgers that fit keep it open page →
Filter only (pills off): the band is still 81px, the filter's height; the header pins under it
Filter only (pills off): the band is still 81px, the filter's height; the header pins under it open page →
Before c104 (today's per-page wrapper, header at top-0): the pinned header slides under the pills and filter, which cover its first and last columns
Before c104 (today's per-page wrapper, header at top-0): the pinned header slides under the pills and filter, which cover its first and last columns open page →

In the archive

Artifacts

/pages/sticky-band/inventory/providers/detail?provider=anthropic (local dev)/inventory/skills/detail?skill=deploy (local dev)

Qpoint Brand Style Guide