feat: add validated appearance palettes
This commit is contained in:
+14
-4
@@ -3,7 +3,7 @@ import { lazy, useEffect, useMemo, useState } from "react";
|
||||
import { fetchSession, fetchShellAuth, updateProfile } from "./api/auth";
|
||||
import { fetchPlatformModules, fetchPlatformPublicModules, fetchPlatformStatus } from "./api/platform";
|
||||
import { AUTH_REQUIRED_EVENT, isApiError, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client";
|
||||
import type { ApiSettings, AuthInfo, AuthSessionInfo, AuthUpdate, AuthUser, EffectiveViewProjection, LoginResponse, PlatformModuleInfo, PlatformPublicModuleInfo, PlatformWebModule, UserUiPreferences, ViewsRuntimeUiCapability } from "./types";
|
||||
import type { ApiSettings, AuthInfo, AuthSessionInfo, AuthUpdate, AuthUser, EffectiveViewProjection, LoginResponse, PlatformModuleInfo, PlatformPublicModuleInfo, PlatformWebModule, UserUiPalette, UserUiPreferences, ViewsRuntimeUiCapability } from "./types";
|
||||
import AppShell from "./layout/AppShell";
|
||||
import PublicLandingPage from "./features/auth/PublicLandingPage";
|
||||
import LoginModal from "./features/auth/LoginModal";
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
@@ -389,6 +390,7 @@ export default function App() {
|
||||
root.classList.toggle("ui-hide-help-hints", !preferences.show_inline_help_hints);
|
||||
root.classList.toggle("ui-reduce-motion", preferences.reduce_motion);
|
||||
root.classList.toggle("ui-no-sticky-section-sidebars", !preferences.sticky_section_sidebars);
|
||||
root.dataset.palette = normalizeUiPalette(preferences.palette);
|
||||
|
||||
const systemDarkQuery = window.matchMedia?.("(prefers-color-scheme: dark)") ?? null;
|
||||
const applyTheme = () => {
|
||||
@@ -413,7 +415,8 @@ export default function App() {
|
||||
auth?.user.ui_preferences?.show_inline_help_hints,
|
||||
auth?.user.ui_preferences?.reduce_motion,
|
||||
auth?.user.ui_preferences?.sticky_section_sidebars,
|
||||
auth?.user.ui_preferences?.theme
|
||||
auth?.user.ui_preferences?.theme,
|
||||
auth?.user.ui_preferences?.palette
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -716,10 +719,17 @@ function normalizeUiPreferences(value: Partial<UserUiPreferences> | null | undef
|
||||
show_inline_help_hints: Boolean(value?.show_inline_help_hints ?? DEFAULT_UI_PREFERENCES.show_inline_help_hints),
|
||||
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
|
||||
theme,
|
||||
palette: normalizeUiPalette(value?.palette)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeUiPalette(value: UserUiPalette | string | null | undefined): UserUiPalette {
|
||||
return value === "civic_blue" || value === "forest" || value === "plum"
|
||||
? value
|
||||
: "default";
|
||||
}
|
||||
|
||||
function mergeWebModules(localModules: PlatformWebModule[], remoteModules: PlatformWebModule[]): PlatformWebModule[] {
|
||||
if (remoteModules.length === 0) return localModules;
|
||||
const seen = new Set(localModules.map((module) => module.id));
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -2,6 +2,18 @@ import type { PlatformTranslations } from "../types";
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
"en": {
|
||||
"i18n:govoplan-core.color_palette": "Color palette",
|
||||
"i18n:govoplan-core.color_palette_help": "Choose a validated accent palette. It applies to every module through shared semantic tokens.",
|
||||
"i18n:govoplan-core.palette_default": "GovOPlaN default",
|
||||
"i18n:govoplan-core.palette_civic_blue": "Civic blue",
|
||||
"i18n:govoplan-core.palette_forest": "Forest",
|
||||
"i18n:govoplan-core.palette_plum": "Plum",
|
||||
"i18n:govoplan-core.reset_palette": "Reset palette",
|
||||
"i18n:govoplan-core.accessibility": "Accessibility",
|
||||
"i18n:govoplan-core.palette_contrast_validated": "Validated preset contrast",
|
||||
"i18n:govoplan-core.advanced_theme_overrides": "Advanced custom overrides",
|
||||
"i18n:govoplan-core.not_configured": "Not configured",
|
||||
"i18n:govoplan-core.advanced_theme_overrides_follow_up": "Arbitrary token overrides require separate tenant branding policy and accessibility safeguards.",
|
||||
"i18n:govoplan-core.standard_folder_mappings": "Standard folder mappings",
|
||||
"i18n:govoplan-core.standard_folder_mappings_help": "Map each standard mailbox role to a folder exposed by this IMAP account. Leave a field empty to use automatic detection.",
|
||||
"i18n:govoplan-core.inbox_folder": "Inbox folder",
|
||||
@@ -689,6 +701,18 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-core.temporal_selection_invalid": "The selected data state is invalid."
|
||||
},
|
||||
"de": {
|
||||
"i18n:govoplan-core.color_palette": "Farbpalette",
|
||||
"i18n:govoplan-core.color_palette_help": "Wählen Sie eine geprüfte Akzentpalette. Sie gilt über gemeinsame semantische Tokens für alle Module.",
|
||||
"i18n:govoplan-core.palette_default": "GovOPlaN-Standard",
|
||||
"i18n:govoplan-core.palette_civic_blue": "Kommunalblau",
|
||||
"i18n:govoplan-core.palette_forest": "Waldgrün",
|
||||
"i18n:govoplan-core.palette_plum": "Pflaume",
|
||||
"i18n:govoplan-core.reset_palette": "Palette zurücksetzen",
|
||||
"i18n:govoplan-core.accessibility": "Barrierefreiheit",
|
||||
"i18n:govoplan-core.palette_contrast_validated": "Geprüfter Kontrast der Vorgabe",
|
||||
"i18n:govoplan-core.advanced_theme_overrides": "Erweiterte benutzerdefinierte Anpassungen",
|
||||
"i18n:govoplan-core.not_configured": "Nicht konfiguriert",
|
||||
"i18n:govoplan-core.advanced_theme_overrides_follow_up": "Beliebige Token-Anpassungen erfordern eine getrennte Mandanten-Branding-Richtlinie und Barrierefreiheitsprüfungen.",
|
||||
"i18n:govoplan-core.standard_folder_mappings": "Zuordnung der Standardordner",
|
||||
"i18n:govoplan-core.standard_folder_mappings_help": "Ordnen Sie jede Standardfunktion einem Ordner dieses IMAP-Kontos zu. Lassen Sie ein Feld leer, um die automatische Erkennung zu verwenden.",
|
||||
"i18n:govoplan-core.inbox_folder": "Posteingang",
|
||||
|
||||
@@ -3231,6 +3231,7 @@
|
||||
--preview-line: var(--theme-preview-light-line);
|
||||
--preview-text: var(--theme-preview-light-text);
|
||||
--preview-muted: var(--theme-preview-light-muted);
|
||||
--preview-accent: var(--theme-preview-accent-default);
|
||||
display: grid;
|
||||
gap: 0;
|
||||
min-height: 112px;
|
||||
@@ -3240,6 +3241,10 @@
|
||||
background: var(--preview-bg);
|
||||
}
|
||||
|
||||
.theme-preview[data-preview-palette="civic_blue"] { --preview-accent: var(--theme-preview-accent-civic-blue); }
|
||||
.theme-preview[data-preview-palette="forest"] { --preview-accent: var(--theme-preview-accent-forest); }
|
||||
.theme-preview[data-preview-palette="plum"] { --preview-accent: var(--theme-preview-accent-plum); }
|
||||
|
||||
.theme-preview[data-preview-theme="dark"] {
|
||||
--preview-bg: var(--theme-preview-dark-bg);
|
||||
--preview-bar: var(--theme-preview-dark-bar);
|
||||
@@ -3266,8 +3271,8 @@
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: var(--radius-round);
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||
background: var(--preview-accent);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--preview-accent) 35%, transparent);
|
||||
}
|
||||
|
||||
.theme-preview-body {
|
||||
|
||||
@@ -242,6 +242,10 @@
|
||||
--theme-preview-dark-line: #454740;
|
||||
--theme-preview-dark-text: #f6f4ed;
|
||||
--theme-preview-dark-muted: #aaa79d;
|
||||
--theme-preview-accent-default: #ef6b3a;
|
||||
--theme-preview-accent-civic-blue: #245f91;
|
||||
--theme-preview-accent-forest: #276749;
|
||||
--theme-preview-accent-plum: #704b78;
|
||||
|
||||
--calendar-event-bg-default: #edf8f5;
|
||||
--calendar-event-border-default: #b9d7d0;
|
||||
@@ -429,6 +433,36 @@
|
||||
--campaign-panel-bg: rgba(38, 39, 36, .94);
|
||||
}
|
||||
|
||||
:root[data-palette="civic_blue"] {
|
||||
--accent: #245f91;
|
||||
--accent-rgb: 36, 95, 145;
|
||||
--accent-soft: rgba(36, 95, 145, .35);
|
||||
--accent-hover-bg: rgba(36, 95, 145, .14);
|
||||
--accent-auth-glow: rgba(36, 95, 145, .16);
|
||||
--accent-ring-soft: 0 0 0 2px rgba(36, 95, 145, .20);
|
||||
--badge-accent-text: #ffffff;
|
||||
}
|
||||
|
||||
:root[data-palette="forest"] {
|
||||
--accent: #276749;
|
||||
--accent-rgb: 39, 103, 73;
|
||||
--accent-soft: rgba(39, 103, 73, .35);
|
||||
--accent-hover-bg: rgba(39, 103, 73, .14);
|
||||
--accent-auth-glow: rgba(39, 103, 73, .16);
|
||||
--accent-ring-soft: 0 0 0 2px rgba(39, 103, 73, .20);
|
||||
--badge-accent-text: #ffffff;
|
||||
}
|
||||
|
||||
:root[data-palette="plum"] {
|
||||
--accent: #704b78;
|
||||
--accent-rgb: 112, 75, 120;
|
||||
--accent-soft: rgba(112, 75, 120, .35);
|
||||
--accent-hover-bg: rgba(112, 75, 120, .14);
|
||||
--accent-auth-glow: rgba(112, 75, 120, .16);
|
||||
--accent-ring-soft: 0 0 0 2px rgba(112, 75, 120, .20);
|
||||
--badge-accent-text: #ffffff;
|
||||
}
|
||||
|
||||
.ui-reduce-motion *,
|
||||
.ui-reduce-motion *::before,
|
||||
.ui-reduce-motion *::after {
|
||||
|
||||
@@ -41,6 +41,7 @@ export type AuthUser = {
|
||||
};
|
||||
|
||||
export type UserUiTheme = "system" | "light" | "dark";
|
||||
export type UserUiPalette = "default" | "civic_blue" | "forest" | "plum";
|
||||
|
||||
export type UserUiPreferences = {
|
||||
compact_tables: boolean;
|
||||
@@ -48,6 +49,7 @@ export type UserUiPreferences = {
|
||||
reduce_motion: boolean;
|
||||
sticky_section_sidebars: boolean;
|
||||
theme: UserUiTheme;
|
||||
palette: UserUiPalette;
|
||||
navigation?: NavigationPreferences | null;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user