diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index f3ee83f..a15f40b 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -6,6 +6,7 @@
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
import * as LucideIcons from 'lucide-react';
const { ChevronDown, Lock } = LucideIcons;
import { cn } from '@/lib/utils';
@@ -321,6 +322,7 @@ function SidebarTopItem({ item, sectionName, currentPath, navigate, edition, onU
export function Sidebar() {
const navigate = useNavigate();
const location = useLocation();
+ const { t } = useTranslation();
const activeSection = useUIStore((s) => s.activeSection);
const setActiveSection = useUIStore((s) => s.setActiveSection);
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
@@ -369,6 +371,9 @@ export function Sidebar() {
if (first) navigate(`/${target.name}/${first}`);
};
+ const isAppearanceActive =
+ location.pathname === '/Appearance' || location.pathname.startsWith('/Appearance/');
+
return (
<>
+
+
+
+
{layouts.length > 1 && (
diff --git a/src/features/appearance/AppearancePage.tsx b/src/features/appearance/AppearancePage.tsx
new file mode 100644
index 0000000..e83e938
--- /dev/null
+++ b/src/features/appearance/AppearancePage.tsx
@@ -0,0 +1,129 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+import type { ReactNode } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Check, Moon, Sun } from 'lucide-react';
+
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { cn } from '@/lib/utils';
+import { COLOR_THEMES, useUIStore } from '@/stores/uiStore';
+
+export function AppearancePage() {
+ const { t } = useTranslation();
+ const theme = useUIStore((s) => s.theme);
+ const setTheme = useUIStore((s) => s.setTheme);
+ const colorTheme = useUIStore((s) => s.colorTheme);
+ const setColorTheme = useUIStore((s) => s.setColorTheme);
+ const radius = useUIStore((s) => s.radius);
+ const setRadius = useUIStore((s) => s.setRadius);
+
+ return (
+
+
+
{t('appearance.label', 'Appearance')}
+
+ {t('appearance.description', 'Customize how the interface looks and feels.')}
+
+
+
+
+
+ {t('appearance.mode', 'Mode')}
+ {t('appearance.modeDescription', 'Switch between light and dark mode.')}
+
+
+ setTheme('light')}
+ label={t('appearance.light', 'Light')}
+ >
+
+
+ setTheme('dark')} label={t('appearance.dark', 'Dark')}>
+
+
+
+
+
+
+
+ {t('appearance.colorTheme', 'Color theme')}
+
+ {t('appearance.colorThemeDescription', 'Pick the accent color used across the interface.')}
+
+
+
+ {COLOR_THEMES.map((entry) => (
+ setColorTheme(entry.value)}
+ label={t(entry.labelKey, entry.fallback)}
+ >
+
+
+ ))}
+
+
+
+
+
+ {t('appearance.corners', 'Corners')}
+
+ {t('appearance.cornersDescription', 'Choose between rounded and square corners. Applies to every theme.')}
+
+
+
+ setRadius('rounded')}
+ label={t('appearance.rounded', 'Rounded')}
+ >
+
+
+ setRadius('square')}
+ label={t('appearance.square', 'Square')}
+ >
+
+
+
+
+
+ );
+}
+
+function OptionCard({
+ selected,
+ onClick,
+ label,
+ children,
+}: {
+ selected: boolean;
+ onClick: () => void;
+ label: string;
+ children: ReactNode;
+}) {
+ return (
+
+ );
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d95925a..e515ab0 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -2,11 +2,15 @@
"accounts": "Accounts",
"appearance": {
"colorTheme": "Color theme",
+ "colorThemeDescription": "Pick the accent color used across the interface.",
"corners": "Corners",
+ "cornersDescription": "Choose between rounded and square corners. Applies to every theme.",
"dark": "Dark",
+ "description": "Customize how the interface looks and feels.",
"label": "Appearance",
"light": "Light",
"mode": "Mode",
+ "modeDescription": "Switch between light and dark mode.",
"rounded": "Rounded",
"square": "Square",
"switchToDark": "Switch to dark mode",
diff --git a/src/pages/AdminPanel.tsx b/src/pages/AdminPanel.tsx
index dfae5a3..c972b9b 100644
--- a/src/pages/AdminPanel.tsx
+++ b/src/pages/AdminPanel.tsx
@@ -21,6 +21,7 @@ import { ErrorBoundary } from '@/components/layout/ErrorBoundary';
import { LoadingFallback } from '@/components/common/LoadingFallback';
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
import { friendlyName } from '@/hooks/useGlobalSearch';
+import { AppearancePage } from '@/features/appearance/AppearancePage';
import {
findFirstAccessibleLinkInLayout,
findFirstVisibleLinkInLayout,
@@ -81,6 +82,7 @@ export default function AdminPanel() {
const setSession = useAuthStore((s) => s.setSession);
const accessToken = useAuthStore((s) => s.accessToken);
const setActiveSection = useUIStore((s) => s.setActiveSection);
+ const activeSection = useUIStore((s) => s.activeSection);
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
const { canViewObject } = usePermissions();
@@ -92,6 +94,7 @@ export default function AdminPanel() {
// Tab title mirrors the sidebar label (search index) so it always matches
// what the user sees in the navigation, e.g. "MTA-STS ยท Settings".
const pageTitle = useMemo(() => {
+ if (section === 'Appearance') return t('appearance.label', 'Appearance');
if (!section) return t('dashboard.title', 'Dashboard');
if (!viewName) return section;
const entry =
@@ -207,6 +210,15 @@ export default function AdminPanel() {
}
}
+ if (section === 'Appearance') {
+ // Appearance is a client-side page outside the schema layouts: keep the
+ // sidebar bound to a real section while it is open.
+ if (!layouts.some((l) => l.name === activeSection)) {
+ setActiveSection(layouts[0]?.name ?? '');
+ }
+ return;
+ }
+
if (section) {
setActiveSection(section);
return;
@@ -222,6 +234,7 @@ export default function AdminPanel() {
}, [
section,
viewName,
+ activeSection,
schema,
setActiveSection,
navigate,
@@ -284,7 +297,11 @@ export default function AdminPanel() {
-
+ {section === 'Appearance' ? (
+
+ ) : (
+
+ )}