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

@@ -23,6 +23,8 @@ import {
useUpdateJarRetention,
} from '../../api/queries/admin/environments';
import type { Environment } from '../../api/queries/admin/environments';
import { ENV_COLORS, envColorVar, type EnvColor } from '../../components/env-colors';
import { Check } from 'lucide-react';
import { PageLoader } from '../../components/PageLoader';
import styles from './UserManagement.module.css';
import sectionStyles from '../../styles/section-card.module.css';
@@ -120,6 +122,7 @@ export default function EnvironmentsPage() {
displayName: newName,
production: selected.production,
enabled: selected.enabled,
color: selected.color,
});
toast({ title: 'Environment renamed', variant: 'success' });
} catch {
@@ -135,6 +138,7 @@ export default function EnvironmentsPage() {
displayName: selected.displayName,
production: value,
enabled: selected.enabled,
color: selected.color,
});
toast({ title: value ? 'Marked as production' : 'Marked as non-production', variant: 'success' });
} catch {
@@ -150,6 +154,7 @@ export default function EnvironmentsPage() {
displayName: selected.displayName,
production: selected.production,
enabled: value,
color: selected.color,
});
toast({ title: value ? 'Environment enabled' : 'Environment disabled', variant: 'success' });
} catch {
@@ -157,6 +162,22 @@ export default function EnvironmentsPage() {
}
}
async function handleColorChange(color: EnvColor) {
if (!selected || selected.color === color) return;
try {
await updateEnv.mutateAsync({
slug: selected.slug,
displayName: selected.displayName,
production: selected.production,
enabled: selected.enabled,
color,
});
toast({ title: 'Environment color updated', variant: 'success' });
} catch {
toast({ title: 'Failed to update color', variant: 'error', duration: 86_400_000 });
}
}
if (isLoading) return <PageLoader />;
return (
@@ -279,6 +300,44 @@ export default function EnvironmentsPage() {
</div>
</div>
<div className={sectionStyles.section}>
<SectionHeader>Appearance</SectionHeader>
<p className={styles.inheritedNote}>
This color is shown as a 3px bar across every page while this environment is active.
</p>
<div role="radiogroup" aria-label="Environment color" style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
{ENV_COLORS.map((c) => {
const isSelected = selected.color === c;
return (
<button
key={c}
type="button"
role="radio"
aria-checked={isSelected}
aria-label={c}
title={c}
onClick={() => handleColorChange(c)}
style={{
width: 36,
height: 36,
borderRadius: '50%',
background: envColorVar(c),
border: isSelected ? '2px solid var(--text-primary)' : '1px solid var(--border-subtle)',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
padding: 0,
}}
>
{isSelected && <Check size={16} aria-hidden />}
</button>
);
})}
</div>
</div>
<div className={sectionStyles.section}>
<SectionHeader>Status</SectionHeader>
<div className={styles.securitySection}>