Trigger-Bar Column Picker
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
- A super component (working name
DataListingBar, name open) composesFilterTriggerBar+UxColumnPickerinto one strip: the bar's filter props pass through,columns+v-model:visibleColumnsrender the picker on the right edge,:columnPicker="false"opts out, and its own#endslot takes any other trailing control.FilterTriggerBarstays filter-only and gains only an#endslot (needed for the inside-the-border join). UxColumnPickergains a trigger variant that takes the bar's chrome (TONEroles, hairline divider, inset focus ring), so the join reads as one instrument; the standalone bordered trigger remains for listings without a bar.- 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 ofFilterTriggerBarorapp/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(#endslot),components/ux/{Popover,ColumnPicker}.vue(extracted from design; picker gains asegmenttrigger variant), newcomponents/data/ListingBar.vue(super component, name open). - qcontrol (
feat/new-pages):crates/qdash/ui/app/components/DevicesOverview.vue,pages/inventory/**/index.vue, possiblycomponents/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
UxColumnPickerto the fullDataListingBarwith a Team key. The newteamscolumn (user → team via/api/teams) carriesfilterKey: 'team', anduseQuickFilter(gf, 'team')starts there: Team · All · Data · Frontend · Payments · Platform · Columns. Theusers/hostscolumns are gone, souser/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):UsersOverviewswapsFilterTriggerBarforDataListingBarand gains a column model on itsUxTableList(notDataTable: rows pre-open by ref, own search);TeamsOverview+pages/teams/index(usePeopleData({ register: 'teams' })→teamkey,?team=now the filter, not a forward);inventory/{endpoints (Team; endpoint columnfilterKey: 'user','team'), providers (label→filterKey: 'provider'), models (Provider), workspaces (users/hosts→user/host)}, all inDataTable #toolbar;inventory/indexHarnesses strip ontouseQuickFilter.useQuickFiltergainsQUICK_FILTER_MAX_OPTIONS(8): a key with more values stays off the strip —FilterTriggerBarhas 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 standaloneUxColumnPickeruntil a short key has a column. The seven existing strips are unchanged. - build (2026-09-25): Mark's follow-up — add a
DataTableslot, skip the deep Phase 1, execute. Layer:UxPopover+UxColumnPickermoved from designapp/components/ux/tocomponents/ux/(popover rootinline-flex, inset focus ring; picker gains a#triggerslot exposing{ open, label, visibleCount, hiddenCount, total });FilterTriggerBargains#end(trailing hairline-divided segment, scope{ tone }) with the overflow clip moved inward to the key+values group so a popover can escape;DataTablegains#toolbar(title row when titled, own row otherwise, outside the scroll box) and the c93 sort-by-hidden-column gap closes (fallback toinitialSort, else first sortable); newDataListingBar(data/ListingBar.vue) composes bar + picker — bar props pass through,columns+v-model:visibleColumnsadd the picker,columnPicker=falseopts out,sidedefaults toabove, 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; newDataListingBarentry +_demos/DataListingBar.vue(live filter + picker in a titledDataTable; opt-out / nothing-hidden / three-hidden / multi-key + lead-in / overflow + extra#endscenarios);FilterTriggerBar,UxColumnPicker,DataTabledemos gain the slot scenarios;/pages/column-pickergains a "Seated in the listing bar" section. qdash (feat/new-pages):DevicesOverviewandinventory/{index (Harnesses table), harnesses, sessions, tools, skills, mcp-servers}swapFilterTriggerBarforDataListingBarinsideDataTable #toolbar, first column locked, visible keys as a local ref;UsersOverviewkeeps the bar (UxTableList, no column model). LayerCLAUDE.mdmigration 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
filterKeyDefshas 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 withsuggest()ingf.register(). Fix is wiring, not a control: new qdashcomposables/useQuickFilter.tsturns the page's registration into the strip's inputs (keyDefs= registered keys with suggestions, plus the initial key;filterKey;optionsviasuggest(key)+formatVal;value;onValue→gf.set/clear), returned reactive sov-model:filterKeybinds 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 onequickFilterprop in place ofteamOptions/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);usePeopleDatadropsteamOptions.inventory/index(posture only) stays single-key by the caret rule. Design: theDataListingBardemo'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.
DataListingBarnow offers only thefilterKeyDefsits catalog has a column for (restrictKeys, default true); a column matches bykeyor by the newCol.filterKey?: string | string[]; a current key the table can't answer is replaced by the first offered one (emitted). qdash annotations: sessionsexit_code→status,risk→flagged; harnessesadapter_id→['provider','kind'](its cell shows vendor · kind); skillsharnessList→adapter;UsersOverviewfilters 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):
useQuickFilterfirst evaluated each page'ssuggest()during setup (an immediate watcher, then a plainwatchon a computed — which also evaluates its source at creation); Harnesses/Skills/Tools declare the listssuggestreads aftergf.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 inonMounted. 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-visibleoverlay 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
#triggerslot + bar#endscope rather than avariantprop. - 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
DataTablelisting. Ownership: Mark's two readings (growFilterTriggerBarvs. a super component) weighed inplan.md"Ownership fork" → super component; the bar stays filter-only with an#endslot. All six questions closed. - plan (2026-09-25): survey of the bar (layer, eight qdash consumers,
TONErulings) 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.
FilterTriggerBarhas no overflow handling: a key with many values (14 hosts on Endpoints) pushes the key segment off the left edge. Stopgap in qdashuseQuickFilter—QUICK_FILTER_MAX_OPTIONS = 8: a key with more values stays off the strip (it remains in the Filter bar above), sohost/usernever show as bar keys today, and Repos & Workspaces seated a standaloneUxColumnPickeruntil 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.
initialSortmatches column labels, not keys (DataTable): pages that pass a key (Endpointscost, Providerstokens, WorkspaceslastMs) 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; qcontrol5450453d+ Endpoints on disk); the layer question (accept keys too) stays open.