From 4a9cd90f90900d1d61572130d7296c49abd5549f Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 9 May 2026 05:26:26 -0400 Subject: [PATCH] refactor(decnet_web/Config): extract AppearanceTab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit APPEARANCE panel — accent-color picker — into its own tab. State is local since no other tab cares about the value; localStorage persistence + the document.documentElement[data-accent] mirror move along with it. - New Config/tabs/AppearanceTab.tsx - AppearanceTab.test.tsx covers the matrix default, reading the saved accent from localStorage on mount, and the click-to-flip flow writing both localStorage and the html data-accent attr. --- .../Config/tabs/AppearanceTab.test.tsx | 35 +++++++++ .../components/Config/tabs/AppearanceTab.tsx | 78 +++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 decnet_web/src/components/Config/tabs/AppearanceTab.test.tsx create mode 100644 decnet_web/src/components/Config/tabs/AppearanceTab.tsx 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) => ( + + ))} +
+
+
+ ); +};