diff --git a/decnet_web/src/components/Config/tabs/AppearanceTab.test.tsx b/decnet_web/src/components/Config/tabs/AppearanceTab.test.tsx new file mode 100644 index 00000000..da8a309a --- /dev/null +++ b/decnet_web/src/components/Config/tabs/AppearanceTab.test.tsx @@ -0,0 +1,35 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithRouter } from '../../../test/renderWithRouter'; +import { AppearanceTab } from './AppearanceTab'; + +beforeEach(() => { + localStorage.clear(); + document.documentElement.removeAttribute('data-accent'); +}); + +describe('AppearanceTab', () => { + it('starts with the matrix accent by default', () => { + renderWithRouter(); + expect(screen.getByText('● MATRIX')).toBeInTheDocument(); + expect(screen.getByText('○ VIOLET')).toBeInTheDocument(); + }); + + it('reads the saved accent from localStorage on mount', () => { + localStorage.setItem('decnet_tweaks', JSON.stringify({ accent: 'violet' })); + renderWithRouter(); + expect(screen.getByText('● VIOLET')).toBeInTheDocument(); + }); + + it('switching to violet writes localStorage + the data-accent attribute', async () => { + const user = userEvent.setup(); + renderWithRouter(); + await user.click(screen.getByText('○ VIOLET')); + + expect(screen.getByText('● VIOLET')).toBeInTheDocument(); + expect(document.documentElement.getAttribute('data-accent')).toBe('violet'); + const stored = JSON.parse(localStorage.getItem('decnet_tweaks') ?? '{}'); + expect(stored.accent).toBe('violet'); + }); +}); diff --git a/decnet_web/src/components/Config/tabs/AppearanceTab.tsx b/decnet_web/src/components/Config/tabs/AppearanceTab.tsx new file mode 100644 index 00000000..4144782c --- /dev/null +++ b/decnet_web/src/components/Config/tabs/AppearanceTab.tsx @@ -0,0 +1,78 @@ +import React, { useState } from 'react'; +import { useToast } from '../../Toasts/useToast'; + +const TWEAKS_KEY = 'decnet_tweaks'; + +export type Accent = 'matrix' | 'violet'; + +const loadInitialAccent = (): Accent => { + try { + const raw = localStorage.getItem(TWEAKS_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed?.accent === 'violet') return 'violet'; + } + } catch { /* noop */ } + return 'matrix'; +}; + +const persistAccent = (value: Accent): void => { + let existing: Record = {}; + try { + const raw = localStorage.getItem(TWEAKS_KEY); + if (raw) existing = JSON.parse(raw) ?? {}; + } catch { existing = {}; } + localStorage.setItem(TWEAKS_KEY, JSON.stringify({ ...existing, accent: value })); + document.documentElement.setAttribute('data-accent', value); +}; + +/** APPEARANCE tab — accent-color picker. State is owned here because + * no other Config tab cares about the choice; it persists to + * localStorage and flips the data-accent attribute on . */ +export const AppearanceTab: React.FC = () => { + const [accent, setAccent] = useState(loadInitialAccent); + const { push: pushToast } = useToast(); + + const handleAccentChange = (value: Accent) => { + setAccent(value); + persistAccent(value); + pushToast({ text: `ACCENT · ${value.toUpperCase()}`, icon: 'check-circle', tone: 'violet' }); + }; + + return ( + + + ACCENT COLOR + + Swaps the UI accent (nav bars, hover glows, chip borders) between matrix-green and electric-violet. Persists per-browser. + + + {(['matrix', 'violet'] as const).map((value) => ( + handleAccentChange(value)} + className="save-btn" + style={{ + padding: '8px 16px', + fontSize: '0.75rem', + letterSpacing: '1.5px', + borderColor: accent === value + ? (value === 'violet' ? 'var(--violet)' : 'var(--matrix)') + : 'var(--border)', + color: accent === value + ? (value === 'violet' ? 'var(--violet)' : 'var(--matrix)') + : 'var(--matrix)', + opacity: accent === value ? 1 : 0.6, + background: 'transparent', + }} + > + {accent === value ? '● ' : '○ '} + {value.toUpperCase()} + + ))} + + + + ); +};
+ Swaps the UI accent (nav bars, hover glows, chip borders) between matrix-green and electric-violet. Persists per-browser. +