← Cycles
Design · c95

Trigger-Bar Column Picker

activecreated 2026-09-25· last activity 2026-09-27· mode: assemblage — two finished modules composed into one; the work is the join (ownership, "default" semantics, chrome), not either part

Seat c93's `UxColumnPicker` on the right edge of c92's `FilterTriggerBar` as a default-but-optional add-on — one strip above every qdash listing table instead of two controls competing for the same edge

Why

Every qdash listing now carries two controls in the strip between its stat band and its table, and both want the same seat: the right edge, over the table. The trigger bar (c92) already owns that edge by design (full-width root, right-aligned, no consumer wrapper). The column picker (c93) was ruled to sit "above the whole component" in a consumer-owned toolbar row — the same row. Left separate, every listing composes the row by hand and the picker drifts from page to page. Hosting the picker on the bar gives it one canonical seat and lets a consumer opt in by passing its column catalog.

Objective

  1. A super component (working name DataListingBar, name open) composes FilterTriggerBar + UxColumnPicker into one strip: the bar's filter props pass through, columns + v-model:visibleColumns render the picker on the right edge, :columnPicker="false" opts out, and its own #end slot takes any other trailing control. FilterTriggerBar stays filter-only and gains only an #end slot (needed for the inside-the-border join).
  2. UxColumnPicker gains a trigger variant that takes the bar's chrome (TONE roles, hairline divider, inset focus ring), so the join reads as one instrument; the standalone bordered trigger remains for listings without a bar.
  3. The join is prototyped live on the design site (both candidate forms, full scenario coverage) ahead of the visual design, transcribed once the Figma lands, extracted to the layer with docs and a release, and rolled out to the qdash trigger-bar listings on feat/new-pages.

Visual designs to follow (Mark). Composition options, the ownership fork, phases, and the six questions with their answers in plan.md.

