From 3442b5fa4e534cc026892d4c07cd9fc88b7c4996 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 5 Aug 2026 00:03:33 +0200 Subject: [PATCH] refactor: open search from titlebar command --- README.md | 9 ++- docs/INTERFACE_PATTERN_MIGRATION.md | 2 +- src/govoplan_search/backend/manifest.py | 12 +++- .../scripts/test-search-overlay-structure.mjs | 12 ++-- webui/src/components/GlobalSearch.tsx | 58 +++++-------------- webui/src/components/searchOverlayLayout.ts | 17 +----- webui/src/styles/search.css | 19 +----- 7 files changed, 47 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 000e6ed..1807747 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,14 @@ **Repository type:** module (platform). -Permission-aware global and contextual search for GovOPlaN. +Permission-aware global and contextual search for GovOPlaN. Search is the first +command in the titlebar action group. Clicking its icon, pressing `F3`, or +pressing `Ctrl`/`Cmd`+`K` opens the same full query field and result overlay. + +The titlebar command, result overlay, filters, and Search administration route +announce stable help contexts. Pressing `F1` while one of those controls is +focused opens its Search documentation, with the current page retained as a +fallback. The route, overlay, state, accessibility, and consequence mapping is recorded in [`docs/INTERFACE_PATTERN_MIGRATION.md`](docs/INTERFACE_PATTERN_MIGRATION.md). diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md index 13b7aad..235071b 100644 --- a/docs/INTERFACE_PATTERN_MIGRATION.md +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -6,7 +6,7 @@ authorization, and optional external engines remain provider capabilities. | Surface | Task and archetype | Consequence and state contract | | --- | --- | --- | -| Title-bar search and anchored overlay | Global or context-sensitive focused lookup | F3 and Ctrl/Cmd+K open the same focus-contained Core dialog. Arrow keys move through the listbox, Enter opens the selected result, Escape closes it and restores focus. | +| Title-bar Search command and overlay | Global or context-sensitive focused lookup | The left-most titlebar command, F3, and Ctrl/Cmd+K open the same focus-contained Core dialog with a full-width query field. Arrow keys move through the listbox, Enter opens the selected result, Escape closes it and restores focus. | | Overlay filters | Progressive-disclosure filter popover | Module and resource filters only narrow authorized results. Active filters stay visible and removable by keyboard. | | `/search` | Full-page search/results fallback | Query and filters are URL-stable. Loading, empty, provider-partial, failed, and paged states remain inside the result region. | | Result entries | Permission-filtered list-detail destinations | A source module supplies the title, safe summary, breadcrumbs, and destination. Search does not infer or bypass source authorization. | diff --git a/src/govoplan_search/backend/manifest.py b/src/govoplan_search/backend/manifest.py index 88af05a..9650578 100644 --- a/src/govoplan_search/backend/manifest.py +++ b/src/govoplan_search/backend/manifest.py @@ -209,7 +209,8 @@ manifest = ModuleManifest( "Search works with the built-in database index and can aggregate " "optional providers. Source modules announce searchable types, " "context scopes, and ACL-aware index entries. External engines " - "remain optional adapters. F3 or the title-bar field opens the " + "remain optional adapters. The title-bar Search command, F3, " + "or Ctrl/Cmd+K opens the " "keyboard-navigable search overlay; filters never broaden the " "current principal's source permissions. Provider failures are " "shown as partial diagnostics without discarding safe results." @@ -222,6 +223,15 @@ manifest = ModuleManifest( documentation_types=("admin", "user"), audience=("administrator", "user"), related_modules=("connectors", "views"), + metadata={ + "kind": "reference", + "help_contexts": [ + "search.global", + "search.results", + "search.filters", + "search.admin.index", + ], + }, links=( DocumentationLink( label="Search interface pattern audit", diff --git a/webui/scripts/test-search-overlay-structure.mjs b/webui/scripts/test-search-overlay-structure.mjs index d416a15..24f46cc 100644 --- a/webui/scripts/test-search-overlay-structure.mjs +++ b/webui/scripts/test-search-overlay-structure.mjs @@ -9,18 +9,20 @@ const source = readFileSync("src/components/GlobalSearch.tsx", "utf8"); const layoutSource = readFileSync("src/components/searchOverlayLayout.ts", "utf8"); const styles = readFileSync("src/styles/search.css", "utf8"); -assert(source.includes("onFocus={handleSourceFocus}"), "focusing the titlebar field opens Search"); -assert(source.includes("suppressRestoredFocusRef.current"), "restored dialog focus does not immediately reopen Search"); +assert(source.includes("titlebar-icon-link titlebar-search-button"), "Search uses the shared titlebar icon-button appearance"); +assert(source.includes("onClick={openOverlay}"), "clicking the titlebar Search command opens Search"); +assert(!source.includes("sourceInputRef"), "the titlebar no longer reserves a persistent Search field"); assert(source.includes("("search.contexts")'), "contextual Search contributions are consumed"); assert(!source.includes("navigate(`/search"), "normal Search interaction no longer opens a page route"); -assert(layoutSource.includes("anchor.left - left"), "desktop input placement is derived from the original field"); -assert(styles.includes(".global-search-source.is-overlay-open"), "the original field is hidden while its overlay counterpart is active"); +assert(layoutSource.includes("const inputWidth = width"), "the opened query field spans the Search overlay"); +assert(layoutSource.includes("(viewportWidth - width) / 2"), "the Search overlay is centered in the viewport"); +assert(styles.includes("margin-top: 8px"), "results follow the opened query field without overlap"); assert(styles.includes(".search-overlay-results-panel"), "full Search results have a bounded overlay panel"); console.log("Search overlay structure checks passed."); diff --git a/webui/src/components/GlobalSearch.tsx b/webui/src/components/GlobalSearch.tsx index 5e8cf67..1dbe325 100644 --- a/webui/src/components/GlobalSearch.tsx +++ b/webui/src/components/GlobalSearch.tsx @@ -73,14 +73,12 @@ export default function GlobalSearch({ settings }: GlobalSearchProps) { const [filtersOpen, setFiltersOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); - const rootRef = useRef(null); - const sourceInputRef = useRef(null); + const rootRef = useRef(null); const overlayInputRef = useRef(null); const filtersRef = useRef(null); const resultsRef = useRef(null); const requestSequenceRef = useRef(0); const loadMoreControllerRef = useRef(null); - const suppressRestoredFocusRef = useRef(false); const effectiveModules = useMemo( () => scope === "context" && currentContext @@ -152,7 +150,6 @@ export default function GlobalSearch({ settings }: GlobalSearchProps) { const closeOverlay = useCallback(() => { loadMoreControllerRef.current?.abort(); - suppressRestoredFocusRef.current = true; setOpen(false); setFiltersOpen(false); setActiveIndex(-1); @@ -163,14 +160,6 @@ export default function GlobalSearch({ settings }: GlobalSearchProps) { setOpen(true); }, [measureOverlay]); - const handleSourceFocus = useCallback(() => { - if (suppressRestoredFocusRef.current) { - suppressRestoredFocusRef.current = false; - return; - } - openOverlay(); - }, [openOverlay]); - useEffect(() => { function focusSearch(event: KeyboardEvent) { const commandSearch = @@ -395,41 +384,26 @@ export default function GlobalSearch({ settings }: GlobalSearchProps) { return ( <> -
-
+ type="button" + data-help-context-id="search.global" + data-help-module-id="search" + data-help-scope="action" + className={`titlebar-icon-link titlebar-search-button${open ? " is-context-active" : ""}`} + title="Search (F3 / Ctrl+K)" + aria-label="Global search" + aria-keyshortcuts="F3 Control+K Meta+K" + aria-haspopup="dialog" + aria-expanded={open} + onClick={openOverlay}> +