refactor(decnet_web/Config): extract AppearanceTab

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.
This commit is contained in:
2026-05-09 05:26:26 -04:00
parent ccae1612bd
commit 4a9cd90f90
2 changed files with 113 additions and 0 deletions

View File

@@ -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(<AppearanceTab />);
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(<AppearanceTab />);
expect(screen.getByText('● VIOLET')).toBeInTheDocument();
});
it('switching to violet writes localStorage + the data-accent attribute', async () => {
const user = userEvent.setup();
renderWithRouter(<AppearanceTab />);
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');
});
});

View File

@@ -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<string, unknown> = {};
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 <html>. */
export const AppearanceTab: React.FC = () => {
const [accent, setAccent] = useState<Accent>(loadInitialAccent);
const { push: pushToast } = useToast();
const handleAccentChange = (value: Accent) => {
setAccent(value);
persistAccent(value);
pushToast({ text: `ACCENT · ${value.toUpperCase()}`, icon: 'check-circle', tone: 'violet' });
};
return (
<div className="config-panel">
<div className="config-field">
<span className="config-label">ACCENT COLOR</span>
<p style={{ fontSize: '0.75rem', opacity: 0.5, margin: '4px 0 12px' }}>
Swaps the UI accent (nav bars, hover glows, chip borders) between matrix-green and electric-violet. Persists per-browser.
</p>
<div style={{ display: 'flex', gap: '8px' }}>
{(['matrix', 'violet'] as const).map((value) => (
<button
key={value}
type="button"
onClick={() => 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()}
</button>
))}
</div>
</div>
</div>
);
};