diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 23891c7..10947ad 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1122,12 +1122,13 @@ export function appendBandwidthSample( } interface BandwidthChartProps { + animationsEnabled: boolean; running: boolean; paused: boolean; speedHistoryRef: React.MutableRefObject<{ time: number; speed: number }[]>; } -const BandwidthChart = memo(function BandwidthChart({ running, paused, speedHistoryRef }: BandwidthChartProps): ReactElement { +const BandwidthChart = memo(function BandwidthChart({ animationsEnabled, running, paused, speedHistoryRef }: BandwidthChartProps): ReactElement { const canvasRef = useRef(null); const containerRef = useRef(null); @@ -1260,12 +1261,11 @@ const BandwidthChart = memo(function BandwidthChart({ running, paused, speedHist if (!running || paused) { return; } - const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const interval = setInterval(() => { drawChart(); - }, reducedMotion ? 1000 : 250); - return () => clearInterval(interval); - }, [drawChart, running, paused]); + }, animationsEnabled ? 250 : 1000); + return () => clearInterval(interval); + }, [animationsEnabled, drawChart, running, paused]); useEffect(() => { const handleResize = () => { @@ -5347,7 +5347,7 @@ export function App(): ReactElement { return (
{ event.preventDefault(); const hasFiles = event.dataTransfer.types.includes("Files"); @@ -5373,6 +5373,7 @@ export function App(): ReactElement { > Paket- und Linkstatus werden laufend aktualisiert. Auswahl, Reihenfolge und aktive Filter bleiben beim Ansichtswechsel erhalten. @@ -5730,7 +5731,7 @@ export function App(): ReactElement { {tab === "statistics" && ( } + chart={} model={statisticsViewModel} /> )} diff --git a/src/renderer/shell/AppShell.tsx b/src/renderer/shell/AppShell.tsx index f06485b..b0e8b5b 100644 --- a/src/renderer/shell/AppShell.tsx +++ b/src/renderer/shell/AppShell.tsx @@ -29,6 +29,7 @@ function getServerShellMode(): ResponsiveShellMode { export interface AppShellProps { activeView: MainView; + animationsEnabled?: boolean; onViewChange: (view: MainView) => void; sidebar: ReactNode; sidebarStatus: ReactNode; @@ -43,6 +44,7 @@ export interface AppShellProps { export function AppShell({ activeView, + animationsEnabled = true, onViewChange, sidebar, sidebarStatus, @@ -74,7 +76,7 @@ export function AppShell({ return (
diff --git a/src/renderer/shell/shell.css b/src/renderer/shell/shell.css index d8559e3..cc45e98 100644 --- a/src/renderer/shell/shell.css +++ b/src/renderer/shell/shell.css @@ -55,8 +55,8 @@ padding: 0 16px; color: var(--ui-text); font-size: 16px; - font-weight: 700; - line-height: 20px; + font-weight: 700; + line-height: 20px; } .md-shell-brand-mark { @@ -919,50 +919,7 @@ } .md-dialog-actions, - .md-update-dialog-actions { - flex-wrap: wrap; - } -} - -@media (prefers-reduced-motion: reduce) { - .md-shell *, - .md-shell *::before, - .md-shell *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - scroll-behavior: auto !important; - transition-duration: 0.01ms !important; - } - - .md-shell-workspace { - transition-duration: 520ms !important; - } - - .md-shell-sidebar-panel { - transition-duration: 260ms, 520ms, 0s !important; - } - - .md-shell-navigation::before { - transition-duration: 420ms, 420ms, 420ms, 120ms !important; - } - - .md-application-menu-tree .menu-dropdown { - transition-property: opacity, transform, visibility !important; - transition-duration: 150ms, 180ms, 0s !important; - transition-timing-function: ease, cubic-bezier(0.2, 0.8, 0.2, 1), linear !important; - transition-delay: 0s, 0s, 180ms !important; - } - - .md-application-menu-tree .menu-dropdown.is-open { - transition-delay: 0s !important; - } - - .md-application-menu-tree .menu-submenu-dropdown { - transition-duration: 0.01ms !important; - transition-delay: 0s !important; - } - - .md-application-menu-tree .menu-submenu-dropdown.is-open { - transition-delay: 0s !important; + .md-update-dialog-actions { + flex-wrap: wrap; } } diff --git a/src/renderer/styles.css b/src/renderer/styles.css index b497979..8d37306 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -89,15 +89,7 @@ transform: translate3d(var(--ui-sliding-selection-x, 0px), 0, 0); } -@media (prefers-reduced-motion: reduce) { - .ui-sliding-selection::before { - transition-property: transform, width, height !important; - transition-duration: var(--ui-sliding-selection-duration, 0ms), var(--ui-sliding-selection-duration, 0ms), var(--ui-sliding-selection-duration, 0ms) !important; - transition-timing-function: cubic-bezier(0.22, 0.76, 0.22, 1), cubic-bezier(0.22, 0.76, 0.22, 1), cubic-bezier(0.22, 0.76, 0.22, 1) !important; - } -} - -html, +html, body, #root { margin: 0; diff --git a/src/renderer/theme.css b/src/renderer/theme.css index d7f370e..ac4bdf4 100644 --- a/src/renderer/theme.css +++ b/src/renderer/theme.css @@ -406,13 +406,12 @@ textarea, outline-offset: 2px; } -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - scroll-behavior: auto !important; - transition-duration: 0.01ms !important; - } -} +.is-runtime-motion-disabled, +.is-runtime-motion-disabled *, +.is-runtime-motion-disabled *::before, +.is-runtime-motion-disabled *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; +} diff --git a/src/renderer/views/downloads/downloads.css b/src/renderer/views/downloads/downloads.css index a6c4a62..7dbc989 100644 --- a/src/renderer/views/downloads/downloads.css +++ b/src/renderer/views/downloads/downloads.css @@ -591,25 +591,6 @@ transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1); } -@media (prefers-reduced-motion: reduce) { - .downloads-virtual-spacer { - transition-duration: 1500ms !important; - } - - .downloads-virtual-row { - transition-duration: 1500ms, 1500ms, 1500ms !important; - } - - .downloads-table.is-column-drag-active [data-download-column], - .downloads-table.is-column-drag-settling [data-download-column] { - transition-duration: 220ms !important; - } - - .downloads-table.is-column-drag-active [data-column-dragging="true"] { - transition-duration: 0s !important; - } -} - .downloads-column-sort { width: 100%; border: 0; diff --git a/src/renderer/views/history/history.css b/src/renderer/views/history/history.css index f06166b..15b542b 100644 --- a/src/renderer/views/history/history.css +++ b/src/renderer/views/history/history.css @@ -499,9 +499,3 @@ } } - -@media (prefers-reduced-motion: reduce) { - .history-detail-disclosure:not(.is-history-motion-disabled) { - transition-duration: 520ms, 260ms !important; - } -} diff --git a/tests/app-shell.test.tsx b/tests/app-shell.test.tsx index 7239ad4..769ffa9 100644 --- a/tests/app-shell.test.tsx +++ b/tests/app-shell.test.tsx @@ -58,7 +58,6 @@ describe("desktop shell", () => { expect(source.match(/menu-dropdown\$\{openMenu ===/g)).toHaveLength(3); expect(css).toMatch(/\.menu-dropdown\s*\{[^}]*opacity:\s*0;[^}]*transform:\s*translateY\(-8px\);[^}]*visibility:\s*hidden;/s); expect(css).toMatch(/\.menu-dropdown\.is-open\s*\{[^}]*opacity:\s*1;[^}]*transform:\s*translateY\(0\);[^}]*visibility:\s*visible;/s); - expect(shellCss).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.md-application-menu-tree \.menu-dropdown\s*\{[^}]*transition-duration:\s*150ms, 180ms, 0s !important;/); }); it("anchors every top-level application dropdown to the right header edge", () => { @@ -78,7 +77,6 @@ describe("desktop shell", () => { expect(shellCss).toMatch(/\.md-application-menu-tree :where\(\.menu-dropdown, \.menu-submenu-dropdown\)\s*\{[^}]*overflow:\s*visible;/s); expect(shellCss).toMatch(/\.md-application-menu-tree \.menu-submenu-dropdown\s*\{[^}]*right:\s*100%;[^}]*left:\s*auto;[^}]*opacity:\s*0;[^}]*transform:\s*translateX\(8px\);[^}]*visibility:\s*hidden;[^}]*pointer-events:\s*none;[^}]*transition:[^}]*transform 220ms cubic-bezier\(0\.22, 0\.76, 0\.22, 1\)/s); expect(shellCss).toMatch(/\.md-application-menu-tree \.menu-submenu-dropdown\.is-open\s*\{[^}]*opacity:\s*1;[^}]*transform:\s*translateX\(0\);[^}]*visibility:\s*visible;[^}]*pointer-events:\s*auto;/s); - expect(shellCss).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.md-application-menu-tree \.menu-submenu-dropdown\s*\{[^}]*transition-duration:\s*0\.01ms !important;[^}]*transition-delay:\s*0s !important;/); }); it("uses the product asset in the header brand", () => { @@ -101,11 +99,10 @@ describe("desktop shell", () => { expect(html).toContain("data-main-view=\"downloads\""); expect(source).toContain("ResizeObserver"); expect(css).toMatch(/\.md-shell-navigation::before\s*\{[^}]*transform:\s*translate3d\(var\(--md-navigation-active-x[^}]*transition:\s*transform 420ms cubic-bezier\(0\.22, 0\.76, 0\.22, 1\), width 420ms cubic-bezier\(0\.22, 0\.76, 0\.22, 1\)/s); - expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.md-shell-navigation::before\s*\{[^}]*transition-duration:\s*420ms, 420ms, 420ms, 120ms !important;/); expect(css).toMatch(/\.md-shell-navigation-item\.is-active\s*\{[^}]*background:\s*transparent;/s); }); - it("renders context regions without global placeholders", () => { + it("renders context regions without global placeholders", () => { const html = renderToStaticMarkup( { expect(html).toContain("data-ui-region=\"sidebar\""); expect(html).toContain("data-ui-region=\"sidebar-status\""); expect(html).toContain("data-ui-region=\"main\""); - expect(html).toContain("1–1 von 1"); - }); - + expect(html).toContain("1–1 von 1"); + }); + + it("uses the explicit application animation setting instead of the Windows motion preference", () => { + const renderShell = (animationsEnabled: boolean): string => renderToStaticMarkup( + {}} + sidebar={null} + sidebarStatus={null} + headerActions={null} + toolbar={null} + footer={null} + contextInfo={null} + sidebarCollapsed={false} + onSidebarCollapsedChange={() => {}} + > +
Downloads
+
+ ); + + expect(renderShell(true)).toContain("is-runtime-motion-enabled"); + expect(renderShell(false)).toContain("is-runtime-motion-disabled"); + }); + it("does not reserve a collapsed sidebar column when sidebar slots are empty", () => { const html = renderToStaticMarkup( { expect(shellCss).toMatch(/\.md-shell-sidebar:hover \.md-shell-sidebar-toggle,\s*\.md-shell-sidebar\.is-collapsed \.md-shell-sidebar-toggle,\s*\.md-shell-sidebar-toggle:focus-visible\s*\{[^}]*opacity:\s*1;/s); }); - it("keeps the explicitly requested sidebar motion active when Windows animations are disabled", () => { - const shellCss = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8"); - - expect(shellCss).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.md-shell-workspace\s*\{[^}]*transition-duration:\s*520ms !important;/s); - expect(shellCss).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.md-shell-sidebar-panel\s*\{[^}]*transition-duration:\s*260ms, 520ms, 0s !important;/s); - }); - it("keeps every view sidebar on the same content axis", () => { const shellCss = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8").replaceAll("\r\n", "\n"); diff --git a/tests/downloads-view.test.tsx b/tests/downloads-view.test.tsx index 0eace2c..b0dcd8b 100644 --- a/tests/downloads-view.test.tsx +++ b/tests/downloads-view.test.tsx @@ -98,7 +98,6 @@ describe("Downloadtabellen-Spalten", () => { expect(css).toMatch(/\.downloads-table\.is-column-drag-active \[data-download-column\]\s*\{[^}]*transform:\s*translate3d\(var\(--downloads-column-drag-x, 0px\), 0, 0\);[^}]*transition:\s*transform 220ms/s); expect(css).toMatch(/\.downloads-table\.is-column-drag-active \[data-column-dragging="true"\]\s*\{[^}]*transition:\s*none;/s); expect(css).not.toMatch(/\.downloads-table\.is-column-drag-active \[data-column-dragging="true"\]\s*\{[^}]*background:/s); - expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-table\.is-column-drag-active \[data-download-column\][^{]*\{[^}]*transition-duration:\s*220ms !important;/s); }); it("changes package collapse state only through user actions", () => { @@ -334,8 +333,8 @@ describe("virtualisierte Paketanimation", () => { const css = fs.readFileSync(path.join(process.cwd(), "src/renderer/views/downloads/downloads.css"), "utf8"); expect(DOWNLOAD_DISCLOSURE_DURATION_MS).toBe(1500); - expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-virtual-spacer\s*\{[^}]*transition-duration:\s*1500ms !important;/s); - expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-virtual-row\s*\{[^}]*transition-duration:\s*1500ms, 1500ms, 1500ms !important;/s); + expect(css).toMatch(/\.downloads-virtual-spacer\s*\{[^}]*transition:\s*height 1500ms/s); + expect(css).toMatch(/\.downloads-virtual-row\s*\{[^}]*transition:\s*height 1500ms[^}]*transform 1500ms[^}]*opacity 1500ms/s); }); it("schneidet beim Einfahren den gemeinsamen Paketbereich ab statt Unterzeilen zusammenzuziehen", () => { diff --git a/tests/statistics-view.test.tsx b/tests/statistics-view.test.tsx index 16faf0d..65ccf01 100644 --- a/tests/statistics-view.test.tsx +++ b/tests/statistics-view.test.tsx @@ -500,14 +500,14 @@ describe("bandwidth chart palette", () => { expect(palette).toEqual({ accent: "rgb(74, 222, 128)" }); }); - it("labels the live chart and slows redraws when reduced motion is requested", () => { + it("labels the live chart and follows the explicit application animation setting", () => { const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8"); const chartBlock = source.slice(source.indexOf("const BandwidthChart"), source.indexOf("interface DownloadSpeedSparklineProps")); expect(chartBlock).toContain('role="img"'); expect(chartBlock).toContain('aria-label="Bandbreitenverlauf der letzten 60 Sekunden"'); - expect(chartBlock).toContain('window.matchMedia("(prefers-reduced-motion: reduce)")'); - expect(chartBlock).toContain("reducedMotion ? 1000 : 250"); + expect(chartBlock).toContain("animationsEnabled ? 250 : 1000"); + expect(chartBlock).not.toContain("prefers-reduced-motion"); }); it("asks for confirmation before deleting all saved download statistics", () => {