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 ViewSelector = viewsRuntime?.Selector ?? null;
|
||||||
const searchRuntime = usePlatformUiCapability<SearchRuntimeUiCapability>("search.runtime");
|
const searchRuntime = usePlatformUiCapability<SearchRuntimeUiCapability>("search.runtime");
|
||||||
const GlobalSearch = searchRuntime?.GlobalSearch ?? null;
|
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 activeTenant = auth?.active_tenant ?? auth?.tenant ?? null;
|
||||||
const tenants = auth?.tenants ?? (activeTenant ? [activeTenant] : []);
|
const tenants = auth?.tenants ?? (activeTenant ? [activeTenant] : []);
|
||||||
@@ -138,7 +146,7 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="titlebar">
|
<header className={`titlebar${showGlobalSearch ? " has-global-search" : ""}`}>
|
||||||
{!backendReachable ?
|
{!backendReachable ?
|
||||||
<div
|
<div
|
||||||
className="backend-offline-topbar-alert"
|
className="backend-offline-topbar-alert"
|
||||||
@@ -158,94 +166,98 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
{auth && showContextSelectors &&
|
<div className="titlebar-leading">
|
||||||
<div className="titlebar-context-selectors">
|
{auth && showContextSelectors &&
|
||||||
{activeTenant && showTenantControl &&
|
<div className="titlebar-context-selectors">
|
||||||
<div className="tenant-selector" ref={tenantRef}>
|
{activeTenant && showTenantControl &&
|
||||||
<span className="tenant-label">{translateText("i18n:govoplan-core.tenant_label_prefix")}</span>
|
<div className="tenant-selector" ref={tenantRef}>
|
||||||
{canSwitchTenant ?
|
<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 ?
|
|
||||||
<>
|
<>
|
||||||
{auth.user &&
|
<button className="tenant-name-button" onClick={() => setTenantOpen(!tenantOpen)}>
|
||||||
<div className="account-menu-header">
|
<strong>{activeTenant.name}</strong>
|
||||||
<strong>{auth.user.display_name || auth.user.email}</strong>
|
<span className="tenant-caret">▾</span>
|
||||||
<span>{auth.user.email}</span>
|
</button>
|
||||||
</div>
|
{tenantOpen &&
|
||||||
}
|
<div className="dropdown-menu tenant-menu">
|
||||||
<button className="dropdown-item" onClick={handleAccountSettings}><Settings size={16} /> {translateText("i18n:govoplan-core.account_settings.82cf8a5f")}</button>
|
{tenants.map((tenant) => {
|
||||||
<button className="dropdown-item" onClick={handleLogout}><LogOut size={16} /> {translateText("i18n:govoplan-core.sign_out.dc1649a1")}</button>
|
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>
|
</div>
|
||||||
|
}
|
||||||
|
{ViewSelector &&
|
||||||
|
<ViewSelector settings={settings} auth={auth} projection={projection} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</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} />}
|
{loginOpen && <LoginModal settings={settings} onClose={() => setLoginOpen(false)} onLogin={handleLogin} />}
|
||||||
</header>);
|
</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-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; }
|
.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); }
|
.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; }
|
.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-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); }
|
.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; }
|
.code-panel { background: var(--code-bg); color: var(--code-text); padding: 18px; border-radius: 4px; overflow: auto; }
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.api-mini { display: none; }
|
.api-mini { display: none; }
|
||||||
|
.titlebar.has-global-search { grid-template-columns: minmax(0, 1fr) 34px auto; }
|
||||||
.workspace { grid-template-columns: 1fr; }
|
.workspace { grid-template-columns: 1fr; }
|
||||||
.section-sidebar { display: none; }
|
.section-sidebar { display: none; }
|
||||||
.wizard-card { grid-template-columns: 1fr; }
|
.wizard-card { grid-template-columns: 1fr; }
|
||||||
|
|||||||
@@ -428,6 +428,7 @@ export type GlobalSearchProps = {
|
|||||||
|
|
||||||
export type SearchRuntimeUiCapability = {
|
export type SearchRuntimeUiCapability = {
|
||||||
GlobalSearch: ComponentType<GlobalSearchProps>;
|
GlobalSearch: ComponentType<GlobalSearchProps>;
|
||||||
|
anyOf?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardWidgetSize = "small" | "medium" | "wide" | "full";
|
export type DashboardWidgetSize = "small" | "medium" | "wide" | "full";
|
||||||
|
|||||||
Reference in New Issue
Block a user