feat: add validated appearance palettes

This commit is contained in:
2026-08-20 06:26:52 +02:00
parent fd90b60430
commit 6643c8fc1e
11 changed files with 196 additions and 17 deletions
+52 -6
View File
@@ -2,7 +2,7 @@ import DescriptionList from "../../components/DescriptionList";
import ContentGrid, { FormGrid } from "../../components/ContentGrid";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "react-router";
import type { ApiSettings, AuthInfo, AuthUpdate, FilesConnectorsUiCapability, MailProfilesUiCapability, NavigationPreferences, SettingsSectionContribution, SettingsSectionsUiCapability, UserUiPreferences, UserUiTheme } from "../../types";
import type { ApiSettings, AuthInfo, AuthUpdate, FilesConnectorsUiCapability, MailProfilesUiCapability, NavigationPreferences, SettingsSectionContribution, SettingsSectionsUiCapability, UserUiPalette, UserUiPreferences, UserUiTheme } from "../../types";
import Card from "../../components/Card";
import FormField from "../../components/FormField";
import PasswordField from "../../components/PasswordField";
@@ -35,7 +35,8 @@ const DEFAULT_UI_PREFERENCES: UserUiPreferences = {
show_inline_help_hints: true,
reduce_motion: false,
sticky_section_sidebars: true,
theme: "system"
theme: "system",
palette: "default"
};
const UI_THEME_OPTIONS: Array<{ value: UserUiTheme; label: string }> = [
@@ -44,6 +45,13 @@ const UI_THEME_OPTIONS: Array<{ value: UserUiTheme; label: string }> = [
{ value: "dark", label: "i18n:govoplan-core.dark_theme.164a90d9" }
];
const UI_PALETTE_OPTIONS: Array<{ value: UserUiPalette; label: string }> = [
{ value: "default", label: "i18n:govoplan-core.palette_default" },
{ value: "civic_blue", label: "i18n:govoplan-core.palette_civic_blue" },
{ value: "forest", label: "i18n:govoplan-core.palette_forest" },
{ value: "plum", label: "i18n:govoplan-core.palette_plum" }
];
const SETTINGS_DOCUMENTATION = {
contextId: "core.settings",
documentationType: "user" as const
@@ -155,6 +163,7 @@ export default function SettingsPage({
const [reduceMotion, setReduceMotion] = useState(currentUiPreferences.reduce_motion);
const [stickySections, setStickySections] = useState(currentUiPreferences.sticky_section_sidebars);
const [theme, setTheme] = useState<UserUiTheme>(currentUiPreferences.theme);
const [palette, setPalette] = useState<UserUiPalette>(currentUiPreferences.palette);
const [navigation, setNavigation] = useState<NavigationPreferences | null>(currentUiPreferences.navigation ?? null);
const [uiBusy, setUiBusy] = useState(false);
const [uiResult, setUiResult] = useState("");
@@ -171,6 +180,7 @@ export default function SettingsPage({
reduceMotion !== currentUiPreferences.reduce_motion ||
stickySections !== currentUiPreferences.sticky_section_sidebars ||
theme !== currentUiPreferences.theme ||
palette !== currentUiPreferences.palette ||
JSON.stringify(navigation) !== JSON.stringify(currentUiPreferences.navigation ?? null);
useUnsavedDraftGuard({
@@ -225,6 +235,7 @@ export default function SettingsPage({
setReduceMotion(currentUiPreferences.reduce_motion);
setStickySections(currentUiPreferences.sticky_section_sidebars);
setTheme(currentUiPreferences.theme);
setPalette(currentUiPreferences.palette);
setNavigation(currentUiPreferences.navigation ?? null);
}, [
currentUiPreferences.compact_tables,
@@ -232,6 +243,7 @@ export default function SettingsPage({
currentUiPreferences.reduce_motion,
currentUiPreferences.sticky_section_sidebars,
currentUiPreferences.theme,
currentUiPreferences.palette,
currentUiPreferences.navigation
]);
@@ -271,6 +283,7 @@ export default function SettingsPage({
setReduceMotion(currentUiPreferences.reduce_motion);
setStickySections(currentUiPreferences.sticky_section_sidebars);
setTheme(currentUiPreferences.theme);
setPalette(currentUiPreferences.palette);
setNavigation(currentUiPreferences.navigation ?? null);
}
@@ -281,6 +294,7 @@ export default function SettingsPage({
reduce_motion: reduceMotion,
sticky_section_sidebars: stickySections,
theme,
palette,
navigation
};
}
@@ -473,15 +487,36 @@ export default function SettingsPage({
width="fill"
ariaLabel="i18n:govoplan-core.theme.a797e309" />
</FormField>
<ThemePreview theme={theme} />
<FormField
label="i18n:govoplan-core.color_palette"
help="i18n:govoplan-core.color_palette_help"
>
<select
value={palette}
onChange={(event) => setPalette(event.target.value as UserUiPalette)}
>
{UI_PALETTE_OPTIONS.map((item) => (
<option key={item.value} value={item.value}>{item.label}</option>
))}
</select>
</FormField>
<div className="button-row compact-actions">
<Button onClick={() => setPalette("default")} disabled={palette === "default"}>
i18n:govoplan-core.reset_palette
</Button>
</div>
<ThemePreview theme={theme} palette={palette} />
<DescriptionList variant="inline" density="compact">
<div><dt>i18n:govoplan-core.theme.a797e309</dt><dd>{themeLabel(theme)}</dd></div>
<div><dt>i18n:govoplan-core.accent_color.e49578ed</dt><dd>i18n:govoplan-core.default_brand_accent.606ae693</dd></div>
<div><dt>i18n:govoplan-core.accent_color.e49578ed</dt><dd>{paletteLabel(palette)}</dd></div>
<div><dt>i18n:govoplan-core.accessibility</dt><dd>i18n:govoplan-core.palette_contrast_validated</dd></div>
<div><dt>i18n:govoplan-core.advanced_theme_overrides</dt><dd>i18n:govoplan-core.not_configured</dd></div>
<div><dt>i18n:govoplan-core.language.89b86ab0</dt><dd>{languageLabel}</dd></div>
<div><dt>i18n:govoplan-core.enabled.df174a3f</dt><dd>{enabledLanguages.map((item) => item.code.toUpperCase()).join(", ")}</dd></div>
<div><dt>i18n:govoplan-core.available.7c62a142</dt><dd>{availableLanguages.map((item) => item.code.toUpperCase()).join(", ")}</dd></div>
<div><dt>i18n:govoplan-core.density.f9160c22</dt><dd>{compactTables ? "i18n:govoplan-core.compact_preview.3e06901d" : "i18n:govoplan-core.comfortable.2313707a"}</dd></div>
</DescriptionList>
<p className="muted small-note">i18n:govoplan-core.advanced_theme_overrides_follow_up</p>
</FormGrid>
</Card>
</ContentGrid>
@@ -616,20 +651,31 @@ function normalizeUiPreferences(value: Partial<UserUiPreferences> | null | undef
reduce_motion: Boolean(value?.reduce_motion ?? DEFAULT_UI_PREFERENCES.reduce_motion),
sticky_section_sidebars: Boolean(value?.sticky_section_sidebars ?? DEFAULT_UI_PREFERENCES.sticky_section_sidebars),
theme,
palette: normalizeUiPalette(value?.palette),
navigation: value?.navigation ?? null
};
}
function normalizeUiPalette(value: UserUiPalette | string | null | undefined): UserUiPalette {
return UI_PALETTE_OPTIONS.some((item) => item.value === value)
? value as UserUiPalette
: "default";
}
function themeLabel(value: UserUiTheme): string {
return UI_THEME_OPTIONS.find((item) => item.value === value)?.label ?? UI_THEME_OPTIONS[0].label;
}
function ThemePreview({ theme }: {theme: UserUiTheme;}) {
function paletteLabel(value: UserUiPalette): string {
return UI_PALETTE_OPTIONS.find((item) => item.value === value)?.label ?? UI_PALETTE_OPTIONS[0].label;
}
function ThemePreview({ theme, palette }: {theme: UserUiTheme;palette: UserUiPalette;}) {
const variants: UserUiTheme[] = theme === "system" ? ["light", "dark"] : [theme];
return (
<div className="theme-preview-list" aria-label="i18n:govoplan-core.theme.a797e309">
{variants.map((variant) =>
<div key={variant} className="theme-preview" data-preview-theme={variant}>
<div key={variant} className="theme-preview" data-preview-theme={variant} data-preview-palette={palette}>
<div className="theme-preview-header">
<span>{themeLabel(variant)}</span>
<i />