Skip to content

Commit 14abab0

Browse files
committed
Query dialogs: promote the chip + popover primitives to lib/ui and standardize the footer buttons (M8)
The Search and Selection dialogs' reusable primitives now live in `lib/ui/` and appear in Debug > Components, so David's standard-component restyle sweep reaches them. Behavior-preserving except the one deliberate footer-button restyle. - **`lib/ui/Dropdown.svelte`** (promoted from `filter-chips/FilterChipPopover.svelte`): the generic positioned floater (frosted glass, auto-flip, focus trap, Esc-scoped close). Same API (`anchor` / `open` / `onClose` / `ariaLabel` / children). Renders `.ui-dropdown` (was `.filter-chip-popover`); the host dialog's capture-phase Escape guard, the E2E overlay-dismissal helpers, and `search-filters.spec.ts` follow the rename. `RecentItemsPopover` and the three filter popovers now float through it. - **`lib/ui/FilterDropdown.svelte`** (new): a thin `Dropdown` + labelled section header for the Size / Modified / Search-in surfaces. Chosen over a `variant` prop so the generic `Dropdown` stays free of filter markup. The `*FilterPopover` bodies thread only `anchor` / `open` / `onClose` / `label`. - **`lib/ui/Chip.svelte`** (promoted from `filter-chips/FilterChip.svelte`): one component, two variants. `filter` keeps the exact filter-chip look + API (popover trigger, × clear, Backspace clear, aria-expanded). `recent` replaces the inline `<button>` pills in `RecentItemsFooter.svelte` (leading mode badge, truncating label, click, right-click remove). Same appearance for both. - **Footer buttons (the `size="mini"` fix, deliberate visual change):** `QueryDialog`'s primary/secondary actions ("Show all in main window" / "Go to file" / "Select these files") and the recent footer's "All …" button are now standard `Button` (`variant="primary|secondary"`, `size="regular"`) with the shortcut hint on a `ShortcutChip` instead of bespoke inline spans. Dropped the now-unused `.shortcut-hint` contrast cases from the Go a11y model; `ShortcutChip` carries its own audited contrast. Also dropped the doubled `⌘H` in Selection's trailing label (the chip renders it now). - **Catalog:** new `Dropdown`, `FilterDropdown`, and `Chip` sections in Debug > Components, wired into the sidebar. - **Cleanup:** deleted the orphaned `SearchFooterActions.svelte` (+ its three tests) - Search's footer renders through `QueryDialog` from `config.{primary,secondary}Action`, not a Search-local component. - Tier-3 a11y + behavior tests moved/added under `lib/ui/` for `Dropdown` and `Chip`; existing dialog + chip tests stay green (4496 Svelte tests pass). Docs updated across `lib/ui`, `filter-chips`, `query-ui`, and `search`.
1 parent 9effb0e commit 14abab0

47 files changed

Lines changed: 940 additions & 751 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/src/lib/query-ui/DETAILS.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,9 @@ companion test catalog (mirrors the file table above):
154154
| `recent-chips-layout.test.ts` | Greedy-fit packing against mocked widths |
155155
| `recent-items-utils.test.ts` | `modeBadge`, `modeName`, `formatAge`, `filterSummary`, `chipTooltip` rules |
156156

157-
Filter-chips tests (`FilterChips`, `FilterChip`, `FilterChipPopover`, `filter-chip-state`, `filter-popover-helpers`) are
158-
catalogued in [`filter-chips/CLAUDE.md`](filter-chips/CLAUDE.md).
157+
Filter-chips tests (`FilterChips`, the `*FilterPopover` bodies, `filter-chip-state`, `filter-popover-helpers`) are
158+
catalogued in [`filter-chips/CLAUDE.md`](filter-chips/CLAUDE.md). The chip and popover primitives themselves are
159+
`$lib/ui/Chip` and `$lib/ui/Dropdown` (tested in `lib/ui/`).
159160

160161
## State shape contract
161162

