feat(webui): center optional global search
This commit is contained in:
@@ -39,6 +39,14 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
|
||||
const ViewSelector = viewsRuntime?.Selector ?? null;
|
||||
const searchRuntime = usePlatformUiCapability<SearchRuntimeUiCapability>("search.runtime");
|
||||
const GlobalSearch = searchRuntime?.GlobalSearch ?? null;
|
||||
const canUseGlobalSearch = Boolean(
|
||||
searchRuntime &&
|
||||
(
|
||||
!searchRuntime.anyOf?.length ||
|
||||
hasAnyScope(auth, searchRuntime.anyOf)
|
||||
)
|
||||
);
|
||||
const showGlobalSearch = Boolean(auth && GlobalSearch && canUseGlobalSearch);
|
||||
|
||||
const activeTenant = auth?.active_tenant ?? auth?.tenant ?? null;
|
||||
const tenants = auth?.tenants ?? (activeTenant ? [activeTenant] : []);
|
||||
@@ -138,7 +146,7 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="titlebar">
|
||||
<header className={`titlebar${showGlobalSearch ? " has-global-search" : ""}`}>
|
||||
{!backendReachable ?
|
||||
<div
|
||||
className="backend-offline-topbar-alert"
|
||||
@@ -158,94 +166,98 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
|
||||
</button>
|
||||
}
|
||||
|
||||
{auth && showContextSelectors &&
|
||||
<div className="titlebar-context-selectors">
|
||||
{activeTenant && showTenantControl &&
|
||||
<div className="tenant-selector" ref={tenantRef}>
|
||||
<span className="tenant-label">{translateText("i18n:govoplan-core.tenant_label_prefix")}</span>
|
||||
{canSwitchTenant ?
|
||||
<>
|
||||
<button className="tenant-name-button" onClick={() => setTenantOpen(!tenantOpen)}>
|
||||
<strong>{activeTenant.name}</strong>
|
||||
<span className="tenant-caret">▾</span>
|
||||
</button>
|
||||
{tenantOpen &&
|
||||
<div className="dropdown-menu tenant-menu">
|
||||
{tenants.map((tenant) => {
|
||||
const active = tenant.id === activeTenant.id;
|
||||
return (
|
||||
<button
|
||||
key={tenant.id}
|
||||
className={`dropdown-item ${active ? "active" : ""}`}
|
||||
onClick={() => void handleTenantSelect(tenant)}
|
||||
disabled={Boolean(switchingTenantId)}>
|
||||
|
||||
<span>{switchingTenantId === tenant.id ? translateText("i18n:govoplan-core.switching.ca40b0ec") : tenant.name}</span>
|
||||
{active && <Check size={16} />}
|
||||
</button>);
|
||||
|
||||
})}
|
||||
{tenantError && <DismissibleAlert tone="danger" compact dismissible={false} className="dropdown-error">{tenantError}</DismissibleAlert>}
|
||||
</div>
|
||||
}
|
||||
</> :
|
||||
|
||||
<strong>{activeTenant.name}</strong>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{ViewSelector &&
|
||||
<ViewSelector settings={settings} auth={auth} projection={projection} />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="titlebar-spacer" />
|
||||
|
||||
{auth && GlobalSearch &&
|
||||
<GlobalSearch settings={settings} auth={auth} />
|
||||
}
|
||||
|
||||
<LanguageMenu />
|
||||
<HelpMenu auth={auth} />
|
||||
|
||||
{auth && notificationsAvailable &&
|
||||
<button className="titlebar-icon-link titlebar-notification-button" onClick={openNotificationCenter} title={translateText("i18n:govoplan-core.notifications.753a22b2")} aria-label={translateText("i18n:govoplan-core.notifications.753a22b2")}>
|
||||
<Bell size={18} />
|
||||
{unreadNotificationCount > 0 &&
|
||||
<span className="titlebar-notification-badge" aria-label={`${unreadNotificationCount} unread`}>
|
||||
{notificationBadgeLabel}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
<div className="context-menu-wrap" ref={accountRef}>
|
||||
<button className="account-pill" onClick={() => setAccountOpen(!accountOpen)}>
|
||||
<UserCircle size={22} />
|
||||
<span>{displayUserName}</span>
|
||||
<span className="tenant-caret">▾</span>
|
||||
</button>
|
||||
{accountOpen &&
|
||||
<div className="dropdown-menu account-menu">
|
||||
{auth ?
|
||||
<div className="titlebar-leading">
|
||||
{auth && showContextSelectors &&
|
||||
<div className="titlebar-context-selectors">
|
||||
{activeTenant && showTenantControl &&
|
||||
<div className="tenant-selector" ref={tenantRef}>
|
||||
<span className="tenant-label">{translateText("i18n:govoplan-core.tenant_label_prefix")}</span>
|
||||
{canSwitchTenant ?
|
||||
<>
|
||||
{auth.user &&
|
||||
<div className="account-menu-header">
|
||||
<strong>{auth.user.display_name || auth.user.email}</strong>
|
||||
<span>{auth.user.email}</span>
|
||||
</div>
|
||||
}
|
||||
<button className="dropdown-item" onClick={handleAccountSettings}><Settings size={16} /> {translateText("i18n:govoplan-core.account_settings.82cf8a5f")}</button>
|
||||
<button className="dropdown-item" onClick={handleLogout}><LogOut size={16} /> {translateText("i18n:govoplan-core.sign_out.dc1649a1")}</button>
|
||||
<button className="tenant-name-button" onClick={() => setTenantOpen(!tenantOpen)}>
|
||||
<strong>{activeTenant.name}</strong>
|
||||
<span className="tenant-caret">▾</span>
|
||||
</button>
|
||||
{tenantOpen &&
|
||||
<div className="dropdown-menu tenant-menu">
|
||||
{tenants.map((tenant) => {
|
||||
const active = tenant.id === activeTenant.id;
|
||||
return (
|
||||
<button
|
||||
key={tenant.id}
|
||||
className={`dropdown-item ${active ? "active" : ""}`}
|
||||
onClick={() => void handleTenantSelect(tenant)}
|
||||
disabled={Boolean(switchingTenantId)}>
|
||||
|
||||
<span>{switchingTenantId === tenant.id ? translateText("i18n:govoplan-core.switching.ca40b0ec") : tenant.name}</span>
|
||||
{active && <Check size={16} />}
|
||||
</button>);
|
||||
|
||||
})}
|
||||
{tenantError && <DismissibleAlert tone="danger" compact dismissible={false} className="dropdown-error">{tenantError}</DismissibleAlert>}
|
||||
</div>
|
||||
}
|
||||
</> :
|
||||
|
||||
<button className="dropdown-item" onClick={() => {setLoginOpen(true);setAccountOpen(false);}}><UserCircle size={16} /> {translateText("i18n:govoplan-core.sign_in.ada2e9e9")}</button>
|
||||
<strong>{activeTenant.name}</strong>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{ViewSelector &&
|
||||
<ViewSelector settings={settings} auth={auth} projection={projection} />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{auth && GlobalSearch && canUseGlobalSearch &&
|
||||
<div className="titlebar-global-search">
|
||||
<GlobalSearch settings={settings} auth={auth} />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="titlebar-actions">
|
||||
<LanguageMenu />
|
||||
<HelpMenu auth={auth} />
|
||||
|
||||
{auth && notificationsAvailable &&
|
||||
<button className="titlebar-icon-link titlebar-notification-button" onClick={openNotificationCenter} title={translateText("i18n:govoplan-core.notifications.753a22b2")} aria-label={translateText("i18n:govoplan-core.notifications.753a22b2")}>
|
||||
<Bell size={18} />
|
||||
{unreadNotificationCount > 0 &&
|
||||
<span className="titlebar-notification-badge" aria-label={`${unreadNotificationCount} unread`}>
|
||||
{notificationBadgeLabel}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
<div className="context-menu-wrap" ref={accountRef}>
|
||||
<button className="account-pill" onClick={() => setAccountOpen(!accountOpen)}>
|
||||
<UserCircle size={22} />
|
||||
<span>{displayUserName}</span>
|
||||
<span className="tenant-caret">▾</span>
|
||||
</button>
|
||||
{accountOpen &&
|
||||
<div className="dropdown-menu account-menu">
|
||||
{auth ?
|
||||
<>
|
||||
{auth.user &&
|
||||
<div className="account-menu-header">
|
||||
<strong>{auth.user.display_name || auth.user.email}</strong>
|
||||
<span>{auth.user.email}</span>
|
||||
</div>
|
||||
}
|
||||
<button className="dropdown-item" onClick={handleAccountSettings}><Settings size={16} /> {translateText("i18n:govoplan-core.account_settings.82cf8a5f")}</button>
|
||||
<button className="dropdown-item" onClick={handleLogout}><LogOut size={16} /> {translateText("i18n:govoplan-core.sign_out.dc1649a1")}</button>
|
||||
</> :
|
||||
|
||||
<button className="dropdown-item" onClick={() => {setLoginOpen(true);setAccountOpen(false);}}><UserCircle size={16} /> {translateText("i18n:govoplan-core.sign_in.ada2e9e9")}</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loginOpen && <LoginModal settings={settings} onClose={() => setLoginOpen(false)} onLogin={handleLogin} />}
|
||||
</header>);
|
||||
|
||||
|
||||
@@ -26,7 +26,12 @@
|
||||
.icon-nav-item:hover, .icon-nav-item.active { background: var(--rail-bg-active); color: var(--on-accent); border-left-color: var(--accent); }
|
||||
.icon-rail.compact { width: 58px; }
|
||||
.app-main { min-width: 0; min-height: 0; height: 100vh; display: grid; grid-template-rows: 64px 51px minmax(0, 1fr); }
|
||||
.titlebar { position: relative; background: var(--titlebar-bg); border-bottom: var(--border-line); display: flex; align-items: center; padding: 0 18px; gap: 36px; z-index: 100; box-shadow: var(--shadow-chrome); }
|
||||
.titlebar { position: relative; background: var(--titlebar-bg); border-bottom: var(--border-line); display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; padding: 0 18px; gap: 18px; z-index: 100; box-shadow: var(--shadow-chrome); }
|
||||
.titlebar.has-global-search { grid-template-columns: minmax(0, 1fr) minmax(190px, min(360px, 28vw)) minmax(0, 1fr); }
|
||||
.titlebar-leading { grid-column: 1; display: flex; align-items: center; min-width: 0; }
|
||||
.titlebar-global-search { grid-column: 2; position: relative; width: 100%; min-width: 0; height: 34px; }
|
||||
.titlebar-actions { grid-column: 2; display: flex; align-items: center; justify-self: end; min-width: 0; gap: 10px; }
|
||||
.titlebar.has-global-search .titlebar-actions { grid-column: 3; }
|
||||
.titlebar-context-selectors { display: flex; align-items: center; min-width: 0; gap: 12px; }
|
||||
.tenant-selector { height: 40px; min-width: 210px; border: var(--border-line); border-radius: var(--radius-sm); background: var(--surface); display: flex; align-items: center; padding: 0 12px; gap: 5px; box-shadow: var(--shadow-xs); }
|
||||
.tenant-label, .tenant-caret, .muted { color: var(--muted); }
|
||||
@@ -139,6 +144,7 @@
|
||||
.code-panel { background: var(--code-bg); color: var(--code-text); padding: 18px; border-radius: 4px; overflow: auto; }
|
||||
@media (max-width: 900px) {
|
||||
.api-mini { display: none; }
|
||||
.titlebar.has-global-search { grid-template-columns: minmax(0, 1fr) 34px auto; }
|
||||
.workspace { grid-template-columns: 1fr; }
|
||||
.section-sidebar { display: none; }
|
||||
.wizard-card { grid-template-columns: 1fr; }
|
||||
|
||||
@@ -428,6 +428,7 @@ export type GlobalSearchProps = {
|
||||
|
||||
export type SearchRuntimeUiCapability = {
|
||||
GlobalSearch: ComponentType<GlobalSearchProps>;
|
||||
anyOf?: string[];
|
||||
};
|
||||
|
||||
export type DashboardWidgetSize = "small" | "medium" | "wide" | "full";
|
||||
|
||||
Reference in New Issue
Block a user