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:
35
decnet_web/src/components/Config/tabs/AppearanceTab.test.tsx
Normal file
35
decnet_web/src/components/Config/tabs/AppearanceTab.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
||||
78
decnet_web/src/components/Config/tabs/AppearanceTab.tsx
Normal file
78
decnet_web/src/components/Config/tabs/AppearanceTab.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user