Skip to content

feat(#92): unify search, filter, and command palette in the header - #109

Merged
sarg3nt merged 8 commits into
mainfrom
feature/92-unified-search-palette
May 17, 2026
Merged

feat(#92): unify search, filter, and command palette in the header#109
sarg3nt merged 8 commits into
mainfrom
feature/92-unified-search-palette

Conversation

@sarg3nt

@sarg3nt sarg3nt commented May 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • New global HeaderSearch input drives search, per-gear filter, and the Cmd/Ctrl+K command palette from a single text field at the top of every page. Header is now full-width above the sidebar so the input is visually viewport-centered. Leading > enters palette mode (slide-down panel under the input, no centered modal); backspace over > exits. / focuses, Esc clears/blurs.
  • Per-gear filter inputs are removed across the codebase. Each gear's page script now registers via window.gearbox.filter.register({onInput,onSubmit,onClear}); toolbar buttons + dropdowns also surface as palette commands via window.gearbox.commands.register({id,label,group,run}). Logs alone registers 27 commands (16 dynamic log sources, 4 line counts, 3 severities, refresh/copy/download/fullscreen).
  • Help overlay (?) redesigned as full-window smoked glass — no panel, no border, shortcuts laid out in a centered two-column grid directly on the blurred backdrop, per the issue spec.

Closes #92

Test plan

  • HAProxy: / focuses search; typing "bazar" filters backend cards down to one.
  • Home: typing fades non-matching tiles; Enter opens first match; empty-input Enter falls back to a DuckDuckGo search in a new tab.
  • Logs: Cmd+K> prefix appears, palette opens; "log s…" + ↵ switches the log source; "100" / "200" / etc. set line count; "copy", "download", "fullscreen" all work via the palette.
  • Alerts / Services / OS Updates / Bx / Traffic / Metrics / Certificates: each gear's filter (where present) drives via the HeaderSearch; each gear's toolbar surfaces commands in the palette.
  • Help overlay: ? opens full-window smoked glass; Esc, backdrop click, or ? again closes. Shortcuts render in a centered 2-column grid with no panel chrome.
  • Box width stable across idle ↔ typing ↔ palette mode at lg+ (no visible width shift on keystroke).
  • Responsive: hint chips drop out below lg (~1023px) and the box shrinks; row collapses to a magnifying-glass button below md (~767px).
  • Narrow desktop (~1280px): right-side toolbar fully visible; left chip/breadcrumb truncates; search shifts left to give the right column priority.
  • Wide desktop (≥ 2400px): search visually centered on viewport (≤4px off at 3000px).
  • Palette panel never collapses to an unusable strip on narrow viewports — minimum width 360px, clamped within the viewport with an 8px gutter.

🤖 Generated with Claude Code

sarg3nt added 7 commits May 16, 2026 14:13
Foundation for issue #92: replace the centered Cmd+K modal and the
homepage DuckDuckGo box with a single viewport-centered HeaderSearch
input that drives three behaviours via one keystroke source.

Layout
------
- Header moves out of #main-content and spans the full viewport
  (fixed top-0 inset-x-0). Sidebar now starts below the header
  (top:55px). The Gearbox logo + sidebar-collapse toggle relocate
  into the header so all chrome lives in a single bar across the
  top of the page.
- Header is a 3-col CSS grid (1fr | auto | 1fr): chip+breadcrumb
  on the left, HeaderSearch in the auto-sized middle column
  (visually centered on the viewport rather than the content area),
  per-page toolbar (#header-page-content) on the right.

HeaderSearch
------------
- New @HeaderSearch templ in base.templ + static/js/common/header-search.js.
- One input drives:
  - search/filter mode (default): each keystroke fires the active
    filter callback registered via window.gearbox.filter.register.
    Enter submits; with no registered onSubmit it falls back to a
    DuckDuckGo search in a new tab (preserves the old home-page
    behaviour).
  - palette mode (leading `>`): slide-down panel anchored to the
    input lists boxes / gears / settings / global actions / per-gear
    commands. Backspace from `>` returns to search mode.
- Cmd/Ctrl+K focuses the input and inserts `>`; `/` focuses the
  input from anywhere; Esc exits palette / clears / blurs in turn.
- Magnifying glass ↔ chevron icon swap signals current mode.
- Narrow viewports collapse to a magnifying-glass button.

Command palette
---------------
- Rewrite command-palette.js to consume HeaderSearch instead of its
  own input + centered overlay. Panel is fixed beneath the input,
  no backdrop blur, no full-viewport dim — page remains interactive
  (VS Code pattern).
- Per-gear commands surface via window.gearbox.commands.register
  ({id, label, group, run}); the palette groups them by `group`.

Help overlay
------------
- Full-window smoked-glass redesign: no panel/border/card, shortcuts
  render in a centered two-column grid directly on the blurred
  backdrop. Clicking anywhere on the backdrop closes; Esc and `?`
  still work.
- Shortcut list updated for the new keys (`>` enters palette mode,
  `/` focuses the search bar, etc.). The legacy filter-candidate
  list in shortcut-help.js is gone — HeaderSearch owns `/`.

Registry API
------------
- New static/js/common/gear-commands.js exposes:
    window.gearbox.commands.{register,clear,list,onChange,run}
    window.gearbox.filter.{register,clear,current,onChange}
- HeaderSearch + command-palette subscribe to both via onChange so
  they re-render the moment a gear's page script registers.

Refs #92
Each gear's hand-rolled filter input is removed from its templ and
replaced with a window.gearbox.filter.register call in the page
script. Toolbar buttons + dropdowns gain palette entries via
window.gearbox.commands.register so they're reachable via Cmd+K → ">".

Home gear
  - Drop the inline DuckDuckGo form (#home-search). Tile filtering
    runs through the global HeaderSearch; non-matching tiles fade
    (no GridStack re-layout). Enter opens the first matching tile,
    or falls back to DuckDuckGo when nothing matches.
  - Palette: "Add tile", "Toggle edit mode".

Logs gear (the issue's worked example)
  - #filter-input + its X clear button removed from the templ;
    filterLogs() reads __logFilterQuery instead.
  - Palette: every log source option (16 sources on this install),
    every line-count, every severity, plus Refresh / Copy /
    Download / Toggle fullscreen — the same controls the keyboard-
    averse user reaches by clicking.

Alerts, Services, HAProxy Overview
  - Filter inputs removed from the templ; per-page apply* functions
    read from a JS-managed query updated by the filter callback.
  - Palette includes each status / health / disabled dropdown
    option plus a refresh entry.

OS Updates, Bx (Fleet)
  - #pkg-search and #bx-search become hidden inputs so the
    Tabulator-backed createDataGrid({searchInput: '#…'}) keeps
    latching onto them; the filter callback writes .value and
    dispatches `input` so the existing datagrid pipeline is
    untouched.
  - Palette adds the view-filter dropdowns plus "Check for updates"
    / "Install package…" (OS Updates) and "Add box…" (Bx).

Traffic Visualization
  - No text filter on this page; the view-mode dropdown stays and
    each option becomes a palette entry, plus reset / toggle
    physics / refresh.

Metrics, Certificates
  - No filters to migrate. Palette gains "Toggle edit mode" +
    "Refresh metrics" for Metrics and "Refresh certificate list"
    for Certificates.

Refs #92
…e chevron

Three feedback-driven tweaks after the first cut landed:

1. The per-gear placeholder ("Filter backends…", "Filter log lines…",
   etc.) is gone. The box reads identically on every page: magnifier
   icon + "/ to search · Cmd K for palette" hint chips. The hint
   chips are the affordance — the box doesn't need a label that
   changes per gear too. filter.placeholder is still accepted on the
   API but no longer rendered; keeping the field so callers don't
   break.

2. The header's grid template was `1fr auto 1fr`, which sized the
   center column to its intrinsic content. Entering palette mode hid
   the hint chips, shrank the intrinsic width, and the box visibly
   narrowed. Switched to `1fr minmax(320px, 640px) 1fr` so the center
   column has a guaranteed width regardless of inner content. Also
   dropped the `focus-within:ring-2 focus-within:border-blue-500` on
   the row — the focus ring added 2px of perceived width on focus
   and the user wants the box style identical at all times.

3. The chevron-right icon I'd added for palette mode is gone. The
   `>` character in the input is already the affordance; the extra
   glyph was redundant. Now in palette mode the input shows JUST the
   `>` text the user typed, no leading icon at all. Magnifier comes
   back when palette mode exits.

Refs #92
…onsive

The box was a fixed 640px with a min of 320px in the grid template,
which was much too wide; the user asked for it to default to the size
needed to fit its contents and shrink on narrow viewports.

Three changes that together get the right behaviour:

1. Center grid column dropped back to `auto` (sizes to intrinsic
   content) instead of `minmax(320px, 640px)`. The natural width at
   wide viewports is now ~445px — icon + input + hint chips + clear
   button + padding — exactly what the user wanted.

2. Width-preserving hide for the icon, hint chips, and clear button.
   They use `invisible` (visibility:hidden) instead of `hidden`
   (display:none), so they keep their layout slot when not rendered.
   This was the missing piece behind every "box shrinks when I do X"
   complaint:
   - Idle vs typing: the clear button used to appear from display:none
     on first keystroke and widen the row by ~24px.
   - Search mode vs palette mode: the magnifier + chips used to leave
     the flow entirely, shrinking the grid auto-column.
   All three states now measure 445.36px at vw≥1024px and don't budge.

3. Hint chips responsive breakpoint moved from `sm:flex` to `lg:flex`.
   At <1024px the chips drop out via real display:none, the grid
   column shrinks to ~229px, and the user gets the "shrink some on
   narrow viewports" behaviour they asked for. Below md (768px) the
   row collapses entirely to a magnifying-glass button as before.

Also fixed a long-standing bug in the mobile expand/collapse flow:
the resize handler was unconditionally adding `flex` to the row at
md+, which overrode the responsive `hidden md:flex` rule and stuck
the row visible even after the viewport shrank back below md. Now
the row's display is owned entirely by `hidden md:flex`, and a
single .header-search-expanded marker class (with a matching CSS
rule in base.templ) drives the compact-overlay state on phones.

Refs #92
… slot

Issue: the hint chips were a flex sibling of the input, so the input
itself was only ~156px wide and the hint area was unusable for typing.
Even when focused, the user couldn't type into the right ~200px of the
box because that space belonged to the hint chips, not the input.

Fix: wrap the input in a `relative` div and absolutely position the
hint chips at the right edge of THAT wrapper, with
`pointer-events-none`. The input is now `w-full` inside the wrapper
and gets every pixel of horizontal space. The chips are an overlay
that fades in/out without affecting layout, GitHub-search style.

Visibility is now driven entirely by Tailwind peer modifiers on the
overlay (the JS hint-toggle is gone):

  opacity-0  peer-placeholder-shown:opacity-100  peer-focus:opacity-0

  - Idle (empty, not focused): chips visible.
  - Focused (empty or not): chips fade out.
  - Has content: chips fade out — including palette mode, where the
    `>` character counts as content.

Also shrank the default box. The input wrapper uses
`min-w-[8rem] lg:min-w-[13rem]`, which sizes the row's intrinsic
content to ~280px at lg+ (down from ~445px) and ~200px at md.
The gap between the magnifier and the `/` chip is now ~10px instead
of ~80px, matching the user's "get rid of most of that" feedback.

Refs #92
The grid was `1fr auto 1fr`, which gave the left and right columns
an equal share of remaining space regardless of content. On medium-
sized viewports that left wide empty space on the LEFT while the
RIGHT toolbar's per-page controls (selects + buttons) got clipped
because their column couldn't grow past its equal share.

Changed the template to:
  minmax(0, 1fr) auto minmax(max-content, 1fr)

- LEFT  → `minmax(0, 1fr)`: can absorb all slack OR shrink to 0.
  The chip name + breadcrumb may clip on narrow viewports.
- CENTER → `auto`: the search box at its content width, unchanged.
- RIGHT → `minmax(max-content, 1fr)`: its full natural width is the
  floor, so it never clips; beyond that it shares free space evenly
  with the left so the search stays visually viewport-centered when
  there's room.

Behaviour at three sample viewport widths (HAProxy gear, full
toolbar):

  3000px  left=1332 search=281 right=1332 → search 4px off center
  1750px  left=633  search=281 right=781  → right at max-content,
                                            left absorbs ~250px slack
  1280px  left=163  search=281 right=781  → left clips
                                            (chip+breadcrumb truncate)
                                            right stays fully visible

Refs #92
When the viewport shrinks below md, the HeaderSearch row collapses
to a 34px magnifying-glass compact button. The palette positioner
was clamping its width to the search wrap, so on narrow viewports
the slide-down panel became an unusable ~34px vertical strip with
one-letter labels.

Decouple palette width from the search wrap:
  width = clamp(360px, searchWrap.width, min(640px, viewport - 16))

Then re-anchor centered on the search input and clamp the resulting
left position so the panel stays inside the viewport with an 8px
gutter on each side.

At vw=875 the panel is 360px wide (MIN_W) and left-aligned near
the viewport edge; at vw=1500 it's its natural ~445px under the
search box; at vw≥1880 it caps at 640px MAX_W. The list rows now
read cleanly across the full responsive range.

Refs #92
Copilot AI review requested due to automatic review settings May 16, 2026 23:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements issue #92 by introducing a unified HeaderSearch input in the global header that drives (1) per-gear filtering, (2) global DuckDuckGo fallback search, and (3) a redesigned command palette that appears as a slide-down panel anchored to the header input. It also updates multiple gears to register filter + command palette entries via new window.gearbox registries, and redesigns the ? shortcut overlay to a full-window smoked-glass layout.

Changes:

  • Add global HeaderSearch controller and per-page window.gearbox.{filter,commands} registries; remove scattered per-gear filter inputs.
  • Redesign command palette to reuse the header input (palette mode via leading >), including per-gear commands registration.
  • Redesign shortcut help overlay (?) to a full-window smoked-glass reference grid and keep universal Esc handling.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
gearbox/static/js/common/header-search.js New unified header search controller: / focus, Cmd/Ctrl+K palette mode, dispatch to registered filter callbacks, mobile compact behavior.
gearbox/static/js/common/gear-commands.js New page-scoped registries for per-gear filter handlers and command palette commands (window.gearbox).
gearbox/static/js/common/command-palette.js Command palette redesigned to be a slide-down panel driven by HeaderSearch query and per-gear commands.
gearbox/static/js/common/shortcut-help.js Shortcut help overlay redesigned; continues to own universal Esc behavior.
gearbox/internal/framework/templates/layouts/base.templ Header layout reworked to full-width above sidebar; adds HeaderSearch + slide-down panel markup; script load order updated.
gearbox/static/js/gears/home.js Home tile filtering and “open first match / DDG fallback” now wired through HeaderSearch filter registration; adds palette commands.
gearbox/internal/gears/home/pages.templ Removes the Home-local search form; documents new HeaderSearch usage for Home.
gearbox/internal/gears/bx/pages.templ Removes visible Bx search input; keeps hidden input for Tabulator search binding.
gearbox/static/js/bx/bx-page.js Registers Bx filter proxying into hidden #bx-search; registers palette commands for view + add box.
gearbox/internal/framework/templates/pages/os_updates.templ Removes visible package search input; keeps hidden input for Tabulator binding.
gearbox/static/js/os-updates/os-updates-page.js Registers OS Updates filter proxying into hidden #pkg-search; registers palette commands.
gearbox/internal/framework/templates/pages/services.templ Removes page-local services text filter; replaces with HeaderSearch-driven query variable + palette commands.
gearbox/internal/framework/templates/pages/overview.templ Removes HAProxy overview text filter input; uses HeaderSearch-driven query variable + palette commands; restores saved query.
gearbox/internal/framework/templates/pages/logs.templ Removes Logs page-local text filter input; uses HeaderSearch-driven query variable; registers extensive Logs palette commands.
gearbox/internal/framework/templates/pages/metrics.templ Adds palette commands (edit toggle, refresh).
gearbox/internal/framework/templates/pages/certificates.templ Adds palette command (refresh).
gearbox/internal/framework/templates/pages/alerts.templ Removes visible alert search input; wires filter + palette commands via registrations.
gearbox/static/js/traffic/traffic-visualization.js Adds palette commands for view dropdown + actions (reset/physics/refresh).
Comments suppressed due to low confidence (2)

gearbox/static/js/common/header-search.js:349

  • In compact/mobile mode, this outside-click handler will never collapse the expanded search row because #header-search-row always retains the hidden class (CSS overrides display via .header-search-expanded). As written, row.classList.contains('hidden') stays true even when the row is visible, so clicks outside won’t call collapseCompact().

Consider checking for row.classList.contains('header-search-expanded') instead (or removing/adding hidden in JS when expanding/collapsing).

        // Compact-mode: clicking outside the search collapses the row.
        document.addEventListener('click', function (e) {
            if (!window.matchMedia('(max-width: 767px)').matches) return;
            if (!row || row.classList.contains('hidden')) return;
            if (wrap && wrap.contains(e.target)) return;
            // Don't collapse while the palette panel is the click target.
            const panel = document.getElementById('header-search-panel');
            if (panel && panel.contains(e.target)) return;
            collapseCompact();

gearbox/internal/framework/templates/layouts/base.templ:2481

  • The HeaderSearch behavior documentation here says the placeholder switches to the page-provided window.gearbox.filter.register({ placeholder }), but header-search.js currently ignores the placeholder and keeps a single shared placeholder/hint UI. This mismatch will confuse future changes.

Either update the comment to reflect the current behavior, or wire the registered placeholder into the input (and ensure it doesn’t cause layout shift).

// HeaderSearch is the unified search / filter / command-palette input.
// One text input drives three behaviours, chosen by what the user types:
//   - empty / search mode: hint reads "Type / to search · Cmd+K for palette".
//     Per-gear pages can register a filter callback via window.gearbox.filter
//     (see common/gear-commands.js); when active, the placeholder switches
//     to whatever the page asked for and each keystroke calls the callback.
//     With no filter callback registered, Enter falls back to a DuckDuckGo
//     web search.

Comment thread gearbox/static/js/common/header-search.js Outdated
Comment thread gearbox/internal/framework/templates/layouts/base.templ
Comment thread gearbox/static/js/common/shortcut-help.js
Comment thread gearbox/internal/framework/templates/pages/overview.templ Outdated
Six findings (4 inline + 2 in the review body, all valid):

1. `clear()` in header-search.js fired BOTH `filter.onInput('')` AND
   `filter.onClear()`. Most gears were re-filtering twice; any side-
   effectful onClear would run on top of the empty-input path. Pick
   one path: prefer onClear if registered, otherwise dispatch empty
   onInput. Removed the now-unused dispatchClear helper.

2. toggleSidebar() / applySidebarState() only updated the hidden
   `#sidebar-icon-{collapse,expand}` shim left behind in the sidebar
   for legacy code. The VISIBLE icons live in the new header toggle
   (`#sidebar-icon-{collapse,expand}-header`) and were left untouched,
   so clicking the toggle moved the sidebar but the icon never
   swapped. Mirror the state to both pairs.

3. The `?` shortcut overlay is `role="dialog" aria-modal="true"` but
   open() just removed `hidden` — focus stayed wherever it was, so
   the dialog wasn't announced and keyboard focus could land outside
   it. Make the overlay focusable on open and move focus into it;
   restore focus to the prior element on close. No formal focus
   trap is needed since the overlay has no interactive controls
   (clicking anywhere closes).

4. The Overview gear restored the saved text filter by writing
   __backendFilterQuery directly THEN calling HeaderSearch.setValue,
   BEFORE registering the filter — so the bridge would never carry
   restored state through the registered onInput. Swap the order:
   register first, then setValue, and let the dispatch update the
   query variable through the same code path keystrokes use.

5. The outside-click handler that collapses the compact mobile
   search row checked `row.classList.contains('hidden')` to bail
   when closed — but the row's templ markup is `hidden md:flex`, so
   it ALWAYS carries `hidden` regardless of overlay state. The check
   effectively disabled the feature. Use the .header-search-expanded
   marker class instead, which the overlay state actually toggles.

6. The HeaderSearch templ doc-comment still described the original
   "registered placeholder switches the input's placeholder" plan;
   we dropped that earlier so the box reads the same on every page.
   Updated the comment to match.

Verified in the browser: toggling the sidebar swaps the visible
header icon; clear() with both callbacks registered fires onClear
exactly once and onInput zero times; opening the help overlay moves
focus onto it.

Refs #92
@sarg3nt
sarg3nt merged commit a8a1af8 into main May 17, 2026
22 checks passed
@sarg3nt
sarg3nt deleted the feature/92-unified-search-palette branch May 28, 2026 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Command Pallet and Search Improvements

2 participants