mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-10 18:56:53 +00:00
8d9e6574cc
* feat(appearance): add Calm/Signature visual style, readability mode, and chart palette Turn the "too intense / italic headers hurt / the security graph fights my eyes" feedback into a token-driven Visual style with Calm as the new default and Signature one click back to the prior look. - Heading family routes through a `.font-heading` utility driven by `--font-heading`/`--heading-style`: operational headings render upright in the interface face under Calm and italic Instrument Serif under Signature. Base rule sets family + style only, so each call site keeps its own weight/tracking and Signature stays a true no-op; the Calm lift is a `[data-headings="clean"]` descendant rule. Brand lockup, empty-state heroes, and onboarding stay serif. - Severity charts resolve through `--sev-*` tokens with Muted, Heat, and Signature palettes; FindingsByType routes its series through the severity ramp plus a neutral so no brand-cyan sits next to rose. The risk trend flattens its gradient under Muted/Heat/reduced and keeps the gradient under Signature. - Appearance settings gain Visual style cards, a Security visualization palette, a Readability master toggle, a Motion & effects group, and a "Reset to default" button (restores the Calm axes, disabled while readability is on). Contrast moves under Readability and Ambient glow under Motion & effects. A card is selected only while the stored sub-axes match its preset, so a custom combination de-selects both. - The topbar Theme quick-switch swaps the interface/data font pickers for a Visual style switch and a Readability toggle (text size kept); its footer Settings link jumps straight to Appearance. - Readability is a sticky master that forces the calm resolution and a contrast lift at apply time without mutating the stored sub-axes. - New users default to Calm; any pre-existing persisted appearance state keeps the Signature look. The pre-paint script mirrors the store. - SegmentedControl gains a `disabled` prop and a nullable value (no active segment for a custom combination, with a roving-tabindex keyboard anchor). Adds unit/component coverage for the store, migration, chart shape logic, the disabled control, the reset/de-selection, and the quick-switch. * fix(appearance): migrate Blueprint serif headings and surface readability locks - Migrate the two operational Blueprint headings (catalog tile name, drift-policy option title) from font-serif italic to the .font-heading utility; the first pass only covered font-display, so Calm still left these italic. font-serif and font-display both resolve to the same display face, so this is the same fix. - Lock the Visual style cards under Readability (parity with the topbar switch and the on-screen guidance to turn Readability off to choose a style by hand). - Lock the Border brightness slider under Readability and show its forced +0.03 readout, since Readability overrides the stored value; dragging it previously appeared to do nothing. - Correct the Appearance docs sentence for the topbar quick switch (it listed fonts; the quick switch now carries visual style, readability, and text size).
49 lines
2.3 KiB
TypeScript
49 lines
2.3 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import { SegmentedControl } from './segmented-control';
|
|
|
|
const OPTS = [
|
|
{ value: 'a', label: 'A' },
|
|
{ value: 'b', label: 'B' },
|
|
];
|
|
|
|
describe('SegmentedControl disabled', () => {
|
|
it('marks the group aria-disabled and ignores clicks when disabled', () => {
|
|
const onChange = vi.fn();
|
|
render(<SegmentedControl value="a" options={OPTS} onChange={onChange} disabled ariaLabel="Mode" />);
|
|
expect(screen.getByRole('radiogroup', { name: 'Mode' }).getAttribute('aria-disabled')).toBe('true');
|
|
fireEvent.click(screen.getByRole('radio', { name: 'B' }));
|
|
expect(onChange).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('ignores arrow-key navigation when disabled', () => {
|
|
const onChange = vi.fn();
|
|
render(<SegmentedControl value="a" options={OPTS} onChange={onChange} disabled ariaLabel="Mode" />);
|
|
fireEvent.keyDown(screen.getByRole('radio', { name: 'A' }), { key: 'ArrowRight' });
|
|
expect(onChange).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('still fires onChange when enabled', () => {
|
|
const onChange = vi.fn();
|
|
render(<SegmentedControl value="a" options={OPTS} onChange={onChange} ariaLabel="Mode" />);
|
|
fireEvent.click(screen.getByRole('radio', { name: 'B' }));
|
|
expect(onChange).toHaveBeenCalledWith('b');
|
|
});
|
|
|
|
it('commits onChange on arrow-key navigation when enabled', () => {
|
|
const onChange = vi.fn();
|
|
render(<SegmentedControl value="a" options={OPTS} onChange={onChange} ariaLabel="Mode" />);
|
|
fireEvent.keyDown(screen.getByRole('radio', { name: 'A' }), { key: 'ArrowRight' });
|
|
expect(onChange).toHaveBeenCalledWith('b');
|
|
});
|
|
|
|
it('keeps a keyboard entry point when no option is active (value=null)', () => {
|
|
render(<SegmentedControl value={null} options={OPTS} onChange={() => {}} ariaLabel="Mode" />);
|
|
const radios = screen.getAllByRole('radio');
|
|
expect(radios.some((r) => r.getAttribute('aria-checked') === 'true')).toBe(false);
|
|
// The first option anchors the roving tabindex so Tab still reaches the group.
|
|
expect(radios[0].getAttribute('tabindex')).toBe('0');
|
|
expect(radios[1].getAttribute('tabindex')).toBe('-1');
|
|
});
|
|
});
|