fix: fix animations on analyitics page when setting disabled (#474)

This commit is contained in:
2026-08-26 16:55:14 +09:30
parent a93c3ccda7
commit e1408fe35d
8 changed files with 83 additions and 12 deletions
@@ -14,6 +14,7 @@
} from "./timeRange";
import { computeGradeForecast, aggregateToMonthlyPoints } from "./utils/gradePrediction";
import PredictionMonthsSlider from "./PredictionMonthsSlider.svelte";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
interface Props {
data: Assessment[];
@@ -166,6 +167,8 @@
return monthly.length >= 3;
});
const animationsOn = $derived($settingsState.animations);
/** Historical + future forecast points so tooltips work across the dashed line. */
const chartData = $derived.by((): TrendPoint[] => {
if (!showPrediction || !forecast?.points.length) {
@@ -243,7 +246,7 @@
curve: curveMonotoneX,
"fill-opacity": showSubjectTrends ? 0.12 : 0.35,
line: { class: "stroke-2" },
motion: "tween",
motion: animationsOn ? "tween" : false,
},
xAxis: {
ticks: timeRange === "7d" ? 7 : undefined,
@@ -267,12 +270,12 @@
</defs>
<ChartClipPath
initialWidth={showPrediction ? undefined : 0}
motion={showPrediction
? undefined
: {
initialWidth={animationsOn && !showPrediction ? 0 : undefined}
motion={animationsOn && !showPrediction
? {
width: { type: "tween", duration: 900, easing: cubicInOut },
}}
}
: undefined}
>
{#each series as s, i (s.key)}
{@const meta = chartSeries.find((c) => c.key === s.key)}
@@ -27,6 +27,7 @@
} from "./gradeDistribution";
import { loadDistributionMode, saveDistributionMode } from "./storage";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
@@ -312,7 +313,7 @@
motion: {
y: { type: "tween", duration: 600, easing: cubicInOut },
y: { type: "tween", duration: $settingsState.animations ? 600 : 0, easing: cubicInOut },
},
@@ -20,6 +20,7 @@
type TimeRange,
} from "./timeRange";
import { openAnalyticsPrivacyPopup } from "./openAnalyticsPrivacyPopup";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
let analyticsData: Assessment[] | null = $state(null);
let loading = $state(true);
@@ -39,6 +40,8 @@
let timestampInterval: ReturnType<typeof setInterval> | null = null;
let contentReady = $state(false);
const fadeDuration = $derived($settingsState.animations ? 200 : 0);
const emptyFadeDuration = $derived($settingsState.animations ? 300 : 0);
const formattedTimestamp = $derived(() => {
if (!lastUpdated) return "";
@@ -204,7 +207,7 @@
<div class="bsplus-analytics-root">
{#if error}
<p class="bsplus-analytics-alert bsplus-analytics-animate" role="alert" transition:fade={{ duration: 200 }}>
<p class="bsplus-analytics-alert bsplus-analytics-animate" role="alert" transition:fade={{ duration: fadeDuration }}>
{error}
</p>
{/if}
@@ -462,7 +465,7 @@
</div>
</div>
{:else}
<div class="bsplus-analytics-layout bsplus-analytics-animate" transition:fade={{ duration: 300 }}>
<div class="bsplus-analytics-layout bsplus-analytics-animate" transition:fade={{ duration: emptyFadeDuration }}>
<aside class="bsplus-analytics-filters" aria-label="Analytics">
{@render sidebarTitle()}
{@render sidebarActions()}
@@ -1,5 +1,6 @@
<script lang="ts">
import { onDestroy } from "svelte";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
let {
value = $bindable<[number, number]>([0, 100]),
@@ -88,7 +89,7 @@
value = next;
if (animate) {
if (animate && $settingsState.animations) {
animateVisualTo(next);
} else {
visual = next;
@@ -0,0 +1,26 @@
import {
ANALYTICS_MOTION_CLASS,
analyticsMotionEnabled,
applyAnalyticsMotionClass,
} from "./motion";
describe("analyticsMotionEnabled", () => {
it("is on only when the animations setting is true", () => {
expect(analyticsMotionEnabled(true)).toBe(true);
expect(analyticsMotionEnabled(false)).toBe(false);
});
});
describe("applyAnalyticsMotionClass", () => {
it("adds the motion class when animations are enabled", () => {
const el = { classList: { toggle: jest.fn() } };
applyAnalyticsMotionClass(el, true);
expect(el.classList.toggle).toHaveBeenCalledWith(ANALYTICS_MOTION_CLASS, true);
});
it("removes the motion class when animations are disabled", () => {
const el = { classList: { toggle: jest.fn() } };
applyAnalyticsMotionClass(el, false);
expect(el.classList.toggle).toHaveBeenCalledWith(ANALYTICS_MOTION_CLASS, false);
});
});
@@ -0,0 +1,12 @@
export const ANALYTICS_MOTION_CLASS = "bsplus-analytics-motion";
export function analyticsMotionEnabled(animations: boolean | undefined): boolean {
return animations === true;
}
export function applyAnalyticsMotionClass(
target: { classList: { toggle: (token: string, force?: boolean) => unknown } },
animations: boolean | undefined,
): void {
target.classList.toggle(ANALYTICS_MOTION_CLASS, analyticsMotionEnabled(animations));
}
@@ -114,6 +114,25 @@
}
}
.bsplus-analytics-mount:not(.bsplus-analytics-motion) .bsplus-analytics-animate {
animation: none;
opacity: 1;
transform: none;
}
.bsplus-analytics-mount:not(.bsplus-analytics-motion) .bsplus-analytics-btn,
.bsplus-analytics-mount:not(.bsplus-analytics-motion) .bsplus-analytics-card,
.bsplus-analytics-mount:not(.bsplus-analytics-motion) .bsplus-analytics-dropdown-trigger,
.bsplus-analytics-mount:not(.bsplus-analytics-motion) .bsplus-analytics-select {
transition: none;
}
.bsplus-analytics-mount:not(.bsplus-analytics-motion) .bsplus-analytics-btn:hover:not(:disabled),
.bsplus-analytics-mount:not(.bsplus-analytics-motion) .bsplus-analytics-btn:active:not(:disabled),
.bsplus-analytics-mount:not(.bsplus-analytics-motion) .bsplus-analytics-dropdown-trigger:hover {
transform: none;
}
.bsplus-analytics-delay-1 {
animation-delay: 80ms;
}
+8 -2
View File
@@ -5,13 +5,15 @@ import { mount, unmount } from "svelte";
import GradeAnalyticsPage from "./GradeAnalyticsPage.svelte";
import { buildContrastAccentPalette } from "./utils/accentColor";
import { extractSolidColor } from "@/seqta/ui/colors/parseCssColor";
import { applyAnalyticsMotionClass } from "./motion";
type ThemeSettingKey =
| "selectedColor"
| "DarkMode"
| "adaptiveThemeColour"
| "adaptiveThemeGradient"
| "selectedTheme";
| "selectedTheme"
| "animations";
type ThemeListenerRegistration = {
key: ThemeSettingKey;
@@ -117,7 +119,10 @@ function syncThemeFromPage(target: HTMLElement) {
function syncThemeToAnalyticsUi() {
if (shadowHost) syncThemeFromPage(shadowHost);
if (analyticsRoot) syncThemeFromPage(analyticsRoot);
if (analyticsRoot) {
syncThemeFromPage(analyticsRoot);
applyAnalyticsMotionClass(analyticsRoot, settingsState.animations);
}
}
function clearThemeListeners() {
@@ -136,6 +141,7 @@ function watchThemeChanges() {
"adaptiveThemeColour",
"adaptiveThemeGradient",
"selectedTheme",
"animations",
];
const listener = () => syncThemeToAnalyticsUi();