@@ -232,8 +233,10 @@ Small contracts that apply to every consumer of the query UI:
232233
- `QueryBar.svelte`'s run button has the `` shortcut at the suffix slot at `--spacing-xs` from the "Search" label so
233234
the rhythm matches "Go to file ⏎" and "All searches… ⌘H" elsewhere.
234235
- `RecentItemsFooter.svelte` + `recent-chips-layout.ts` use a greedy-fit layout: leading label ("Recent searches:" or
235-
"Recent selections:") and trailing button ("All searches… ⌘H" or equivalent) are always rendered; the middle slot
236-
packs as many chips as fit, dropping the rest silently. No horizontal scrolling, no ellipsis chip.
236+
"Recent selections:") and trailing button ("All searches…" + a `⌘H` `ShortcutChip`, or equivalent) are always
237+
rendered; the middle slot packs as many chips as fit, dropping the rest silently. No horizontal scrolling, no ellipsis
238+
chip. The pills are `$lib/ui/Chip` (`variant="recent"`, mode badge in its `leading` slot), and the trailing control is
239+
a standard `$lib/ui/Button` (`variant="secondary"`); the layout helper measures `.chip-recent` and `.all-recent`.
237240
- Each chip's tooltip leads with the full text so a CSS-ellipsis-truncated chip stays readable on hover.
238241
- Path column font is `--font-size-sm` (matching the filename column) with `--spacing-xxs` row vertical padding so the
239242
row height stays compact.
@@ -301,8 +304,11 @@ The bar's run button reads `Search ⏎` only when `enterAction === 'run-search'`
301304
### Footer buttons always visible
302305

