feat(settings): Akzentfarbe zusätzlich als Hex-Code eingebbar
- normalizeHexColor() (apps/web/src/lib/color.ts) normalisiert Hex-Eingaben (fuehrendes # optional, 3-stellige Kurzform, Gross-/Kleinschreibung, Leerraum) - Kontoformular: Hex-Textfeld neben dem Farbwaehler, beide bidirektional synchron, ersetzt den reinen Anzeige-Span - Ungueltiger Text: aria-invalid + roter Rand + Fehlertext, "Farbe speichern" gesperrt; Zuruecksetzen setzt beides auf #ffed00 - i18n: settings.account.accentColorHex/accentColorHexInvalid in de.json und en.json - Unit-Tests: color.test.ts (9 Faelle), account-settings-form.test.tsx (7 Faelle) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
} from '@/lib/auth-actions';
|
||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||
import { BRAND_YELLOW } from '@/components/brand/brand';
|
||||
import { normalizeHexColor } from '@/lib/color';
|
||||
|
||||
const DEFAULT_ACCENT = BRAND_YELLOW;
|
||||
|
||||
@@ -38,6 +39,8 @@ export function AccountSettingsForm() {
|
||||
|
||||
// Accent color state
|
||||
const [accentColor, setAccentColor] = useState<string>(DEFAULT_ACCENT);
|
||||
const [hexInput, setHexInput] = useState<string>(DEFAULT_ACCENT);
|
||||
const isHexValid = normalizeHexColor(hexInput) !== null;
|
||||
const [accentSuccess, setAccentSuccess] = useState(false);
|
||||
const [accentError, setAccentError] = useState<string | null>(null);
|
||||
const [isAccentPending, startAccentTransition] = useTransition();
|
||||
@@ -48,7 +51,9 @@ export function AccountSettingsForm() {
|
||||
setIsLocalUser(u.isLocalUser ?? false);
|
||||
setUsername(u.displayName ?? u.username ?? '');
|
||||
setHasAvatar(!!u.hasAvatar);
|
||||
setAccentColor(u.accentColor ?? DEFAULT_ACCENT);
|
||||
const c = u.accentColor ?? DEFAULT_ACCENT;
|
||||
setAccentColor(c);
|
||||
setHexInput(c);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
@@ -113,6 +118,7 @@ export function AccountSettingsForm() {
|
||||
}
|
||||
|
||||
function handleSaveAccentColor() {
|
||||
if (!isHexValid) return;
|
||||
setAccentSuccess(false);
|
||||
setAccentError(null);
|
||||
startAccentTransition(async () => {
|
||||
@@ -128,6 +134,7 @@ export function AccountSettingsForm() {
|
||||
|
||||
function handleResetAccentColor() {
|
||||
setAccentColor(DEFAULT_ACCENT);
|
||||
setHexInput(DEFAULT_ACCENT);
|
||||
setAccentSuccess(false);
|
||||
setAccentError(null);
|
||||
startAccentTransition(async () => {
|
||||
@@ -212,14 +219,41 @@ export function AccountSettingsForm() {
|
||||
{t('account.accentColorLabel')}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground mb-4">{t('account.accentColorHelp')}</p>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<input
|
||||
type="color"
|
||||
value={accentColor}
|
||||
onChange={(e) => setAccentColor(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setAccentColor(e.target.value);
|
||||
setHexInput(e.target.value);
|
||||
}}
|
||||
className="h-10 w-16 cursor-pointer rounded-md border border-input p-1"
|
||||
/>
|
||||
<span className="font-mono text-sm text-muted-foreground">{accentColor}</span>
|
||||
<input
|
||||
id="accentColorHex"
|
||||
type="text"
|
||||
inputMode="text"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
maxLength={7}
|
||||
placeholder={DEFAULT_ACCENT}
|
||||
value={hexInput}
|
||||
aria-label={t('account.accentColorHex')}
|
||||
aria-invalid={!isHexValid}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
setHexInput(raw);
|
||||
const n = normalizeHexColor(raw);
|
||||
if (n) setAccentColor(n);
|
||||
}}
|
||||
onBlur={() => {
|
||||
const n = normalizeHexColor(hexInput);
|
||||
if (n) setHexInput(n);
|
||||
}}
|
||||
className={`h-10 w-28 rounded-md border bg-background px-3 py-2 font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
||||
isHexValid ? 'border-input' : 'border-destructive'
|
||||
}`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetAccentColor}
|
||||
@@ -229,6 +263,9 @@ export function AccountSettingsForm() {
|
||||
{t('account.accentColorReset')}
|
||||
</button>
|
||||
</div>
|
||||
{!isHexValid && (
|
||||
<p className="text-xs text-destructive mb-3">{t('account.accentColorHexInvalid')}</p>
|
||||
)}
|
||||
{accentSuccess && (
|
||||
<div className="rounded-md bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 px-4 py-3 text-sm text-green-800 dark:text-green-200 mb-3">
|
||||
{t('account.accentColorSuccess')}
|
||||
@@ -242,7 +279,7 @@ export function AccountSettingsForm() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveAccentColor}
|
||||
disabled={isAccentPending}
|
||||
disabled={isAccentPending || !isHexValid}
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
>
|
||||
{isAccentPending ? '...' : t('account.accentColorSave')}
|
||||
|
||||
Reference in New Issue
Block a user