diff --git a/i18n/default_de.json b/i18n/default_de.json
index b358daf..76f5073 100644
--- a/i18n/default_de.json
+++ b/i18n/default_de.json
@@ -376,6 +376,11 @@
"Search...": "Suchen...",
"To start searching, type in the search field.": "Um die Suche zu starten, gib etwas in das Suchfeld ein.",
"Tip: Use {shortcut} to focus search quickly.": "Tipp: Mit {shortcut} fokussierst du die Suche schnell.",
+ "Searching...": "Suche läuft...",
+ "Search failed. Please try again.": "Suche fehlgeschlagen. Bitte erneut versuchen.",
+ "No results found": "Keine Ergebnisse gefunden",
+ "View all results": "Alle Ergebnisse anzeigen",
+ "to close": "zum Schließen",
"Created from": "Erstellt von",
"Created to": "Erstellt bis",
"Files": "Dateien",
diff --git a/i18n/default_en.json b/i18n/default_en.json
index 2881ffd..d2487b1 100644
--- a/i18n/default_en.json
+++ b/i18n/default_en.json
@@ -376,6 +376,11 @@
"Search...": "Search...",
"To start searching, type in the search field.": "To start searching, type in the search field.",
"Tip: Use {shortcut} to focus search quickly.": "Tip: Use {shortcut} to focus search quickly.",
+ "Searching...": "Searching...",
+ "Search failed. Please try again.": "Search failed. Please try again.",
+ "No results found": "No results found",
+ "View all results": "View all results",
+ "to close": "to close",
"Created from": "Created from",
"Created to": "Created to",
"Files": "Files",
diff --git a/templates/default.phtml b/templates/default.phtml
index b934dce..b38d631 100644
--- a/templates/default.phtml
+++ b/templates/default.phtml
@@ -211,8 +211,6 @@ $componentPageConfig = [
'tenantSsoToggle' => [],
'standardDetailPage' => [],
'globalSearch' => [
- 'inputSelector' => '#side-search',
- 'resultsSelector' => '[data-global-search-results]',
'appBase' => localeBase(),
],
'tabs' => [
@@ -339,6 +337,7 @@ $componentPageConfig['moduleRuntimeComponents'] = $moduleRuntimeComponents;
+
diff --git a/templates/partials/app-main-aside-icon-bar.phtml b/templates/partials/app-main-aside-icon-bar.phtml
index 5309605..c0abd43 100644
--- a/templates/partials/app-main-aside-icon-bar.phtml
+++ b/templates/partials/app-main-aside-icon-bar.phtml
@@ -15,13 +15,6 @@ $hasAdminPanel = layoutHasAdminPanel($layoutAuth);
-
-
-
diff --git a/templates/partials/app-search-dialog.phtml b/templates/partials/app-search-dialog.phtml
new file mode 100644
index 0000000..dab6d7e
--- /dev/null
+++ b/templates/partials/app-search-dialog.phtml
@@ -0,0 +1,66 @@
+
+
diff --git a/templates/partials/app-topbar.phtml b/templates/partials/app-topbar.phtml
index df497f4..6f1df46 100644
--- a/templates/partials/app-topbar.phtml
+++ b/templates/partials/app-topbar.phtml
@@ -38,7 +38,13 @@ $moduleTopbarSlots = is_array($moduleSlots['topbar.right_item'] ?? null) ? $modu
-
+
+
+
-
diff --git a/tests/Architecture/FrontendRuntimeHostContractTest.php b/tests/Architecture/FrontendRuntimeHostContractTest.php
index 69660f9..3658e11 100644
--- a/tests/Architecture/FrontendRuntimeHostContractTest.php
+++ b/tests/Architecture/FrontendRuntimeHostContractTest.php
@@ -32,7 +32,8 @@ class FrontendRuntimeHostContractTest extends TestCase
$this->assertStringContainsString('data-app-component="details-aside-toggle"', $topbarPartial);
$this->assertStringContainsString('data-app-component="aside-panels"', $asideIconBar);
$this->assertStringContainsString('data-app-component="sidebar-toggle"', $asidePartial);
- $this->assertStringContainsString('data-app-component="global-search"', $asidePartial);
+ $searchDialog = $this->readProjectFile('templates/partials/app-search-dialog.phtml');
+ $this->assertStringContainsString('data-app-component="global-search"', $searchDialog);
$this->assertStringContainsString('data-app-component="bookmark-panel"', $bookmarkPanelTemplate);
$this->assertStringContainsString('data-app-component="bookmark-save"', $bookmarkDialog);
$this->assertStringContainsString('data-app-component="session-warning"', $sessionWarningDialog);
diff --git a/web/css/components/app-search.css b/web/css/components/app-search.css
index 8fc4cf6..58f8018 100644
--- a/web/css/components/app-search.css
+++ b/web/css/components/app-search.css
@@ -1,64 +1,151 @@
@layer components {
- /* Global sidebar search — input, result list, preview rows, and search result cards. */
-.app-search {
- position: relative;
- padding: 0 var(--app-spacing);
- margin-bottom: var(--app-spacing);
-}
+ /*
+ * Search command-palette dialog.
+ *
+ * Builds on the core dialog base (app-shell.css → dialog, dialog > article)
+ * and the shared tier system (app-dialog.css → app-dialog-lg).
+ * Only search-specific overrides live here.
+ */
-.app-search-shortcut {
- position: absolute;
- right: calc(var(--app-spacing) + 0.5rem);
- top: 50%;
- transform: translateY(-50%);
- border: 1px solid var(--app-border);
- border-radius: 6px;
- background: var(--app-card-background-color);
- color: var(--app-muted-color);
- font-size: var(--text-xs);
- line-height: var(--leading-none);
- padding: 0.2rem 0.4rem;
- pointer-events: none;
-}
+ /* ── Dialog positioning: top-aligned instead of centered ─────────────── */
-.app-search-empty-state {
- margin: var(--app-spacing);
- padding: 1rem;
- border: 1px dashed var(--app-border);
- border-radius: var(--app-border-radius);
- text-align: center;
- color: var(--app-muted-color);
- font-size: var(--text-sm);
- cursor: text;
-}
+ dialog[data-app-search-dialog] {
+ align-items: flex-start;
+ padding-top: clamp(2rem, 10vh, 8rem);
+ }
-.app-search-empty-state .bi-search {
- display: inline-block;
- font-size: var(--text-xl);
- margin-bottom: 0.5rem;
- color: var(--app-color);
-}
+ /* ── Article: flex column so body scrolls, header/footer stay fixed ──── */
-.app-search-empty-state p {
- margin: 0 0 0.25rem;
-}
+ .app-search-dialog {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
-.app-search-empty-state small {
- display: block;
-}
+ /* ── Header: search input row ────────────────────────────────────────── */
-ul.app-search-results {
- padding: 0;
- margin: 0;
-}
+ .app-search-dialog-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
-ul.app-search-results > li > a {
- justify-content: space-between;
-}
+ .app-search-dialog-header h2 {
+ margin: 0;
+ }
-ul.app-search-results .badge {
+ .app-search-dialog-form {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex: 1;
+ margin: 0;
+ }
+
+ .app-search-dialog-icon {
+ color: var(--app-muted-color);
+ font-size: var(--text-base);
+ flex-shrink: 0;
+ }
+
+ .app-search-dialog-input {
+ flex: 1;
+ border: none;
+ background: transparent;
+ font-size: var(--text-base);
+ color: var(--app-color);
+ outline: none;
+ padding: 0.25rem 0;
+ margin: 0;
+ height: auto;
+ box-shadow: none;
+ }
+
+ .app-search-dialog-input:focus {
+ box-shadow: none;
+ border-color: transparent;
+ }
+
+ .app-search-dialog-shortcut {
+ border: 1px solid var(--app-border);
+ border-radius: 4px;
+ background: var(--app-card-background-color);
+ color: var(--app-muted-color);
+ font-size: var(--text-xs);
+ line-height: var(--leading-none);
+ padding: 0.15rem 0.35rem;
+ pointer-events: none;
+ flex-shrink: 0;
+ }
+
+ .app-search-dialog-header .close {
+ flex-shrink: 0;
+ }
+
+ /* ── Body: scrollable area between header and footer ─────────────────── */
+
+ .app-search-dialog-body {
+ flex: 1;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ }
+
+ /* ── Empty / loading / error states ──────────────────────────────────── */
+
+ .app-search-dialog-empty-state,
+ .app-search-dialog-loading,
+ .app-search-dialog-error {
+ padding: 1.5rem 1rem;
+ text-align: center;
+ color: var(--app-muted-color);
+ font-size: var(--text-sm);
+ }
+
+ .app-search-dialog-empty-state p,
+ .app-search-dialog-loading p,
+ .app-search-dialog-error p {
+ margin: 0 0 0.25rem;
+ }
+
+ .app-search-dialog-empty-state small {
+ display: block;
+ }
+
+ /* ── Results list ────────────────────────────────────────────────────── */
+
+ .app-search-dialog-results {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ /* ── Category group ──────────────────────────────────────────────────── */
+
+ .app-search-dialog-category {
+ list-style: none;
+ margin-bottom: 1rem;
+ }
+
+ .app-search-dialog-category-heading {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: var(--text-xs);
+ font-weight: var(--font-semibold);
+ color: var(--app-muted-color);
+ text-transform: uppercase;
+ letter-spacing: var(--tracking-wide);
+ margin-bottom: 5px;
+ }
+
+ .app-search-dialog-category-heading .bi {
+ font-size: var(--text-sm);
+ }
+
+ .app-search-dialog-category-count {
+ margin-left: auto;
font-size: 10px; /* icon-font metric — intentional px */
- padding: 1px;
+ padding: 1px 4px;
min-width: 18px;
text-align: center;
border-radius: 999px;
@@ -67,84 +154,100 @@ ul.app-search-results .badge {
justify-content: center;
align-items: center;
height: 18px;
- position: relative;
- top: -1px;
-}
+ }
-ul.app-search-preview {
- position: relative;
- font-size: var(--text-xs);
- padding: 0 0 0 0;
- margin: 0;
- opacity: 0.75;
+ /* ── Individual result items ─────────────────────────────────────────── */
+
+ .app-search-dialog-category-items {
list-style: none;
-}
+ padding: 0;
+ margin: 0;
+ flex-direction: column;
+ gap: 5px;
+ }
-/* Hide preview rows (detail results) */
-.search-details-hidden ul.app-search-preview {
- display: none;
-}
+ .app-search-dialog-item {
+ border-radius: var(--app-border-radius);
+ transition: background var(--app-transition);
+ cursor: pointer;
+ list-style: none;
+ }
-ul.app-search-preview li {
- position: relative;
- padding: 0;
-}
-
-ul.app-search-preview li::before {
- content: "";
- position: absolute;
- left: 17px;
- bottom: 11px;
- width: 8px;
- height: 10px;
- background: transparent;
- border-left: 1px solid var(--app-border);
- border-bottom: 1px solid var(--app-border);
- border-bottom-left-radius: 6px;
-}
-
-ul.app-search-preview a {
- padding-inline: 30px;
+ .app-search-dialog-item a {
+ display: block;
+ color: var(--app-color);
+ text-decoration: none;
+ font-size: var(--text-sm);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- display: block;
- border: none;
-}
+ border-radius: var(--app-border-radius);
+ }
-ul.app-search-preview a:hover {
- border: none;
-}
+ .app-search-dialog-item:hover,
+ .app-search-dialog-item-active {
+ background: color-mix(in srgb, var(--app-contrast) 8%, transparent);
+ }
-input#side-search {
- --app-form-element-spacing-horizontal: 10px;
- --app-form-element-spacing-vertical: 6px;
- height: auto;
- font-size: var(--text-sm);
- padding-right: 3rem;
- margin-bottom: 0;
- border-radius: var(--border-radius);
-}
+ .app-search-dialog-item-active a {
+ color: var(--app-primary);
+ }
-ul.app-search-preview:has(li) {
- margin-bottom: 1rem!important;
-}
+ /* ── Category "view all" link ────────────────────────────────────────── */
-.search-result-row {
+ .app-search-dialog-category-view-all {
+ font-size: var(--text-xs);
+ color: var(--app-primary);
+ text-decoration: none;
+ }
+
+ .app-search-dialog-category-view-all:hover {
+ text-decoration: underline;
+ }
+
+ /* ── Footer ──────────────────────────────────────────────────────────── */
+
+ .app-search-dialog-footer {
+ justify-content: space-between;
+ font-size: var(--text-xs);
+ color: var(--app-muted-color);
+ }
+
+ .app-search-dialog-view-all {
+ color: var(--app-primary);
+ text-decoration: none;
+ font-weight: var(--font-medium);
+ }
+
+ .app-search-dialog-view-all:hover {
+ text-decoration: underline;
+ }
+
+ /* ── Mobile ──────────────────────────────────────────────────────────── */
+
+ @media (max-width: 576px) {
+ dialog[data-app-search-dialog] {
+ padding-top: 1rem;
+ }
+ }
+
+ /* ── Full search results page (Grid.js) ──────────────────────────────── */
+
+ .search-result-row {
display: flex;
gap: 12px;
align-items: flex-start;
-}
+ }
-.search-result-title-container {
+ .search-result-title-container {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
-}
+ }
-.search-result-avatar,
-.search-result-avatar-placeholder {
+ .search-result-avatar,
+ .search-result-avatar-placeholder {
width: 40px;
height: 40px;
border-radius: 10px;
@@ -156,23 +259,21 @@ ul.app-search-preview:has(li) {
color: var(--app-muted-color);
flex-shrink: 0;
object-fit: cover;
-}
+ }
-
-
-.search-result-title {
+ .search-result-title {
font-weight: var(--font-semibold);
text-decoration: none;
-}
+ }
-.search-result-desc {
+ .search-result-desc {
margin: 0;
color: var(--app-muted-color);
font-size: var(--text-sm);
-}
+ }
-.search-result-type {
+ .search-result-type {
color: var(--app-muted-color);
font-weight: var(--font-semibold);
-}
+ }
}
diff --git a/web/css/layout/app-topbar.css b/web/css/layout/app-topbar.css
index a7de63f..fb2956e 100644
--- a/web/css/layout/app-topbar.css
+++ b/web/css/layout/app-topbar.css
@@ -1,274 +1,342 @@
@layer layout {
-.app-skip-link {
- position: fixed;
- top: calc(var(--app-spacing) * 0.5);
- left: var(--app-spacing);
- padding: calc(var(--app-spacing) * 0.5) var(--app-spacing);
- border: 1px solid var(--app-primary);
- border-radius: var(--app-border-radius);
- background: var(--app-background-color);
- color: var(--app-primary);
- text-decoration: none;
- transform: translateY(-180%);
- transition: transform var(--app-transition);
- z-index: 40;
-}
+ .app-skip-link {
+ position: fixed;
+ top: calc(var(--app-spacing) * 0.5);
+ left: var(--app-spacing);
+ padding: calc(var(--app-spacing) * 0.5) var(--app-spacing);
+ border: 1px solid var(--app-primary);
+ border-radius: var(--app-border-radius);
+ background: var(--app-background-color);
+ color: var(--app-primary);
+ text-decoration: none;
+ transform: translateY(-180%);
+ transition: transform var(--app-transition);
+ z-index: 40;
+ }
-.app-skip-link:focus,
-.app-skip-link:focus-visible {
- transform: translateY(0);
-}
+ .app-skip-link:focus,
+ .app-skip-link:focus-visible {
+ transform: translateY(0);
+ }
-.app-header {
- --app-topbar-item-padding: 8px;
- --app-topbar-list-gap: 8px;
- --app-topbar-divider: 1px solid var(--app-border);
- --app-topbar-margin-bottom: calc(var(--app-spacing) * 2);
- --app-topbar-muted: var(--app-form-element-placeholder-color);
- --app-topbar-bg: var(--app-background-color);
- padding-block-start: 0;
- padding-left: var(--app-spacing);
- padding-right: var(--app-spacing);
- margin-bottom: var(--app-topbar-margin-bottom);
- color: var(--app-contrast);
- border-bottom: var(--app-topbar-divider);
-}
+ .app-header {
+ --app-topbar-item-padding: 8px;
+ --app-topbar-list-gap: 8px;
+ --app-topbar-divider: 1px solid var(--app-border);
+ --app-topbar-margin-bottom: calc(var(--app-spacing) * 2);
+ --app-topbar-muted: var(--app-form-element-placeholder-color);
+ --app-topbar-bg: var(--app-background-color);
+ padding-block-start: 0;
+ padding-left: var(--app-spacing);
+ padding-right: var(--app-spacing);
+ margin-bottom: var(--app-topbar-margin-bottom);
+ color: var(--app-contrast);
+ border-bottom: var(--app-topbar-divider);
+ }
-main:has(.app-details-container) .app-header {
- margin-bottom: 0;
-}
+ main:has(.app-details-container) .app-header {
+ margin-bottom: 0;
+ }
-.app-header nav.app-topbar {
- display: grid;
- grid-template-columns: auto 1fr auto;
- gap: var(--app-spacing);
- align-items: center;
-}
+ .app-header nav.app-topbar {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ gap: var(--app-spacing);
+ align-items: center;
+ }
-.app-header nav li {
- padding: 0;
-}
+ .app-header nav li {
+ padding: 0;
+ }
-.app-topbar-left,
-.app-topbar-right {
- display: flex;
- gap: var(--app-topbar-list-gap);
- align-items: center;
- color: var(--app-muted-color);
-}
+ .app-topbar-left,
+ .app-topbar-right {
+ display: flex;
+ gap: var(--app-topbar-list-gap);
+ align-items: center;
+ color: var(--app-muted-color);
+ }
-.app-topbar-right {
- justify-content: flex-end;
-}
+ .app-topbar-right {
+ justify-content: flex-end;
+ }
-.app-topbar-center {
- min-height: 1px;
-}
+ .app-topbar-center {
+ min-height: 1px;
+ display: flex;
+ justify-content: center;
+ }
-.app-topbar-menu-item-detail-sidebar {
- display: none;
-}
+ .app-topbar-search-trigger {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ max-width: min(24rem, 50vw);
+ width: 100%;
+ padding: 0.3rem 0.65rem;
+ border: var(--app-border-width) solid var(--app-border);
+ border-radius: var(--app-border-radius);
+ background: transparent;
+ color: var(--app-muted-color);
+ font-size: var(--text-sm);
+ cursor: pointer;
+ transition:
+ border-color var(--app-transition),
+ color var(--app-transition);
+ appearance: none;
+ -webkit-appearance: none;
+ margin: 0;
+ }
-main:has(#app-details-aside-section) .app-topbar-menu-item-detail-sidebar {
- display: list-item;
-}
+ .app-topbar-search-trigger:hover,
+ .app-topbar-search-trigger:focus-visible {
+ border-color: var(--app-primary);
+ }
-.app-header details.dropdown > summary:not([role]) {
- border: none;
- height: auto;
- padding: var(--app-topbar-item-padding);
- border-radius: 0;
- background: transparent;
-}
+ .app-topbar-search-trigger .bi {
+ font-size: var(--text-sm);
+ flex-shrink: 0;
+ }
-/* .app-header details.dropdown > summary:not([role]):after {
+ .app-topbar-search-trigger-text {
+ flex: 1;
+ text-align: left;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .app-topbar-search-trigger-shortcut {
+ border: 1px solid var(--app-border);
+ border-radius: 4px;
+ background: var(--app-card-background-color);
+ color: var(--app-muted-color);
+ font-size: var(--text-xs);
+ line-height: var(--leading-none);
+ padding: 0.1rem 0.3rem;
+ flex-shrink: 0;
+ }
+
+ @media (max-width: 576px) {
+ .app-topbar-search-trigger-text,
+ .app-topbar-search-trigger-shortcut {
+ display: none;
+ }
+
+ .app-topbar-search-trigger {
+ width: auto;
+ padding: var(--app-topbar-item-padding);
+ border: none;
+ }
+ }
+
+ .app-topbar-menu-item-detail-sidebar {
+ display: none;
+ }
+
+ main:has(#app-details-aside-section) .app-topbar-menu-item-detail-sidebar {
+ display: list-item;
+ }
+
+ .app-header details.dropdown > summary:not([role]) {
+ border: none;
+ height: auto;
+ padding: var(--app-topbar-item-padding);
+ border-radius: 0;
+ background: transparent;
+ }
+
+ /* .app-header details.dropdown > summary:not([role]):after {
display: none;
} */
-.app-topbar-left > li > a,
-.app-topbar-right > li > a {
- padding: var(--app-topbar-item-padding);
- color: var(--app-topbar-muted);
- display: block;
- margin: 0;
-}
-
-.app-topbar-left button,
-.app-topbar-right button {
- padding: var(--app-topbar-item-padding);
- color: var(--app-topbar-muted);
- display: block;
- margin: 0;
- background: transparent;
- border: none;
- border-radius: 0;
-}
-.app-header #global-back.is-disabled,
-.app-header #global-forward.is-disabled {
- opacity: 0.4;
- cursor: not-allowed;
- pointer-events: none;
-}
-
-.app-topbar-tenant-chip {
- display: inline-flex;
- align-items: center;
- gap: calc(var(--app-spacing) * 0.4);
- max-width: min(26rem, 45vw);
- padding: calc(var(--app-spacing) * 0.35) calc(var(--app-spacing) * 0.75);
- border: var(--app-border-width) solid var(--app-border);
- border-radius: 999px;
- background: transparent;
- color: var(--app-topbar-muted);
-}
-
-.app-topbar-tenant-menu > summary.app-topbar-tenant-chip {
- cursor: pointer;
-}
-
-.app-topbar-tenant-name {
- display: inline-block;
- max-width: min(20rem, 38vw);
- font-size: var(--text-sm);
- line-height: var(--leading-tight);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.app-topbar-user-menu > summary {
- color: var(--app-topbar-muted);
-}
-
-.app-topbar-menu-list {
- min-width: min(17rem, calc(100vw - (var(--app-spacing) * 2)));
- max-width: min(24rem, calc(100vw - (var(--app-spacing) * 2)));
- max-height: min(32rem, calc(100vh - (var(--app-spacing) * 4)));
- overflow-y: auto;
- overflow-x: hidden;
- overscroll-behavior: contain;
- -webkit-overflow-scrolling: touch;
- scrollbar-gutter: stable;
-}
-
-@supports (height: 100dvh) {
- .app-topbar-menu-list {
- max-height: min(32rem, calc(100dvh - (var(--app-spacing) * 4)));
+ .app-topbar-left > li > a,
+ .app-topbar-right > li > a {
+ padding: var(--app-topbar-item-padding);
+ color: var(--app-topbar-muted);
+ display: block;
+ margin: 0;
}
-}
-@media (min-width: 768px) {
- .app-topbar-menu-list {
- max-height: min(40rem, calc(100vh - (var(--app-spacing) * 4)));
+ .app-topbar-left button,
+ .app-topbar-right button {
+ padding: var(--app-topbar-item-padding);
+ color: var(--app-topbar-muted);
+ display: block;
+ margin: 0;
+ background: transparent;
+ border: none;
+ border-radius: 0;
}
-}
-
-@supports (height: 100dvh) {
- @media (min-width: 768px) {
- .app-topbar-menu-list {
- max-height: min(40rem, calc(100dvh - (var(--app-spacing) * 4)));
- }
+ .app-header #global-back.is-disabled,
+ .app-header #global-forward.is-disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ pointer-events: none;
}
-}
-.app-topbar-menu-list--right {
- right: 0;
- left: auto;
-}
-
-/* Override global dropdown default (left:0) without using dir=rtl hacks. */
-.app-header details.dropdown > summary + ul.app-topbar-menu-list--right {
- right: 0;
- left: auto;
- inset-inline-end: 0;
- inset-inline-start: auto;
-}
-
-.app-topbar-menu-list li > a,
-.app-topbar-menu-list li > button {
- display: block;
- width: 100%;
- margin: 0;
- padding: 0.375rem 0.5rem;
- overflow: hidden;
- border: 0;
- border-radius: 3px;
- background: transparent;
- color: var(--app-dropdown-color);
- font: inherit;
- line-height: inherit;
- text-align: left;
- text-decoration: none;
- text-overflow: ellipsis;
- cursor: pointer;
- appearance: none;
- -webkit-appearance: none;
-}
-
-.app-topbar-menu-list li {
- display: block;
-}
-
-.app-topbar-menu-list li > button:hover,
-.app-topbar-menu-list li > button:focus,
-.app-topbar-menu-list li > button:focus-visible {
- background: color-mix(in srgb, var(--app-contrast) 8%, transparent);
-}
-
-.app-topbar-menu-list li > button.app-topbar-menu-with-hotkey {
- display: grid;
- grid-template-columns: 1fr auto;
- align-items: center;
- column-gap: calc(var(--app-spacing) * 0.6);
-}
-
-.app-topbar-hotkey {
- color: var(--app-muted-color);
- font-size: var(--text-xs);
- white-space: nowrap;
-}
-
-.app-topbar-menu-list li.app-topbar-menu-heading {
- padding: calc(var(--app-spacing) * 0.5) 0.5rem calc(var(--app-spacing) * 0.35);
- color: var(--app-muted-color);
- font-weight: var(--font-medium);
- letter-spacing: var(--tracking-wide);
- text-transform: uppercase;
- pointer-events: none;
-}
-
-.app-topbar-menu-list li.app-topbar-menu-divider {
- margin: calc(var(--app-spacing) * 0.25) 0;
- padding: 0;
- height: 1px;
- background: var(--app-border);
- list-style: none;
-}
-
-.app-topbar-menu-list li.app-topbar-menu-divider + li.app-topbar-menu-heading {
- margin-top: calc(var(--app-spacing) * 0.15);
-}
-
-@media (min-width: 768px) {
- header.app-header {
- position: sticky;
- top: 0;
- background: var(--app-topbar-bg);
- z-index: 10;
- }
-}
-
-.app-header details.dropdown[open] > summary::before {
- display: none;
-}
-
-@media (max-width: 767px) {
.app-topbar-tenant-chip {
- max-width: min(14rem, 55vw);
+ display: inline-flex;
+ align-items: center;
+ gap: calc(var(--app-spacing) * 0.4);
+ max-width: min(26rem, 45vw);
+ padding: calc(var(--app-spacing) * 0.35) calc(var(--app-spacing) * 0.75);
+ border: var(--app-border-width) solid var(--app-border);
+ border-radius: 999px;
+ background: transparent;
+ color: var(--app-topbar-muted);
+ }
+
+ .app-topbar-tenant-menu > summary.app-topbar-tenant-chip {
+ cursor: pointer;
}
.app-topbar-tenant-name {
- max-width: min(11rem, 45vw);
+ display: inline-block;
+ max-width: min(20rem, 38vw);
+ font-size: var(--text-sm);
+ line-height: var(--leading-tight);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .app-topbar-user-menu > summary {
+ color: var(--app-topbar-muted);
+ }
+
+ .app-topbar-menu-list {
+ min-width: min(17rem, calc(100vw - (var(--app-spacing) * 2)));
+ max-width: min(24rem, calc(100vw - (var(--app-spacing) * 2)));
+ max-height: min(32rem, calc(100vh - (var(--app-spacing) * 4)));
+ overflow-y: auto;
+ overflow-x: hidden;
+ overscroll-behavior: contain;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-gutter: stable;
+ }
+
+ @supports (height: 100dvh) {
+ .app-topbar-menu-list {
+ max-height: min(32rem, calc(100dvh - (var(--app-spacing) * 4)));
+ }
+ }
+
+ @media (min-width: 768px) {
+ .app-topbar-menu-list {
+ max-height: min(40rem, calc(100vh - (var(--app-spacing) * 4)));
+ }
+ }
+
+ @supports (height: 100dvh) {
+ @media (min-width: 768px) {
+ .app-topbar-menu-list {
+ max-height: min(40rem, calc(100dvh - (var(--app-spacing) * 4)));
+ }
+ }
+ }
+
+ .app-topbar-menu-list--right {
+ right: 0;
+ left: auto;
+ }
+
+ /* Override global dropdown default (left:0) without using dir=rtl hacks. */
+ .app-header details.dropdown > summary + ul.app-topbar-menu-list--right {
+ right: 0;
+ left: auto;
+ inset-inline-end: 0;
+ inset-inline-start: auto;
+ }
+
+ .app-topbar-menu-list li > a,
+ .app-topbar-menu-list li > button {
+ display: block;
+ width: 100%;
+ margin: 0;
+ padding: 0.375rem 0.5rem;
+ overflow: hidden;
+ border: 0;
+ border-radius: 3px;
+ background: transparent;
+ color: var(--app-dropdown-color);
+ font: inherit;
+ line-height: inherit;
+ text-align: left;
+ text-decoration: none;
+ text-overflow: ellipsis;
+ cursor: pointer;
+ appearance: none;
+ -webkit-appearance: none;
+ }
+
+ .app-topbar-menu-list li {
+ display: block;
+ }
+
+ .app-topbar-menu-list li > button:hover,
+ .app-topbar-menu-list li > button:focus,
+ .app-topbar-menu-list li > button:focus-visible {
+ background: color-mix(in srgb, var(--app-contrast) 8%, transparent);
+ }
+
+ .app-topbar-menu-list li > button.app-topbar-menu-with-hotkey {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ column-gap: calc(var(--app-spacing) * 0.6);
+ }
+
+ .app-topbar-hotkey {
+ color: var(--app-muted-color);
+ font-size: var(--text-xs);
+ white-space: nowrap;
+ }
+
+ .app-topbar-menu-list li.app-topbar-menu-heading {
+ padding: calc(var(--app-spacing) * 0.5) 0.5rem
+ calc(var(--app-spacing) * 0.35);
+ color: var(--app-muted-color);
+ font-weight: var(--font-medium);
+ letter-spacing: var(--tracking-wide);
+ text-transform: uppercase;
+ pointer-events: none;
+ }
+
+ .app-topbar-menu-list li.app-topbar-menu-divider {
+ margin: calc(var(--app-spacing) * 0.25) 0;
+ padding: 0;
+ height: 1px;
+ background: var(--app-border);
+ list-style: none;
+ }
+
+ .app-topbar-menu-list
+ li.app-topbar-menu-divider
+ + li.app-topbar-menu-heading {
+ margin-top: calc(var(--app-spacing) * 0.15);
+ }
+
+ @media (min-width: 768px) {
+ header.app-header {
+ position: sticky;
+ top: 0;
+ background: var(--app-topbar-bg);
+ z-index: 10;
+ }
+ }
+
+ .app-header details.dropdown[open] > summary::before {
+ display: none;
+ }
+
+ @media (max-width: 767px) {
+ .app-topbar-tenant-chip {
+ max-width: min(14rem, 55vw);
+ }
+
+ .app-topbar-tenant-name {
+ max-width: min(11rem, 45vw);
+ }
}
}
-}
diff --git a/web/js/app-init.js b/web/js/app-init.js
index d268dbb..b7424c3 100644
--- a/web/js/app-init.js
+++ b/web/js/app-init.js
@@ -244,7 +244,7 @@ const bootRuntime = async () => {
// Phase C: high-risk (search, side panel interactions).
runtime.register('global-search', initGlobalSearch, {
- selector: '[data-app-component="global-search"]',
+ selector: '[data-app-search-dialog]',
configPath: 'components.globalSearch',
});
diff --git a/web/js/components/app-global-search.js b/web/js/components/app-global-search.js
index 1b48fab..2b6f2fb 100644
--- a/web/js/components/app-global-search.js
+++ b/web/js/components/app-global-search.js
@@ -1,218 +1,324 @@
/**
- * Global search sidebar — debounced fetch with live result counts and Ctrl+K shortcut.
+ * Global search command-palette dialog.
+ *
+ * Opens via Ctrl+K / Cmd+K or topbar trigger. Fetches live results from the
+ * preview API and renders them grouped by category with keyboard navigation.
*/
import { warnOnce, resolveHost } from '../core/app-dom.js';
import { isEditableTarget } from '../core/app-form-utils.js';
-import { requestAsidePanelOpen } from '../core/app-ui-channels.js';
+
+const ICON_MAP = {
+ 'users': 'bi-person-badge',
+ 'tenants': 'bi-buildings',
+ 'departments': 'bi-diagram-3',
+ 'roles': 'bi-people',
+ 'permissions': 'bi-shield-lock',
+ 'scheduled-jobs': 'bi-calendar-check',
+ 'pages': 'bi-file-text',
+ 'mail-log': 'bi-envelope-paper',
+ 'docs': 'bi-journal-text',
+ 'hotkeys': 'bi-keyboard',
+ 'contacts': 'bi-person-lines-fill',
+ 'bookmarks': 'bi-bookmark',
+ 'api-audit': 'bi-journal-code',
+ 'system-audit': 'bi-journal-check',
+};
export function initGlobalSearch(root = document, options = {}) {
const host = resolveHost(root);
- const inputSelector = String(options.inputSelector || '#side-search').trim() || '#side-search';
- const resultsSelector = String(options.resultsSelector || '[data-global-search-results]').trim() || '[data-global-search-results]';
+ const dialog = host.matches?.('[data-app-search-dialog]') ? host : host.querySelector('[data-app-search-dialog]');
+ if (!dialog) {return { destroy: () => {} };}
- const input = host.matches?.(inputSelector) ? host : host.querySelector(inputSelector);
- const resultsEl = host.matches?.(resultsSelector) ? host : host.querySelector(resultsSelector);
- if (!input || !resultsEl) {return { destroy: () => {} };}
- if (input.dataset.globalSearchBound === '1' && input._globalSearchApi) {
- return input._globalSearchApi;
+ if (dialog.dataset.globalSearchBound === '1' && dialog._globalSearchApi) {
+ return dialog._globalSearchApi;
}
- input.dataset.globalSearchBound = '1';
+ dialog.dataset.globalSearchBound = '1';
+
+ const input = dialog.querySelector('#search-dialog-input');
+ const resultsEl = dialog.querySelector('[data-search-dialog-results]');
+ const emptyStateEl = dialog.querySelector('[data-search-dialog-empty]');
+ const loadingEl = dialog.querySelector('[data-search-dialog-loading]');
+ const errorEl = dialog.querySelector('[data-search-dialog-error]');
+ const footerEl = dialog.querySelector('[data-search-dialog-footer]');
+ const viewAllLink = dialog.querySelector('[data-search-dialog-view-all]');
+ const closeButton = dialog.querySelector('[data-app-search-dialog-close]');
+ const shortcutEl = dialog.querySelector('[data-search-shortcut]');
+ const emptyHintEl = dialog.querySelector('[data-search-empty-hint-template]');
+ const triggerButton = document.querySelector('[data-app-search-trigger]');
+ const triggerShortcutEl = document.querySelector('[data-search-trigger-shortcut]');
- const emptyStateEl = host.querySelector('[data-global-search-empty]');
- const emptyHintEl = host.querySelector('[data-search-empty-hint-template]');
- const shortcutEl = host.querySelector('[data-search-shortcut]');
- const panel = host.matches?.('#aside-panel-search') ? host : host.querySelector('#aside-panel-search');
- const toggleButton = host.querySelector('[data-search-details-toggle]');
- const toggleIcon = host.querySelector('[data-search-details-icon]');
const appBase = String(options.appBase || document.baseURI || window.location.origin).trim() || document.baseURI;
- const endpoint = new URL('admin/search/data', appBase);
- const resultsPage = new URL('search', appBase);
+ const endpointAttr = dialog.getAttribute('data-search-endpoint') || 'admin/search/data';
+ const resultsPageAttr = dialog.getAttribute('data-search-results-page') || 'search';
+ const endpoint = new URL(endpointAttr, appBase);
+ const resultsPage = new URL(resultsPageAttr, appBase);
+
const minLength = 2;
let pending = null;
+ let activeIndex = -1;
+ let flatItems = [];
+
const shortcutLabel = /(Mac|iPhone|iPad|iPod)/i.test(navigator.platform || navigator.userAgent)
? '⌘K'
: 'Ctrl+K';
+ if (shortcutEl) {shortcutEl.textContent = shortcutLabel;}
+ if (triggerShortcutEl) {triggerShortcutEl.textContent = shortcutLabel;}
if (emptyHintEl) {
const template = emptyHintEl.getAttribute('data-search-empty-hint-template') || '';
emptyHintEl.textContent = template.replace('{shortcut}', shortcutLabel);
}
- if (shortcutEl) {
- shortcutEl.textContent = shortcutLabel;
- }
- const itemsByKey = new Map();
- resultsEl.querySelectorAll('[data-search-key]').forEach((item) => {
- const key = item.getAttribute('data-search-key');
- if (key) {itemsByKey.set(key, item);}
- });
- if (itemsByKey.size === 0) {
- warnOnce('UI_EL_MISSING', 'No [data-search-key] elements found in results container', { module: 'global-search' });
- }
+ // --- State management ---
- const setLoadingState = (value) => {
- resultsEl.querySelectorAll('[data-search-count]').forEach((el) => {
- el.textContent = value;
+ const setState = (state) => {
+ if (emptyStateEl) {emptyStateEl.hidden = state !== 'empty';}
+ if (loadingEl) {loadingEl.hidden = state !== 'loading';}
+ if (errorEl) {errorEl.hidden = state !== 'error';}
+ if (resultsEl) {resultsEl.hidden = state !== 'success';}
+ if (footerEl) {footerEl.hidden = state !== 'success';}
+ };
+
+ const clearHighlight = () => {
+ activeIndex = -1;
+ resultsEl.querySelectorAll('[role="option"]').forEach((el) => {
+ el.removeAttribute('aria-selected');
+ el.classList.remove('app-search-dialog-item-active');
});
+ if (input) {input.removeAttribute('aria-activedescendant');}
};
- const setIdleState = (idle) => {
- if (emptyStateEl) {
- emptyStateEl.hidden = !idle;
- }
- resultsEl.hidden = idle;
+ const setHighlight = (index) => {
+ clearHighlight();
+ if (index < 0 || index >= flatItems.length) {return;}
+ activeIndex = index;
+ const item = flatItems[index];
+ item.setAttribute('aria-selected', 'true');
+ item.classList.add('app-search-dialog-item-active');
+ item.scrollIntoView({ block: 'nearest' });
+ if (input) {input.setAttribute('aria-activedescendant', item.id);}
};
- const render = (items, query) => {
- const renderedKeys = new Set();
- items.forEach((item) => {
- const li = itemsByKey.get(item.key);
- if (!li) {
- warnOnce('UI_EL_MISSING', `No element found for key: ${item.key}`, { module: 'global-search' });
- return;
+ // --- Rendering ---
+
+ const iconForKey = (key) => ICON_MAP[key] || 'bi-search';
+
+ const render = (categories, query) => {
+ resultsEl.innerHTML = '';
+ flatItems = [];
+ let itemIndex = 0;
+ let hasResults = false;
+
+ categories.forEach((category) => {
+ if (category.count === 0 && (!category.items || category.items.length === 0)) {return;}
+ hasResults = true;
+
+ const group = document.createElement('li');
+ group.setAttribute('role', 'presentation');
+ group.classList.add('app-search-dialog-category');
+
+ const heading = document.createElement('div');
+ heading.classList.add('app-search-dialog-category-heading');
+ heading.innerHTML = ` ${escapeHtml(category.label || category.key)} ${category.count}`;
+ group.appendChild(heading);
+
+ if (Array.isArray(category.items) && category.items.length > 0) {
+ const list = document.createElement('ul');
+ list.classList.add('app-search-dialog-category-items');
+ list.setAttribute('role', 'group');
+
+ category.items.forEach((item) => {
+ if (!item || !item.url) {return;}
+ const li = document.createElement('li');
+ li.setAttribute('role', 'option');
+ li.setAttribute('id', `search-item-${itemIndex}`);
+ li.classList.add('app-search-dialog-item');
+ li.dataset.searchUrl = item.url;
+
+ const link = document.createElement('a');
+ link.href = item.url;
+ link.textContent = item.label || '';
+ link.tabIndex = -1;
+ li.appendChild(link);
+ list.appendChild(li);
+
+ flatItems.push(li);
+ itemIndex++;
+ });
+
+ group.appendChild(list);
}
- renderedKeys.add(item.key);
- const countEl = li.querySelector('[data-search-count]');
- const link = li.querySelector('a');
- const previewEl = li.querySelector('[data-search-preview]');
- const base = item.key === 'pages' && item.url ? item.url : (li.getAttribute('data-search-base') || item.url);
- const url = new URL(base, appBase);
- if (item.key !== 'pages') {
+
+ if (category.url && category.count > 0) {
+ const viewCategoryLink = document.createElement('a');
+ viewCategoryLink.classList.add('app-search-dialog-category-view-all');
+ viewCategoryLink.href = category.url;
+ viewCategoryLink.innerHTML = `${escapeHtml(category.label || category.key)} `;
+ group.appendChild(viewCategoryLink);
+ }
+
+ resultsEl.appendChild(group);
+ });
+
+ if (hasResults) {
+ setState('success');
+ if (viewAllLink) {
+ const url = new URL(resultsPage.toString());
url.searchParams.set('search', query);
+ viewAllLink.href = url.toString();
}
- if (countEl) {countEl.textContent = item.count.toString();}
- if (link) {
- link.href = item.count > 0 ? url.toString() : base;
- link.setAttribute('aria-disabled', item.count > 0 ? 'false' : 'true');
- }
- if (previewEl) {
- previewEl.innerHTML = '';
- if (Array.isArray(item.items) && item.items.length > 0) {
- item.items.forEach((previewItem, idx) => {
- if (!previewItem || !previewItem.url) {
- warnOnce('UI_DATA_MISSING', `Preview item missing url at index ${idx} for key: ${item.key}`, {
- module: 'global-search',
- item: previewItem
- });
- return;
- }
- const previewLi = document.createElement('li');
- const previewLink = document.createElement('a');
- previewLink.href = previewItem.url;
- previewLink.textContent = previewItem.label || '';
- previewLi.appendChild(previewLink);
- previewEl.appendChild(previewLi);
- });
+ } else {
+ setState('empty');
+ if (emptyStateEl) {
+ const p = emptyStateEl.querySelector('p');
+ if (p) {
+ p.textContent = emptyStateEl.dataset.noResultsText || '';
}
}
- if (item.count > 0) {
- li.hidden = false;
- } else {
- li.hidden = true;
- }
- });
-
- itemsByKey.forEach((li, key) => {
- if (renderedKeys.has(key)) {
- return;
- }
- const countEl = li.querySelector('[data-search-count]');
- const link = li.querySelector('a');
- const previewEl = li.querySelector('[data-search-preview]');
- const base = li.getAttribute('data-search-base') || '#';
- if (countEl) {countEl.textContent = '0';}
- if (link) {
- link.href = base;
- link.setAttribute('aria-disabled', 'true');
- }
- if (previewEl) {
- previewEl.innerHTML = '';
- }
- li.hidden = true;
- });
+ }
+ clearHighlight();
};
- const clear = () => {
- setIdleState(true);
- setLoadingState('0');
- resultsEl.querySelectorAll('a').forEach((link) => {
- link.setAttribute('aria-disabled', 'true');
- const base = link.closest('[data-search-base]')?.getAttribute('data-search-base');
- link.setAttribute('href', base || '#');
- });
- resultsEl.querySelectorAll('[data-search-preview]').forEach((list) => {
- list.innerHTML = '';
- });
- resultsEl.querySelectorAll('[data-search-key]').forEach((li) => {
- li.hidden = false;
- });
- };
+ // --- Fetch ---
- const fetchCounts = async (value) => {
- setIdleState(false);
+ const fetchResults = async (value) => {
+ setState('loading');
const url = new URL(endpoint.toString());
url.searchParams.set('q', value);
- setLoadingState('…');
- const response = await fetch(url.toString(), { headers: { 'Accept': 'application/json' } });
- if (!response.ok) {
- warnOnce('FETCH_FAILED', `Global search failed: ${response.status} ${response.statusText}`, {
- module: 'global-search'
- });
- clear();
- return;
+
+ try {
+ const response = await fetch(url.toString(), { headers: { 'Accept': 'application/json' } });
+ if (!response.ok) {
+ warnOnce('FETCH_FAILED', `Global search failed: ${response.status} ${response.statusText}`, { module: 'global-search' });
+ setState('error');
+ return;
+ }
+ const data = await response.json();
+ if (Array.isArray(data)) {
+ render(data, value);
+ } else {
+ warnOnce('FETCH_INVALID', 'Invalid response format for global search', { module: 'global-search', type: typeof data });
+ setState('error');
+ }
+ } catch (err) {
+ warnOnce('FETCH_ERROR', 'Global search fetch error', { module: 'global-search', err });
+ setState('error');
}
- const data = await response.json();
- if (Array.isArray(data)) {
- render(data, value);
- } else {
- warnOnce('FETCH_INVALID', `Invalid response format for global search`, {
- module: 'global-search',
- type: typeof data
- });
- clear();
+ };
+
+ // --- Dialog open/close ---
+
+ const openDialog = () => {
+ if (dialog.open) {return;}
+ dialog.showModal();
+ setState('empty');
+ if (emptyStateEl) {
+ const p = emptyStateEl.querySelector('p');
+ if (p) {
+ p.textContent = emptyHintEl
+ ? ''
+ : (dialog.dataset.emptyText || '');
+ }
+ }
+ resetEmptyState();
+ window.requestAnimationFrame(() => {
+ if (input) {
+ input.focus();
+ input.select();
+ }
+ });
+ // If input already has a value, trigger search
+ const currentValue = input ? input.value.trim() : '';
+ if (currentValue.length >= minLength) {
+ fetchResults(currentValue);
+ }
+ };
+
+ const closeDialog = () => {
+ if (!dialog.open) {return;}
+ dialog.close();
+ clearHighlight();
+ };
+
+ const resetEmptyState = () => {
+ if (!emptyStateEl) {return;}
+ const p = emptyStateEl.querySelector('p');
+ if (p) {
+ p.textContent = p.dataset.originalText || '';
+ }
+ };
+
+ // Store original text for empty state
+ if (emptyStateEl) {
+ const p = emptyStateEl.querySelector('p');
+ if (p) {p.dataset.originalText = p.textContent;}
+ }
+
+ // --- Navigation ---
+
+ const navigateToItem = (index) => {
+ if (index < 0 || index >= flatItems.length) {return;}
+ const item = flatItems[index];
+ const url = item.dataset.searchUrl || item.querySelector('a')?.href;
+ if (url) {
+ closeDialog();
+ window.location.href = url;
}
};
const submitSearch = () => {
- const value = input.value.trim();
+ const value = input ? input.value.trim() : '';
if (value.length < minLength) {return;}
- resultsPage.searchParams.set('search', value);
- window.location.href = resultsPage.toString();
+ const url = new URL(resultsPage.toString());
+ url.searchParams.set('search', value);
+ closeDialog();
+ window.location.href = url.toString();
};
- const focusSearch = () => {
- window.requestAnimationFrame(() => {
- input.focus();
- input.select();
- });
- };
-
- const openSearch = () => {
- requestAsidePanelOpen('search', { fromUser: true });
- focusSearch();
- };
-
- // --- Event handlers (named for cleanup) ---
+ // --- Event handlers ---
const onInput = () => {
const value = input.value.trim();
if (value.length < minLength) {
- clear();
+ setState('empty');
+ resetEmptyState();
+ resultsEl.innerHTML = '';
+ flatItems = [];
+ clearHighlight();
+ if (footerEl) {footerEl.hidden = true;}
return;
}
if (pending) {window.clearTimeout(pending);}
pending = window.setTimeout(() => {
- fetchCounts(value).catch((err) => {
+ fetchResults(value).catch((err) => {
warnOnce('FETCH_ERROR', 'Global search fetch error', { module: 'global-search', err });
- clear();
+ setState('error');
});
}, 300);
};
const onInputKeydown = (event) => {
- if (event.key !== 'Enter') {return;}
- event.preventDefault();
- submitSearch();
+ if (event.key === 'ArrowDown') {
+ event.preventDefault();
+ if (flatItems.length === 0) {return;}
+ setHighlight(activeIndex < flatItems.length - 1 ? activeIndex + 1 : 0);
+ return;
+ }
+ if (event.key === 'ArrowUp') {
+ event.preventDefault();
+ if (flatItems.length === 0) {return;}
+ setHighlight(activeIndex > 0 ? activeIndex - 1 : flatItems.length - 1);
+ return;
+ }
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ if (activeIndex >= 0) {
+ navigateToItem(activeIndex);
+ } else {
+ submitSearch();
+ }
+ return;
+ }
};
const onDocumentKeydown = (event) => {
@@ -221,83 +327,86 @@ export function initGlobalSearch(root = document, options = {}) {
if (key !== 'k' && key !== '/') {return;}
if (isEditableTarget(event.target) && event.target !== input) {return;}
event.preventDefault();
- openSearch();
+ openDialog();
};
- const onEmptyStateClick = () => {
- focusSearch();
+ const onCloseClick = () => {
+ closeDialog();
+ };
+
+ const onTriggerClick = () => {
+ openDialog();
+ };
+
+ const onDialogClick = (event) => {
+ // Close on backdrop click (click on