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:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user