import { zodResolver } from '@hookform/resolvers/zod';
import {
changePasswordInputSchema,
DEFAULT_THEME_PRESET_ID,
deriveAccentTokens,
THEME_PRESETS,
updateProfileInputSchema,
} from '@dorfteich/shared';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../auth/auth-context';
import { Field, FormError, FormSuccess, applyFieldErrors } from '../components/forms';
import { SettingsLayout } from '../components/SettingsLayout';
import { useDataExport } from '../export/use-data-export';
import { apiDelete, apiGet, apiPatch, apiPost } from '../lib/api';
import { ApiTokensSection } from '../api-tokens/ApiTokensSection';
import { FeedTokensSection } from '../api-tokens/FeedTokensSection';
import { WatchesSection } from '../watches/WatchesSection';
import { useDocumentTitle } from '../lib/use-document-title';
import { SINGLE_KEY_SHORTCUTS_KEY } from '../lib/single-key-shortcuts';
import { usePersistentState } from '../lib/use-persistent-state';
import { AccentSwatches } from '../theme/AccentSwatches';
import { useAccentChoice } from '../theme/apply-theme';
import { useThemeMode, type ThemeMode } from '../theme/theme';
interface SessionView {
id: string;
createdAt: string;
lastSeenAt: string;
userAgent: string | null;
current: boolean;
}
export function SettingsPage(): React.JSX.Element {
const { t } = useTranslation();
useDocumentTitle(t('settings:title'));
return (
<>
{t('settings:title')}
>
);
}
/** Erscheinungsbild (issue #180): Hell/Dunkel/System — wie
* InteractionSection eine lokale Geräte-Einstellung, kein Server-Zustand. */
function AppearanceSection(): React.JSX.Element {
const { t } = useTranslation();
// Shared hook instead of usePersistentState: keeps the radios and the
// top-bar theme toggle (issue #182) in sync within the same document.
const [mode, setMode] = useThemeMode();
const options: { value: ThemeMode; label: string }[] = [
{ value: 'light', label: t('settings:appearance.light') },
{ value: 'dark', label: t('settings:appearance.dark') },
{ value: 'system', label: t('settings:appearance.system') },
];
return (
{t('settings:appearance.title')}
{t('settings:appearance.hint')}
);
}
/** Akzentfarbe (issue #184, ADR 0018 stage B): presets and a free color as
* ONE mechanism — both run through deriveAccentTokens, so any pick stays
* readable by construction. Same section as the mode radios (the jump-nav
* fence pins the section count). */
function AccentFieldset(): React.JSX.Element {
const { t } = useTranslation();
const [choice, setChoice] = useAccentChoice();
const isCustom = typeof choice === 'object';
// The color input keeps the last custom pick while a preset is selected,
// so re-selecting "custom" restores it instead of jumping to a default.
const [customHex, setCustomHex] = useState(isCustom ? choice.custom : '#2f6f4f');
const derivedPair = (accent: string): { light: string; dark: string } => ({
light: deriveAccentTokens(accent, 'light').accent,
dark: deriveAccentTokens(accent, 'dark').accent,
});
return (
);
}
/** Bedienungs-Einstellungen (issue #170, WCAG 2.1.4): Einzeltasten-Kürzel
* abschaltbar machen — lokale Geräte-Einstellung, kein Server-Zustand. */
function InteractionSection(): React.JSX.Element {
const { t } = useTranslation();
const [disabled, setDisabled] = usePersistentState(SINGLE_KEY_SHORTCUTS_KEY, false);
return (
{/* A table cannot shrink below its min-content width, so the user-agent
column pushed the whole page into horizontal scrolling at 320px
(issue #301, WCAG 1.4.10). It scrolls inside its own box instead —
the content stays reachable, which `overflow: hidden` would not. */}