Skip to content

Commit a8a1af8

Browse files
authored
feat(#92): unify search, filter, and command palette in the header (#109)
* feat(#92): unified HeaderSearch + slide-down command palette 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 * feat(#92): migrate every gear's filter + toolbar to HeaderSearch/palette 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 * fix(#92): HeaderSearch — drop gear placeholder, lock box width, remove 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 * fix(#92): HeaderSearch — content-sized box, stable across modes, responsive 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 * fix(#92): HeaderSearch — hint chips overlay the input, not a separate 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 * fix(#92): header grid — right column gets priority over left's slack 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 * fix(#92): command palette — independent width on narrow viewports 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 * address Copilot review on #109 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
1 parent 5837316 commit a8a1af8

18 files changed

Lines changed: 1738 additions & 667 deletions

File tree

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

Lines changed: 317 additions & 137 deletions
Large diffs are not rendered by default.

gearbox/internal/framework/templates/pages/alerts.templ

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,9 @@ templ AlertsPage(user *models.User, servers []models.BoxConfig, perms *models.Us
1212
<div id="page-header-source" class="hidden">
1313
<div class="flex-1"></div>
1414
<div class="flex items-center space-x-3">
15-
<!-- Search input -->
16-
<input
17-
type="text"
18-
id="alert-search"
19-
placeholder="Search alerts..."
20-
oninput="filterAlerts(this.value)"
21-
class="w-48 h-[38px] px-3 py-1.5 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
22-
/>
15+
<!-- Search input moved to the global HeaderSearch
16+
(issue #92). filterAlerts() is wired through a
17+
window.gearbox.filter.register call by alerts.js. -->
2318
<!-- Status filter -->
2419
<select
2520
id="status-filter"
@@ -354,6 +349,39 @@ templ AlertsPage(user *models.User, servers []models.BoxConfig, perms *models.Us
354349
refreshAlertsData();
355350
initSSE();
356351
}
352+
353+
// HeaderSearch + command palette wiring (issue #92).
354+
if (window.gearbox && window.gearbox.filter) {
355+
window.gearbox.filter.register({
356+
placeholder: 'Search alerts…',
357+
onInput: function (q) { if (typeof filterAlerts === 'function') filterAlerts(q || ''); },
358+
onClear: function () { if (typeof filterAlerts === 'function') filterAlerts(''); },
359+
});
360+
}
361+
if (window.gearbox && window.gearbox.commands) {
362+
const cmds = window.gearbox.commands;
363+
const statusSel = document.getElementById('status-filter');
364+
if (statusSel) {
365+
Array.from(statusSel.options).forEach(function (opt) {
366+
cmds.register({
367+
id: 'alerts.status.' + opt.value,
368+
label: 'Status: ' + opt.text,
369+
subtitle: 'Show ' + opt.text.toLowerCase(),
370+
group: 'Alerts',
371+
run: function () {
372+
statusSel.value = opt.value;
373+
if (typeof changeStatusFilter === 'function') changeStatusFilter(opt.value);
374+
},
375+
});
376+
});
377+
}
378+
cmds.register({
379+
id: 'alerts.refresh',
380+
label: 'Refresh alerts',
381+
group: 'Alerts',
382+
run: function () { if (typeof refreshAlertsData === 'function') refreshAlertsData(); },
383+
});
384+
}
357385
});
358386

359387
// Clean up SSE on page unload

gearbox/internal/framework/templates/pages/certificates.templ

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,16 @@ templ Certificates(user *models.User, servers []models.BoxConfig, perms *models.
532532
setupPageHeader();
533533
loadCertificates();
534534
initSSE();
535+
536+
// Command palette wiring (issue #92).
537+
if (window.gearbox && window.gearbox.commands) {
538+
window.gearbox.commands.register({
539+
id: 'certificates.refresh',
540+
label: 'Refresh certificate list',
541+
group: 'Certificates',
542+
run: function () { if (typeof loadCertificates === 'function') loadCertificates(); },
543+
});
544+
}
535545
});
536546

537547
// Cleanup on page unload

gearbox/internal/framework/templates/pages/logs.templ

Lines changed: 107 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ templ Logs(user *models.User, servers []models.BoxConfig) {
1111

1212
<!-- Hidden container for page header content - will be moved to main header via JS.
1313
Title + box selector are now in the global header breadcrumb +
14-
chip; this slot only carries page-specific controls. -->
14+
chip; this slot only carries page-specific controls.
15+
16+
The filter input itself has moved to the global HeaderSearch
17+
(issue #92); this template still renders the source / lines /
18+
severity selects and the action buttons. Logs registers its
19+
own filter + commands via window.gearbox in the page script
20+
below so the search bar drives filterLogs() and the palette
21+
drives every other toolbar action. -->
1522
<div id="page-header-source" class="hidden">
1623
<!-- Right side spacer pushes controls to the far right of the header -->
1724
<div class="flex-1"></div>
@@ -39,26 +46,8 @@ templ Logs(user *models.User, servers []models.BoxConfig) {
3946
</select>
4047
}
4148

42-
<!-- Filter input and severity -->
43-
<div class="relative">
44-
<input
45-
type="text"
46-
id="filter-input"
47-
placeholder="Filter..."
48-
class="h-[38px] w-32 px-3 py-1.5 pr-8 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
49-
oninput="filterLogs()"
50-
/>
51-
<button
52-
type="button"
53-
id="filter-clear"
54-
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hidden"
55-
onclick="clearLogFilter()"
56-
>
57-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
58-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
59-
</svg>
60-
</button>
61-
</div>
49+
<!-- Severity select (kept here; filterLogs() also reads it).
50+
The text filter input moved to the global HeaderSearch. -->
6251
<select
6352
id="severity-filter"
6453
class="h-[38px] px-3 py-1.5 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
@@ -457,10 +446,14 @@ templ Logs(user *models.User, servers []models.BoxConfig) {
457446
}
458447
}
459448

460-
// Filter a single line element
449+
// Filter state. The text filter used to live in a #filter-input
450+
// element on this page; it's now driven by the global
451+
// HeaderSearch (issue #92). __logFilterQuery is updated by the
452+
// gear-registered filter callback in DOMContentLoaded below, and
453+
// filterLogs() / filterSingleLine() read from it instead.
454+
let __logFilterQuery = '';
461455
function filterSingleLine(lineEl) {
462-
const input = document.getElementById('filter-input');
463-
const filter = input ? input.value.toLowerCase() : '';
456+
const filter = __logFilterQuery;
464457
const severityFilter = document.getElementById('severity-filter');
465458
const severityValue = severityFilter ? severityFilter.value : 'all';
466459

@@ -752,19 +745,14 @@ templ Logs(user *models.User, servers []models.BoxConfig) {
752745
}
753746

754747
function filterLogs() {
755-
const input = document.getElementById('filter-input');
756-
const filter = input.value.toLowerCase();
748+
// __logFilterQuery is maintained by the HeaderSearch
749+
// filter registration in DOMContentLoaded (lower-cased on
750+
// write). filterSingleLine() reads both that and the
751+
// severity dropdown.
752+
const filter = __logFilterQuery;
757753
const severityFilter = document.getElementById('severity-filter').value;
758754
const container = document.getElementById('log-container');
759755
const lines = container.getElementsByClassName('log-line');
760-
const clearBtn = document.getElementById('filter-clear');
761-
762-
// Show/hide clear button
763-
if (filter.length > 0) {
764-
clearBtn.classList.remove('hidden');
765-
} else {
766-
clearBtn.classList.add('hidden');
767-
}
768756

769757
for (let line of lines) {
770758
const textMatch = filter === '' || line.textContent.toLowerCase().includes(filter);
@@ -786,11 +774,11 @@ templ Logs(user *models.User, servers []models.BoxConfig) {
786774
}
787775

788776
function clearLogFilter() {
789-
const input = document.getElementById('filter-input');
790-
input.value = '';
791-
document.getElementById('filter-clear').classList.add('hidden');
777+
// Programmatic-clear path. The HeaderSearch's own clear
778+
// button calls this through the filter registration's
779+
// onClear handler.
780+
__logFilterQuery = '';
792781
filterLogs();
793-
input.focus();
794782
}
795783

796784
function escapeHtml(text) {
@@ -1053,6 +1041,87 @@ templ Logs(user *models.User, servers []models.BoxConfig) {
10531041

10541042
// Initialize SSE for real-time log streaming
10551043
initSSE();
1044+
1045+
// Register with the unified HeaderSearch + command palette
1046+
// (issue #92). Each keystroke in search mode updates the
1047+
// internal __logFilterQuery and re-runs filterLogs(); the
1048+
// command palette gets entries for every toolbar button so
1049+
// they're all reachable via Cmd+K → ">" → fuzzy search.
1050+
if (window.gearbox && window.gearbox.filter) {
1051+
window.gearbox.filter.register({
1052+
placeholder: 'Filter log lines…',
1053+
onInput: function (q) {
1054+
__logFilterQuery = (q || '').toLowerCase();
1055+
filterLogs();
1056+
},
1057+
onClear: function () {
1058+
__logFilterQuery = '';
1059+
filterLogs();
1060+
},
1061+
});
1062+
}
1063+
if (window.gearbox && window.gearbox.commands) {
1064+
const cmds = window.gearbox.commands;
1065+
// Log source — one entry per server-side option. If
1066+
// the user adds a third log type in the future this
1067+
// loop picks it up automatically.
1068+
const logSel = document.getElementById('log-select');
1069+
if (logSel) {
1070+
Array.from(logSel.options).forEach(function (opt) {
1071+
cmds.register({
1072+
id: 'logs.source.' + opt.value,
1073+
label: 'Log source: ' + opt.text,
1074+
subtitle: 'Switch the log feed to ' + opt.text.toLowerCase(),
1075+
group: 'Logs',
1076+
run: function () {
1077+
logSel.value = opt.value;
1078+
if (typeof onLogSourceChange === 'function') onLogSourceChange();
1079+
},
1080+
});
1081+
});
1082+
}
1083+
// Line counts — exposed as palette entries so the user
1084+
// can keyboard-pick "200" without opening the dropdown.
1085+
const linesSel = document.getElementById('lines-select');
1086+
if (linesSel) {
1087+
Array.from(linesSel.options).forEach(function (opt) {
1088+
cmds.register({
1089+
id: 'logs.lines.' + opt.value,
1090+
label: 'Show ' + opt.text + ' lines',
1091+
subtitle: 'Fetch the last ' + opt.text + ' log lines',
1092+
group: 'Logs',
1093+
run: function () {
1094+
linesSel.value = opt.value;
1095+
if (typeof loadLogs === 'function') loadLogs();
1096+
},
1097+
});
1098+
});
1099+
}
1100+
// Severity — same pattern.
1101+
const sevSel = document.getElementById('severity-filter');
1102+
if (sevSel) {
1103+
Array.from(sevSel.options).forEach(function (opt) {
1104+
cmds.register({
1105+
id: 'logs.severity.' + opt.value,
1106+
label: 'Severity: ' + opt.text,
1107+
subtitle: 'Show only ' + opt.text.toLowerCase() + ' lines',
1108+
group: 'Logs',
1109+
run: function () {
1110+
sevSel.value = opt.value;
1111+
if (typeof filterLogs === 'function') filterLogs();
1112+
},
1113+
});
1114+
});
1115+
}
1116+
cmds.register({ id: 'logs.refresh', label: 'Refresh logs', group: 'Logs',
1117+
subtitle: 'Re-fetch the log feed', run: function () { if (typeof manualRefresh === 'function') manualRefresh(); }});
1118+
cmds.register({ id: 'logs.copy', label: 'Copy logs', group: 'Logs',
1119+
subtitle: 'Copy currently-visible lines to the clipboard', run: function () { if (typeof copyLogs === 'function') copyLogs(); }});
1120+
cmds.register({ id: 'logs.download', label: 'Download logs', group: 'Logs',
1121+
subtitle: 'Save visible lines as a .log file', run: function () { if (typeof downloadLogs === 'function') downloadLogs(); }});
1122+
cmds.register({ id: 'logs.fullscreen', label: 'Toggle fullscreen', group: 'Logs',
1123+
subtitle: 'Cycle: normal → browser fullscreen → true fullscreen', run: function () { if (typeof toggleFullscreen === 'function') toggleFullscreen(); }});
1124+
}
10561125
});
10571126

10581127
// Clean up SSE on page unload

gearbox/internal/framework/templates/pages/metrics.templ

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2822,6 +2822,27 @@ templ Metrics(user *models.User, servers []models.BoxConfig) {
28222822
if (slider) {
28232823
slider.addEventListener('input', updateResolutionLabel);
28242824
}
2825+
2826+
// Command palette wiring (issue #92).
2827+
if (window.gearbox && window.gearbox.commands) {
2828+
const cmds = window.gearbox.commands;
2829+
cmds.register({
2830+
id: 'metrics.toggle-edit',
2831+
label: 'Toggle edit mode',
2832+
subtitle: 'Enter drag-to-arrange + resize mode for chart tiles',
2833+
group: 'Metrics',
2834+
run: function () {
2835+
const btn = document.getElementById('metrics-edit-toggle');
2836+
if (btn) btn.click();
2837+
},
2838+
});
2839+
cmds.register({
2840+
id: 'metrics.refresh',
2841+
label: 'Refresh metrics',
2842+
group: 'Metrics',
2843+
run: function () { if (typeof loadHistoryData === 'function') loadHistoryData(); },
2844+
});
2845+
}
28252846
});
28262847

28272848
// Cleanup on page unload

gearbox/internal/framework/templates/pages/os_updates.templ

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -200,18 +200,15 @@ templ OSUpdatesPage(data OSUpdatesPageData) {
200200
</button>
201201
}
202202
}
203-
<span class="datagrid-search-wrap">
204-
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
205-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35M11 19a8 8 0 100-16 8 8 0 000 16z"></path>
206-
</svg>
207-
<input
208-
type="search"
209-
id="pkg-search"
210-
placeholder="Search packages..."
211-
class="h-[38px] pr-3 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 w-56"
212-
autocomplete="off"
213-
/>
214-
</span>
203+
<!-- #pkg-search moved to the global HeaderSearch
204+
(issue #92). The element is kept here as a
205+
hidden input so the Tabulator-backed
206+
datagrid (createDataGrid({searchInput:
207+
'#pkg-search'})) can still latch onto it;
208+
a window.gearbox.filter registration in
209+
os-updates-page.js writes to .value and
210+
dispatches 'input' on every keystroke. -->
211+
<input type="search" id="pkg-search" class="hidden" autocomplete="off"/>
215212
<select
216213
id="pkg-view-filter"
217214
onchange="onPkgViewFilterChange(this.value)"

0 commit comments

Comments
 (0)