@@ -411,15 +434,6 @@
({gradedFiltered().length} with grades)
{/if}
- {#if hasActiveFilters()}
-
diff --git a/src/plugins/built-in/gradeAnalytics/chart/chart-container.svelte b/src/plugins/built-in/gradeAnalytics/chart/chart-container.svelte
index 25bff61c..f2e79efa 100644
--- a/src/plugins/built-in/gradeAnalytics/chart/chart-container.svelte
+++ b/src/plugins/built-in/gradeAnalytics/chart/chart-container.svelte
@@ -25,10 +25,32 @@
return config;
},
});
+
+ function observeChartResize(node: HTMLElement) {
+ let frame = 0;
+ const notify = () => {
+ cancelAnimationFrame(frame);
+ frame = requestAnimationFrame(() => {
+ window.dispatchEvent(new Event("resize"));
+ });
+ };
+
+ const observer = new ResizeObserver(notify);
+ observer.observe(node);
+ notify();
+
+ return {
+ destroy() {
+ cancelAnimationFrame(frame);
+ observer.disconnect();
+ },
+ };
+ }
:global(.bsplus-analytics-animate) {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ width: 100%;
+ min-width: 0;
+}
+
+.bsplus-analytics-chart-cell :global(.bsplus-analytics-card) {
+ flex: 1;
+ width: 100%;
+ min-width: 0;
}
/* Fade-in animation must not paint above the filter toolbar / dropdown */
@@ -610,7 +681,7 @@
@media (min-width: 960px) {
.bsplus-analytics-charts {
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.5rem;
}
}
@@ -618,6 +689,7 @@
.bsplus-analytics-card {
display: flex;
flex-direction: column;
+ height: 100%;
border-radius: var(
--bsplus-theme-card-radius,
var(--bsplus-analytics-radius)
@@ -645,8 +717,9 @@
.bsplus-analytics-card-header {
display: flex;
justify-content: space-between;
- align-items: flex-start;
+ align-items: flex-end;
gap: 1rem;
+ min-height: 4.75rem;
padding: 1.15rem 1.25rem;
border-bottom: 1px solid var(--bsplus-analytics-border);
}
@@ -657,9 +730,10 @@
.bsplus-analytics-card-controls {
display: flex;
- flex-wrap: wrap;
+ flex-wrap: nowrap;
align-items: flex-end;
gap: 0.75rem;
+ flex-shrink: 0;
}
.bsplus-analytics-card-control {
@@ -705,6 +779,8 @@
.bsplus-analytics-card-body {
padding: 1rem 1.15rem;
flex: 1;
+ width: 100%;
+ min-width: 0;
background: var(--bsplus-analytics-surface);
}
@@ -734,20 +810,24 @@
/* ─── Layerchart / SVG (fix default black rects in dark UI) ─── */
.bsplus-chart-host {
- display: flex;
- justify-content: center;
+ display: block;
width: 100%;
+ min-width: 0;
overflow: visible;
color: var(--bsplus-analytics-muted);
}
.bsplus-analytics-root .bsplus-chart-surface {
+ width: 100%;
+ min-width: 0;
height: 280px;
min-height: 280px;
max-height: 280px;
}
.bsplus-analytics-root .bsplus-chart-surface-bar {
+ width: 100%;
+ min-width: 0;
height: 320px;
min-height: 320px;
max-height: 320px;
@@ -787,6 +867,9 @@
}
.bsplus-analytics-root [data-slot="chart"] svg {
+ display: block;
+ width: 100% !important;
+ max-width: 100%;
background: transparent !important;
overflow: visible;
}
@@ -964,10 +1047,9 @@
border-radius: 999px;
font-weight: 700;
font-size: 0.75rem;
- background: color-mix(
- in srgb,
- var(--bsplus-analytics-accent) 14%,
- transparent
+ background: var(
+ --bsplus-analytics-accent-subtle,
+ color-mix(in srgb, var(--bsplus-analytics-accent) 14%, transparent)
);
color: var(--bsplus-analytics-accent);
}
@@ -998,9 +1080,11 @@
display: flex;
flex-wrap: wrap;
align-items: center;
- justify-content: flex-end;
+ justify-content: flex-start;
gap: 0.75rem;
padding-bottom: 0.5rem;
+ color: var(--bsplus-analytics-muted);
+ font-size: 0.8125rem;
}
/* ─── States ─── */
diff --git a/src/plugins/built-in/gradeAnalytics/ui.ts b/src/plugins/built-in/gradeAnalytics/ui.ts
index 8e38cab5..d2d186fd 100644
--- a/src/plugins/built-in/gradeAnalytics/ui.ts
+++ b/src/plugins/built-in/gradeAnalytics/ui.ts
@@ -3,6 +3,7 @@ import pluginStyles from "./styles.css?inline";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { mount, unmount } from "svelte";
import GradeAnalyticsPage from "./GradeAnalyticsPage.svelte";
+import { buildContrastAccentPalette } from "./utils/accentColor";
type ThemeSettingKey =
| "selectedColor"
@@ -96,8 +97,17 @@ function syncThemeFromPage(target: HTMLElement) {
}
const accent = resolvePageAccentColor();
- target.style.setProperty("--bsplus-analytics-accent", accent);
- target.style.setProperty("--better-main", accent);
+ const surface =
+ target.style.getPropertyValue("--background-primary").trim() ||
+ computed.getPropertyValue("--background-primary").trim() ||
+ (target.classList.contains("dark") ? "#1e293b" : "#ffffff");
+ const palette = buildContrastAccentPalette(accent, surface);
+
+ target.style.setProperty("--bsplus-analytics-accent", palette.accent);
+ target.style.setProperty("--bsplus-analytics-accent-subtle", palette.accentSubtle);
+ target.style.setProperty("--better-main", palette.accent);
+ target.style.setProperty("--bsplus-theme-btn-primary-bg", palette.accent);
+ target.style.setProperty("--bsplus-theme-btn-primary-color", palette.onAccent);
target.classList.toggle(
"dark",
diff --git a/src/plugins/built-in/gradeAnalytics/utils/accentColor.ts b/src/plugins/built-in/gradeAnalytics/utils/accentColor.ts
new file mode 100644
index 00000000..606b2042
--- /dev/null
+++ b/src/plugins/built-in/gradeAnalytics/utils/accentColor.ts
@@ -0,0 +1,80 @@
+import Color from "color";
+
+export type ContrastAccentPalette = {
+ accent: string;
+ accentSubtle: string;
+ onAccent: string;
+};
+
+type ColorInstance = ReturnType;
+
+const MIN_CONTRAST_LIGHT = 4.5;
+const MIN_CONTRAST_DARK = 3;
+
+function contrastRatio(foreground: ColorInstance, background: ColorInstance): number {
+ const fg = foreground.luminosity();
+ const bg = background.luminosity();
+ const lighter = Math.max(fg, bg);
+ const darker = Math.min(fg, bg);
+ return (lighter + 0.05) / (darker + 0.05);
+}
+
+function adjustLightnessForContrast(
+ hue: number,
+ saturation: number,
+ lightness: number,
+ background: ColorInstance,
+ isDark: boolean,
+): ColorInstance {
+ const minContrast = isDark ? MIN_CONTRAST_DARK : MIN_CONTRAST_LIGHT;
+ let candidate = Color.hsl(hue, saturation, lightness);
+
+ for (let i = 0; i < 16; i++) {
+ if (contrastRatio(candidate, background) >= minContrast) {
+ return candidate;
+ }
+ const { l } = candidate.hsl().object();
+ candidate = Color.hsl(
+ hue,
+ saturation,
+ isDark ? Math.min(l + 5, 82) : Math.max(l - 5, 18),
+ );
+ }
+
+ return candidate;
+}
+
+/**
+ * Keep the user's hue/saturation but pick lightness so accent text and fills
+ * stay readable against the analytics surface background.
+ */
+export function buildContrastAccentPalette(
+ accentRaw: string,
+ backgroundRaw: string,
+): ContrastAccentPalette {
+ const accent = Color(accentRaw);
+ const background = Color(backgroundRaw);
+ const isDark = background.isDark();
+
+ const { h, s } = accent.hsl().object();
+ const saturation = Math.min(Math.max(s, 42), 88);
+ const baseLightness = isDark ? 64 : 40;
+
+ const foreground = adjustLightnessForContrast(
+ h,
+ saturation,
+ baseLightness,
+ background,
+ isDark,
+ );
+
+ const accentHex = foreground.hex();
+ const subtleLightness = isDark ? 28 : 94;
+ const subtle = Color.hsl(h, saturation * 0.75, subtleLightness);
+
+ return {
+ accent: accentHex,
+ accentSubtle: subtle.alpha(isDark ? 0.22 : 0.14).rgb().string(),
+ onAccent: foreground.isLight() ? "#141414" : "#ffffff",
+ };
+}