ui(env): explicit switcher button+modal, forced selection, 3px color bar
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m6s
CI / docker (push) Successful in 1m18s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s

- Replace EnvironmentSelector "All Envs" dropdown with Button+Modal (DS Modal, forced on first-use).
- Add 8-swatch preset color picker in the Environment settings "Appearance" section; commits via useUpdateEnvironment.
- Render a 3px fixed top bar in the current env's color across every page (z-index 900, below DS modals).
- New env-colors tokens (--env-color-*, light + dark) and envColorVar() helper with slate fallback.
- Vitest coverage for button, modal, and color helpers (13 new specs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 19:24:48 +02:00
parent 79fa4c097c
commit 2835d08418
16 changed files with 593 additions and 38 deletions

View File

@@ -39,7 +39,9 @@ import { useEnvironmentStore } from '../api/environment-store';
import { useState, useMemo, useCallback, useEffect, useRef, createElement } from 'react';
import type { ReactNode } from 'react';
import { ContentTabs } from './ContentTabs';
import { EnvironmentSelector } from './EnvironmentSelector';
import { EnvironmentSwitcherButton } from './EnvironmentSwitcherButton';
import { EnvironmentSwitcherModal } from './EnvironmentSwitcherModal';
import { envColorVar } from './env-colors';
import { useScope } from '../hooks/useScope';
import { formatDuration } from '../utils/format-utils';
import {
@@ -428,6 +430,25 @@ function LayoutContent() {
queryClient.invalidateQueries();
}, [setSelectedEnvRaw, navigate, location.pathname, location.search, queryClient]);
// --- Env switcher modal -------------------------------------------
const [switcherOpen, setSwitcherOpen] = useState(false);
// Force-open the switcher when we have envs loaded but no valid selection.
// This replaces the old "All Envs" fallback: every session must pick one.
const selectionInvalid =
envRecords.length > 0 &&
(selectedEnv === undefined || !envRecords.some((e) => e.slug === selectedEnv));
const switcherForced = selectionInvalid;
useEffect(() => {
if (selectionInvalid) {
if (selectedEnv !== undefined) setSelectedEnvRaw(undefined);
setSwitcherOpen(true);
}
}, [selectionInvalid, selectedEnv, setSelectedEnvRaw]);
const currentEnvRecord = envRecords.find((e) => e.slug === selectedEnv);
const envBarColor = envColorVar(currentEnvRecord?.color);
// --- Section open states ------------------------------------------
const [appsOpen, setAppsOpen] = useState(() => (isAdminPage || isAlertsPage) ? false : readCollapsed(SK_APPS, true));
const [adminOpen, setAdminOpen] = useState(() => isAdminPage ? true : readCollapsed(SK_ADMIN, false));
@@ -954,14 +975,38 @@ function LayoutContent() {
return (
<AppShell sidebar={sidebarElement}>
<div
aria-hidden
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: 3,
background: envBarColor,
zIndex: 900,
pointerEvents: 'none',
}}
/>
<EnvironmentSwitcherModal
open={switcherOpen}
onClose={() => setSwitcherOpen(false)}
envs={envRecords}
value={selectedEnv}
onPick={(slug) => {
setSelectedEnv(slug);
setSwitcherOpen(false);
}}
forced={switcherForced}
/>
<TopBar
breadcrumb={breadcrumb}
environment={
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<EnvironmentSelector
environments={environments}
<EnvironmentSwitcherButton
envs={envRecords}
value={selectedEnv}
onChange={setSelectedEnv}
onClick={() => setSwitcherOpen(true)}
/>
<NotificationBell />
</div>