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"; } from "./timeRange";
import { computeGradeForecast, aggregateToMonthlyPoints } from "./utils/gradePrediction"; import { computeGradeForecast, aggregateToMonthlyPoints } from "./utils/gradePrediction";
import PredictionMonthsSlider from "./PredictionMonthsSlider.svelte"; import PredictionMonthsSlider from "./PredictionMonthsSlider.svelte";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
interface Props { interface Props {
data: Assessment[]; data: Assessment[];
@@ -166,6 +167,8 @@
return monthly.length >= 3; return monthly.length >= 3;
}); });
const animationsOn = $derived($settingsState.animations);
/** Historical + future forecast points so tooltips work across the dashed line. */ /** Historical + future forecast points so tooltips work across the dashed line. */
const chartData = $derived.by((): TrendPoint[] => { const chartData = $derived.by((): TrendPoint[] => {
if (!showPrediction || !forecast?.points.length) { if (!showPrediction || !forecast?.points.length) {
@@ -243,7 +246,7 @@
curve: curveMonotoneX, curve: curveMonotoneX,
"fill-opacity": showSubjectTrends ? 0.12 : 0.35, "fill-opacity": showSubjectTrends ? 0.12 : 0.35,
line: { class: "stroke-2" }, line: { class: "stroke-2" },
motion: "tween", motion: animationsOn ? "tween" : false,
}, },
xAxis: { xAxis: {
ticks: timeRange === "7d" ? 7 : undefined, ticks: timeRange === "7d" ? 7 : undefined,
@@ -267,12 +270,12 @@
</defs> </defs>
<ChartClipPath <ChartClipPath
initialWidth={showPrediction ? undefined : 0} initialWidth={animationsOn && !showPrediction ? 0 : undefined}
motion={showPrediction motion={animationsOn && !showPrediction
? undefined ? {
: {
width: { type: "tween", duration: 900, easing: cubicInOut }, width: { type: "tween", duration: 900, easing: cubicInOut },
}} }
: undefined}
> >
{#each series as s, i (s.key)} {#each series as s, i (s.key)}
{@const meta = chartSeries.find((c) => c.key === s.key)} {@const meta = chartSeries.find((c) => c.key === s.key)}
@@ -27,6 +27,7 @@
} from "./gradeDistribution"; } from "./gradeDistribution";
import { loadDistributionMode, saveDistributionMode } from "./storage"; import { loadDistributionMode, saveDistributionMode } from "./storage";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
@@ -312,7 +313,7 @@
motion: { motion: {
y: { type: "tween", duration: 600, easing: cubicInOut }, y: { type: "tween", duration: $settingsState.animations ? 600 : 0, easing: cubicInOut },
}, },
@@ -20,6 +20,7 @@
type TimeRange, type TimeRange,
} from "./timeRange"; } from "./timeRange";
import { openAnalyticsPrivacyPopup } from "./openAnalyticsPrivacyPopup"; import { openAnalyticsPrivacyPopup } from "./openAnalyticsPrivacyPopup";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
let analyticsData: Assessment[] | null = $state(null); let analyticsData: Assessment[] | null = $state(null);
let loading = $state(true); let loading = $state(true);
@@ -39,6 +40,8 @@
let timestampInterval: ReturnType<typeof setInterval> | null = null; let timestampInterval: ReturnType<typeof setInterval> | null = null;
let contentReady = $state(false); let contentReady = $state(false);
const fadeDuration = $derived($settingsState.animations ? 200 : 0);
const emptyFadeDuration = $derived($settingsState.animations ? 300 : 0);
const formattedTimestamp = $derived(() => { const formattedTimestamp = $derived(() => {
if (!lastUpdated) return ""; if (!lastUpdated) return "";
@@ -204,7 +207,7 @@
<div class="bsplus-analytics-root"> <div class="bsplus-analytics-root">
{#if error} {#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} {error}
</p> </p>
{/if} {/if}
@@ -462,7 +465,7 @@
</div> </div>
</div> </div>
{:else} {: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"> <aside class="bsplus-analytics-filters" aria-label="Analytics">
{@render sidebarTitle()} {@render sidebarTitle()}
{@render sidebarActions()} {@render sidebarActions()}
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
let { let {
value = $bindable<[number, number]>([0, 100]), value = $bindable<[number, number]>([0, 100]),
@@ -88,7 +89,7 @@
value = next; value = next;
if (animate) { if (animate && $settingsState.animations) {
animateVisualTo(next); animateVisualTo(next);
} else { } else {
visual = next; 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 { .bsplus-analytics-delay-1 {
animation-delay: 80ms; animation-delay: 80ms;
} }
+8 -2
View File
@@ -5,13 +5,15 @@ import { mount, unmount } from "svelte";
import GradeAnalyticsPage from "./GradeAnalyticsPage.svelte"; import GradeAnalyticsPage from "./GradeAnalyticsPage.svelte";
import { buildContrastAccentPalette } from "./utils/accentColor"; import { buildContrastAccentPalette } from "./utils/accentColor";
import { extractSolidColor } from "@/seqta/ui/colors/parseCssColor"; import { extractSolidColor } from "@/seqta/ui/colors/parseCssColor";
import { applyAnalyticsMotionClass } from "./motion";
type ThemeSettingKey = type ThemeSettingKey =
| "selectedColor" | "selectedColor"
| "DarkMode" | "DarkMode"
| "adaptiveThemeColour" | "adaptiveThemeColour"
| "adaptiveThemeGradient" | "adaptiveThemeGradient"
| "selectedTheme"; | "selectedTheme"
| "animations";
type ThemeListenerRegistration = { type ThemeListenerRegistration = {
key: ThemeSettingKey; key: ThemeSettingKey;
@@ -117,7 +119,10 @@ function syncThemeFromPage(target: HTMLElement) {
function syncThemeToAnalyticsUi() { function syncThemeToAnalyticsUi() {
if (shadowHost) syncThemeFromPage(shadowHost); if (shadowHost) syncThemeFromPage(shadowHost);
if (analyticsRoot) syncThemeFromPage(analyticsRoot); if (analyticsRoot) {
syncThemeFromPage(analyticsRoot);
applyAnalyticsMotionClass(analyticsRoot, settingsState.animations);
}
} }
function clearThemeListeners() { function clearThemeListeners() {
@@ -136,6 +141,7 @@ function watchThemeChanges() {
"adaptiveThemeColour", "adaptiveThemeColour",
"adaptiveThemeGradient", "adaptiveThemeGradient",
"selectedTheme", "selectedTheme",
"animations",
]; ];
const listener = () => syncThemeToAnalyticsUi(); const listener = () => syncThemeToAnalyticsUi();