feat: govern accessible appearance overrides

This commit is contained in:
2026-08-20 10:50:55 +02:00
parent 0fae09ba3c
commit f11c675d11
14 changed files with 796 additions and 19 deletions
+20 -4
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, UserUiPalette, UserUiPreferences, UserUiTheme } from "../../types";
import type { AppearanceOverridesDocument, 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";
@@ -28,6 +28,7 @@ import CredentialEnvelopeManager from "../../components/CredentialEnvelopeManage
import DocumentationHelpLink from "../../components/help/DocumentationHelpLink";
import WorkspaceLayout from "../../components/WorkspaceLayout";
import { AppearancePalettePreview, AppearancePaletteSelect, APPEARANCE_PALETTE_OPTIONS, appearancePaletteLabel } from "../../components/AppearancePaletteControl";
import AppearanceOverridesEditor from "../../components/AppearanceOverridesEditor";
type SettingsSection = "profile" | "mail-profiles" | "file-connectors" | "interface" | "workspace" | "local-connection" | string;
@@ -37,7 +38,8 @@ const DEFAULT_UI_PREFERENCES: UserUiPreferences = {
reduce_motion: false,
sticky_section_sidebars: true,
theme: "system",
palette: null
palette: null,
appearance_overrides: null
};
const UI_THEME_OPTIONS: Array<{ value: UserUiTheme; label: string }> = [
@@ -158,6 +160,7 @@ export default function SettingsPage({
const [stickySections, setStickySections] = useState(currentUiPreferences.sticky_section_sidebars);
const [theme, setTheme] = useState<UserUiTheme>(currentUiPreferences.theme);
const [palette, setPalette] = useState<UserUiPalette | null>(currentUiPreferences.palette);
const [appearanceOverrides, setAppearanceOverrides] = useState<AppearanceOverridesDocument | null>(currentUiPreferences.appearance_overrides ?? null);
const [navigation, setNavigation] = useState<NavigationPreferences | null>(currentUiPreferences.navigation ?? null);
const [uiBusy, setUiBusy] = useState(false);
const [uiResult, setUiResult] = useState("");
@@ -175,6 +178,7 @@ export default function SettingsPage({
stickySections !== currentUiPreferences.sticky_section_sidebars ||
theme !== currentUiPreferences.theme ||
palette !== currentUiPreferences.palette ||
JSON.stringify(appearanceOverrides) !== JSON.stringify(currentUiPreferences.appearance_overrides ?? null) ||
JSON.stringify(navigation) !== JSON.stringify(currentUiPreferences.navigation ?? null);
useUnsavedDraftGuard({
@@ -230,6 +234,7 @@ export default function SettingsPage({
setStickySections(currentUiPreferences.sticky_section_sidebars);
setTheme(currentUiPreferences.theme);
setPalette(currentUiPreferences.palette);
setAppearanceOverrides(currentUiPreferences.appearance_overrides ?? null);
setNavigation(currentUiPreferences.navigation ?? null);
}, [
currentUiPreferences.compact_tables,
@@ -238,6 +243,7 @@ export default function SettingsPage({
currentUiPreferences.sticky_section_sidebars,
currentUiPreferences.theme,
currentUiPreferences.palette,
currentUiPreferences.appearance_overrides,
currentUiPreferences.navigation
]);
@@ -278,6 +284,7 @@ export default function SettingsPage({
setStickySections(currentUiPreferences.sticky_section_sidebars);
setTheme(currentUiPreferences.theme);
setPalette(currentUiPreferences.palette);
setAppearanceOverrides(currentUiPreferences.appearance_overrides ?? null);
setNavigation(currentUiPreferences.navigation ?? null);
}
@@ -289,6 +296,7 @@ export default function SettingsPage({
sticky_section_sidebars: stickySections,
theme,
palette,
appearance_overrides: appearanceOverrides,
navigation
};
}
@@ -498,13 +506,20 @@ export default function SettingsPage({
<div><dt>i18n:govoplan-core.accent_color.e49578ed</dt><dd>{paletteLabel(palette ?? auth.user.appearance?.inherited_palette ?? "default")}</dd></div>
<div><dt>i18n:govoplan-core.effective_source</dt><dd>{appearanceSourceLabel(auth.user.appearance?.source)}</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.advanced_theme_overrides</dt><dd>{appearanceOverrides ? "i18n:govoplan-core.configured" : "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>
<AppearanceOverridesEditor
value={appearanceOverrides}
onChange={setAppearanceOverrides}
theme={theme}
disabled={auth.user.appearance?.custom_overrides_allowed !== true}
/>
{auth.user.appearance?.custom_overrides_allowed !== true &&
<p className="muted small-note">i18n:govoplan-core.appearance_override_policy_disabled</p>}
</FormGrid>
</Card>
</ContentGrid>
@@ -640,6 +655,7 @@ function normalizeUiPreferences(value: Partial<UserUiPreferences> | null | undef
sticky_section_sidebars: Boolean(value?.sticky_section_sidebars ?? DEFAULT_UI_PREFERENCES.sticky_section_sidebars),
theme,
palette: normalizeOptionalUiPalette(value?.palette),
appearance_overrides: value?.appearance_overrides ?? null,
navigation: value?.navigation ?? null
};
}