303306
The policy: footer actions render unconditionally; when there are no results (or the index isn't ready) they render
304-
disabled instead of hidden, so the layout stays still while the user types. The specific Search footer buttons ("Show
305-
all in main window", "Go to file") live in `lib/search/SearchFooterActions.svelte`.
307+
disabled instead of hidden, so the layout stays still while the user types. `QueryDialog.svelte` renders the
308+
primary/secondary footer actions itself, from `config.primaryAction` / `config.secondaryAction`, as standard
309+
`$lib/ui/Button`s (`variant="primary" | "secondary"`, `size="regular"`) with the shortcut hint on a `ShortcutChip`.
310+
Search wires "Show all in main window" (primary, ⌥⏎) + "Go to file" (secondary, ⏎) through that config; Selection wires
311+
"Select these files" (primary, ⏎). There's no separate per-consumer footer component.
306312

307313
The Content chip is visible-disabled with a "Coming soon" tooltip. It has **no** shortcut. Wiring a shortcut to a
308314
disabled control is hostile UX; reserving `⌘4` is the better contract. When Content ships, it claims `⌘3` and Regex
@@ -404,10 +410,11 @@ entries so callers (the tab-state manager) can release per-entry resources in on
404410
trigger. The same applies to recent-search AI entries (footer chip click + popover Enter both run). Anything the user
405411
deliberately picks from the dialog is the same kind of "yes, please" as pressing Enter.
406412

407-
**Decision**: `RecentItemsPopover` reuses `FilterChipPopover` for positioning + focus trap + Esc-scoped close. **Why**:
413+
**Decision**: `RecentItemsPopover` reuses `$lib/ui/Dropdown` for positioning + focus trap + Esc-scoped close. **Why**:
408414
The plan calls for a sub-overlay-of-an-overlay with the same auto-flip, focus-trap, and "Esc closes only the popover"
409415
semantics as the filter chips. Reimplementing those risks drift; reusing the primitive guarantees the contract covers
410-
both popover kinds via the single `.filter-chip-popover` DOM selector.
416+
both popover kinds via the single `.ui-dropdown` DOM selector (the same selector the filter popovers render through
417+
`FilterDropdown`).
411418

412419
**Decision**: Path pills inside result rows are mouse-only and not in the keyboard Tab order. **Why**: Making the pills
413420
tabbable inside virtualized rows would break the row's arrow-down keyboard flow: pressing Down at the end of a row would

apps/desktop/src/lib/query-ui/QueryDialog.svelte

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
import type { QueryDialogConfig } from './query-dialog-config'
4747
import { getSetting, onSpecificSettingChange } from '$lib/settings'
4848
import { trapFocus } from '$lib/ui/focus-trap'
49+
import Button from '$lib/ui/Button.svelte'
50+
import ShortcutChip from '$lib/ui/ShortcutChip.svelte'
51+
import { tooltip } from '$lib/tooltip/tooltip'
4952
import StatusBadge from '$lib/ui/StatusBadge.svelte'
5053
import { addToast } from '$lib/ui/toast/toast-store.svelte'
5154
import { showAiTranslateErrorToast } from '$lib/ai/translate-error-toast'
@@ -244,7 +247,7 @@
244247
*/
245248
function handleEscapeCapture(e: KeyboardEvent): void {
246249
if (e.key !== 'Escape') return
247-
if (dialogElement?.querySelector('.filter-chip-popover')) {
250+
if (dialogElement?.querySelector('.ui-dropdown')) {
248251
return
249252
}
250253
e.preventDefault()
@@ -928,34 +931,34 @@
928931
{#if config.secondaryAction || config.primaryAction}
929932
<div class="query-dialog__actions" role="group" aria-label="Dialog actions">
930933
{#if config.secondaryAction}
931-
<button
932-
type="button"
933-
class="btn btn-secondary btn-mini"
934+
<Button
935+
variant="secondary"
934936
disabled={config.inputsDisabled || results.length === 0}
935937
onclick={activateSecondary}
936938
aria-label={config.secondaryAction.ariaLabel ?? config.secondaryAction.label}
937-
title={config.secondaryAction.tooltip ?? ''}
938939
>
939-
{config.secondaryAction.label}{#if enterAction === 'go-to-file'}<span
940-
class="shortcut-hint"
941-
aria-hidden="true">{config.secondaryAction.shortcutHint}</span
942-
>{/if}
943-
</button>
940+
<span class="action-label" use:tooltip={config.secondaryAction.tooltip ?? ''}>
941+
{config.secondaryAction.label}{#if enterAction === 'go-to-file'}<ShortcutChip
942+
key={config.secondaryAction.shortcutHint}
943+
size="sm"
944+
/>{/if}
945+
</span>
946+
</Button>
944947
{/if}
945948
{#if config.primaryAction}
946-
<button
947-
type="button"
948-
class="btn btn-primary btn-mini"
949+
<Button
950+
variant="primary"
949951
disabled={config.inputsDisabled || results.length === 0}
950952
onclick={activatePrimary}
951953
aria-label={config.primaryAction.ariaLabel ?? config.primaryAction.label}
952-
title={config.primaryAction.tooltip ?? ''}
953954
>
954-
{config.primaryAction.label}<span
955-
class="shortcut-hint shortcut-on-primary"
956-
aria-hidden="true">{config.primaryAction.shortcutHint}</span
957-
>
958-
</button>
955+
<span class="action-label" use:tooltip={config.primaryAction.tooltip ?? ''}>
956+
{config.primaryAction.label}<ShortcutChip
957+
key={config.primaryAction.shortcutHint}
958+
size="sm"
959+
/>
960+
</span>
961+
</Button>
959962
{/if}
960963
</div>
961964
{/if}
@@ -1068,19 +1071,10 @@
10681071
padding: var(--spacing-sm) var(--spacing-lg);
10691072
}
10701073
1071-
/* No `opacity` dimming on the shortcut hints: at `opacity: 0.8` the composited
1072-
tertiary gray (and the accent-fg on the primary button) dropped below WCAG AA on
1073-
the lighter accents. `--color-text-tertiary` already reads quieter than the button
1074-
label, and `--color-accent-fg` is auto-picked for max contrast on its accent.
1075-
The contrast checker models both pairs (`scripts/check-a11y-contrast/query_dialog_states.go`). */
1076-
.shortcut-hint {
1077-
margin-left: var(--spacing-xs);
1078-
font-family: var(--font-mono);
1079-
font-size: var(--font-size-sm);
1080-
color: var(--color-text-tertiary);
1081-
}
1082-
1083-
.shortcut-hint.shortcut-on-primary {
1084-
color: var(--color-accent-fg);
1074+
/* The action verb leads; the shortcut hint rides a standard `ShortcutChip` to its right. */
1075+
.action-label {
1076+
display: inline-flex;
1077+
align-items: center;
1078+
gap: var(--spacing-xs);
10851079
}
10861080
</style>

apps/desktop/src/lib/query-ui/QueryDialog.svelte.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ describe('QueryDialog ⌘N and ⌘H', () => {
319319
// The popover mounts via FilterChipPopover; either marker class would work,
320320
// but the wrapper exposes the `[data-recent-items-popover]` hook below.
321321
const popoverAfterOpen = document.body.querySelector(
322-
'[data-recent-items-popover], .recent-searches-popover, .filter-chip-popover',
322+
'[data-recent-items-popover], .recent-searches-popover, .ui-dropdown',
323323
)
324324
expect(popoverAfterOpen).not.toBeNull()
325325

0 commit comments

Comments
 (0)