ui(env): explicit switcher button+modal, forced selection, 3px color bar
- 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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user