mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-28 19:27:41 +00:00
feat: add Reduced motion setting and polish chrome, files, and stack-detail (#1501)
A batch of UI/UX polish: - New independent "Reduced motion" appearance setting (separate from Reduced effects). Drives framer-motion via MotionConfig and clamps CSS transitions via data-motion on <html>; toasts are unaffected. Defaults off (OS preference still honored). - Stack-detail Files tab: rename "Files & Volumes" to "Files", add a persisted word-wrap toggle to the file viewer (default on), and add a fullscreen toggle that collapses the Command Center + Logs column so the editor fills the width. - Create Stack > From Git: remove the nested scroll clamp so the deploy toggle and footer are reachable. - Fleet: full-width tab band with icon-only Refresh / Export Dossier, icon-only Check-for-updates / Add-node on the Overview toolbar, theme-aware empty-state headings (calm drops the italic), and fix the Actions card body overlapping the action-row divider. - Snapshots: restyle Restore and Restore all to the ghost button design used by View / Preview / Download, and right-align the per-stack Restore. - Settings sidebar: App Store gradient active style and standard font size. - Compose Doctor: dismiss the high-risk banner (and clear the tab dot) until the findings change, via a shared fingerprint-keyed hook. - Stack-detail Storage: link the "no recent fleet snapshot" warning to the Fleet Snapshots tab (FleetView tabs are now controlled to support the deep link).
This commit is contained in:
@@ -140,9 +140,9 @@ export function AppearanceSection() {
|
||||
const [topNavAlign, setTopNavAlign] = useTopNavAlign();
|
||||
const {
|
||||
theme, accent, borderBoost, glow, contrast, uiFont, monoFont, typeScale,
|
||||
headingStyle, chartStyle, reducedEffects, readability,
|
||||
headingStyle, chartStyle, reducedEffects, reducedMotion, readability,
|
||||
setTheme, setAccent, setBorderBoost, setGlow, setContrast, setUiFont, setMonoFont, setTypeScale,
|
||||
setVisualStyle, setHeadingStyle, setChartStyle, setReducedEffects, setReadability,
|
||||
setVisualStyle, setHeadingStyle, setChartStyle, setReducedEffects, setReducedMotion, setReadability,
|
||||
} = useTheme();
|
||||
const accentLabel = ACCENTS.find((a) => a.id === accent)?.label ?? 'Cyan';
|
||||
// Readability is a sticky master: it forces the calm resolution at apply time
|
||||
@@ -252,6 +252,17 @@ export function AppearanceSection() {
|
||||
/>
|
||||
</SettingsField>
|
||||
|
||||
<SettingsField
|
||||
label="Reduced motion"
|
||||
helper="Minimizes interface animations and transitions (dialogs, menus, expand and collapse). Toasts are unaffected."
|
||||
>
|
||||
<TogglePill
|
||||
checked={reducedMotion}
|
||||
onChange={setReducedMotion}
|
||||
aria-label="Reduced motion"
|
||||
/>
|
||||
</SettingsField>
|
||||
|
||||
<SettingsField
|
||||
label="Ambient glow"
|
||||
helper="Intensity of the accent-tinted glow behind the page."
|
||||
|
||||
@@ -81,16 +81,16 @@ export function SettingsSidebar({ currentSection, onSectionChange, dirtyFlags, o
|
||||
onClick={() => onSectionChange(item.id)}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
'relative flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs transition-colors',
|
||||
'relative flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
|
||||
isActive
|
||||
? 'text-stat-value'
|
||||
: 'text-stat-subtitle hover:bg-accent/40 hover:text-stat-value',
|
||||
? 'text-brand bg-gradient-to-r from-brand/[0.12] to-transparent'
|
||||
: 'text-foreground/80 hover:bg-muted/40',
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-y-1 left-0 w-[3px] rounded-full bg-brand shadow-[0_0_8px_color-mix(in_oklch,var(--brand)_30%,transparent)]"
|
||||
className="absolute inset-y-0 left-0 w-[2px] rounded-sm bg-brand"
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
|
||||
@@ -13,6 +13,7 @@ function resetTheme() {
|
||||
result.current.setVisualStyle('signature');
|
||||
result.current.setContrast(0);
|
||||
result.current.setGlow(0.16);
|
||||
result.current.setReducedMotion(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,6 +51,17 @@ describe('AppearanceSection', () => {
|
||||
expect(container.querySelectorAll('[data-disabled]').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('reduced motion is independent of readability and toggles data-motion on <html>', () => {
|
||||
render(<AppearanceSection />);
|
||||
const motion = () => screen.getByRole('switch', { name: 'Reduced motion' }) as HTMLButtonElement;
|
||||
expect(document.documentElement.dataset.motion).toBeUndefined();
|
||||
// Readability flattens effects but must not disable the motion toggle.
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'Readability mode' }));
|
||||
expect(motion().disabled).toBe(false);
|
||||
fireEvent.click(motion());
|
||||
expect(document.documentElement.dataset.motion).toBe('reduced');
|
||||
});
|
||||
|
||||
it('readability also locks the Visual style cards and the Border brightness slider', () => {
|
||||
const { container } = render(<AppearanceSection />);
|
||||
const calmCard = () => screen.getByRole('button', { name: /readable default/i }) as HTMLButtonElement;
|
||||
|
||||
Reference in New Issue
Block a user