Scope

  • Design: cycles/c95.trigger-bar-column-picker/ (initial-prompt.md, plan.md, this record); Phase 1 prototype — a temporary design-local shadow of FilterTriggerBar or app/pages/pages/trigger-bar-column-picker.vue + page-registry.ts; _demos/FilterTriggerBar.vue, _demos/UxColumnPicker.vue, new _demos/DataListingBar.vue, component-registry.ts; scripts/shot-programs/c95-*.json.
  • q-nuxt-layer: components/filter/TriggerBar.vue (#end slot), components/ux/{Popover,ColumnPicker}.vue (extracted from design; picker gains a segment trigger variant), new components/data/ListingBar.vue (super component, name open).
  • qcontrol (feat/new-pages): crates/qdash/ui/app/components/DevicesOverview.vue, pages/inventory/**/index.vue, possibly components/UsersOverview.vue.

Absorbs c93's layer extraction (UxPopover / UxColumnPicker, design-local at creation) into Phase 3.

Key Changes

  • Workspaces gets the full bar (2026-09-25, design c92 todos plan 15): Repos & Workspaces moves from plan 08's standalone UxColumnPicker to the full DataListingBar with a Team key. The new teams column (user → team via /api/teams) carries filterKey: 'team', and useQuickFilter(gf, 'team') starts there: Team · All · Data · Frontend · Payments · Platform · Columns. The users / hosts columns are gone, so user / host (both over the cap) live only in the Filter bar. qdash on disk (the strip half is uncommitted, like every other strip consumer).
  • new consumers (2026-09-25, design c92 todos plan 08 Part B): Mark: "the trigger bar doesn't have our new column editor or the dropdown key option we just created — add it to this and other instances on the other listing pages." qdash (feat/new-pages, on disk): UsersOverview swaps FilterTriggerBar for DataListingBar and gains a column model on its UxTableList (not DataTable: rows pre-open by ref, own search); TeamsOverview + pages/teams/index (usePeopleData({ register: 'teams' }) → team key, ?team= now the filter, not a forward); inventory/{endpoints (Team; endpoint column filterKey: 'user','team'), providers (label→filterKey: 'provider'), models (Provider), workspaces (users/hosts→user/host)}, all in DataTable #toolbar; inventory/index Harnesses strip onto useQuickFilter. useQuickFilter gains QUICK_FILTER_MAX_OPTIONS (8): a key with more values stays off the strip — FilterTriggerBar has no overflow handling and a 14-host key pushed the key segment off-screen (layer question, not changed here); Workspaces, whose only keys are over the cap, seats a standalone UxColumnPicker until a short key has a column. The seven existing strips are unchanged.
  • build (2026-09-25): Mark's follow-up — add a DataTable slot, skip the deep Phase 1, execute. Layer: UxPopover + UxColumnPicker moved from design app/components/ux/ to components/ux/ (popover root inline-flex, inset focus ring; picker gains a #trigger slot exposing { open, label, visibleCount, hiddenCount, total }); FilterTriggerBar gains #end (trailing hairline-divided segment, scope { tone }) with the overflow clip moved inward to the key+values group so a popover can escape; DataTable gains #toolbar (title row when titled, own row otherwise, outside the scroll box) and the c93 sort-by-hidden-column gap closes (fallback to initialSort, else first sortable); new DataListingBar (data/ListingBar.vue) composes bar + picker — bar props pass through, columns + v-model:visibleColumns add the picker, columnPicker=false opts out, side defaults to above, segment reads "Columns" until something is hidden then "N of M" in the selected tone. Design site: local prototypes deleted; registry entries for both extracted components point at the layer and list the new slots; new DataListingBar entry + _demos/DataListingBar.vue (live filter + picker in a titled DataTable; opt-out / nothing-hidden / three-hidden / multi-key + lead-in / overflow + extra #end scenarios); FilterTriggerBar, UxColumnPicker, DataTable demos gain the slot scenarios; /pages/column-picker gains a "Seated in the listing bar" section. qdash (feat/new-pages): DevicesOverview and inventory/{index (Harnesses table), harnesses, sessions, tools, skills, mcp-servers} swap FilterTriggerBar for DataListingBar inside DataTable #toolbar, first column locked, visible keys as a local ref; UsersOverview keeps the bar (UxTableList, no column model). Layer CLAUDE.md migration notes added (unreleased). Not committed, not released — Mark reviews first.
  • key dropdown (2026-09-25): Mark, with the c92 reference screenshot: "the original filter bar shows the left filter item as a dropdown, can we integrate that as well?" The bar already renders the key caret + dropdown when filterKeyDefs has more than one entry (c92 rule: a caret only when there is a choice); every qdash strip passed one key, so none showed it — while every page registers two or three keys with suggest() in gf.register(). Fix is wiring, not a control: new qdash composables/useQuickFilter.ts turns the page's registration into the strip's inputs (keyDefs = registered keys with suggestions, plus the initial key; filterKey; options via suggest(key) + formatVal; value; onValue → gf.set/clear), returned reactive so v-model:filterKey binds directly. Seven strips switched: inventory/{sessions (status·flagged), harnesses (provider·kind), tools (kind·server), skills (adapter·source), mcp-servers (source)}, DevicesOverview + UsersOverview (team·provider) — the two presentational components now take one quickFilter prop in place of teamOptions/activeTeam/update:team; pages/{devices, users/index, teams-users-devices} build the objects (the combined page gets one per strip so the two key segments are independent); usePeopleData drops teamOptions. inventory/index (posture only) stays single-key by the caret rule. Design: the DataListingBar demo's default instance now shows the multi-key form. No layer change.
  • keys restricted to columns (2026-09-25): Mark: "populate that dropdown with the columns from the table" → of three models (table-local filter · hybrid · global keys restricted to columns) he chose global keys only, restricted to columns. DataListingBar now offers only the filterKeyDefs its catalog has a column for (restrictKeys, default true); a column matches by key or by the new Col.filterKey?: string | string[]; a current key the table can't answer is replaced by the first offered one (emitted). qdash annotations: sessions exit_code→status, risk→flagged; harnesses adapter_id→['provider','kind'] (its cell shows vendor · kind); skills harnessList→adapter; UsersOverview filters to its cols map. Net: Devices and Users offer Team only (no caret — Provider isn't a column); Sessions Status·Flagged; Harnesses Provider·Kind; Tools Kind·Server; Skills Harness·Source; MCP servers Source. Demo gains a restricted-keys scenario.
  • setup-order bug (2026-09-25): useQuickFilter first evaluated each page's suggest() during setup (an immediate watcher, then a plain watch on a computed — which also evaluates its source at creation); Harnesses/Skills/Tools declare the lists suggest reads after gf.register(), so setup threw "Cannot access 'vendors' before initialization" and the pages rendered blank. Fix: suggestions live in lazy computeds evaluated at render; async results settle through a watch created in onMounted. Verified on all seven strips.
  • key-select ring (2026-09-25): Mark's styling note (blue outline on the key segment after a pick) → the c92 peer-focus-visible overlay removed; Chrome treats a clicked <select> as keyboard-focused, so the ring showed on every mouse pick.
  • decisions (2026-09-25, follow-up): DataTable seat added; Phase 1 collapsed; picker chrome via #trigger slot + bar #end scope rather than a variant prop.
  • decisions (2026-09-25): prototype both visual joins now, ahead of the Figma; absorb c93's layer extraction into this cycle's Phase 3; roll out Devices first then every trigger-bar DataTable listing. Ownership: Mark's two readings (grow FilterTriggerBar vs. a super component) weighed in plan.md "Ownership fork" → super component; the bar stays filter-only with an #end slot. All six questions closed.
  • plan (2026-09-25): survey of the bar (layer, eight qdash consumers, TONE rulings) and the picker (design-local, trigger chrome slot-owned) → three composition options (slot / props / props + slot, recommending props + slot), two candidate visual joins (segment inside the bar / sibling beside it), five phases, six open questions → plan.md.

Outcome

(open)

Known gaps (open — Mark asked these be kept, 2026-09-25)

  • High-cardinality keys don't fit the bar. FilterTriggerBar has no overflow handling: a key with many values (14 hosts on Endpoints) pushes the key segment off the left edge. Stopgap in qdash useQuickFilter — QUICK_FILTER_MAX_OPTIONS = 8: a key with more values stays off the strip (it remains in the Filter bar above), so host / user never show as bar keys today, and Repos & Workspaces seated a standalone UxColumnPicker until c92 todos plan 15 gave it a Team column (now the full bar, Team key). Needs a layer answer before host or user can sit on the bar: horizontal scroll inside the values group, an overflow ("+N more") menu, or the values segment becoming a select/combobox past a threshold. Test with the Endpoints host key (14 values) and the DataListingBar demo's overflow scenario.
  • The caret rarely shows. With keys restricted to columns, most listings have one column-backed key (Users → Team, Providers / Models → Provider, Endpoints → Team), so there is nothing to drop down. Revisit together with the gap above — the high-cardinality keys are the missing second key on most pages.
  • initialSort matches column labels, not keys (DataTable): pages that pass a key (Endpoints cost, Providers tokens, Workspaces lastMs) start unsorted, and their comments say otherwise. Either the pages pass labels or the layer accepts both. qdash fixed 2026-09-25 (Mark: yes) — the three pages now pass labels (spend, tokens · share, last active; qcontrol 5450453d + Endpoints on disk); the layer question (accept keys too) stays open.

Artifacts

Qpoint Brand Style Guide