From 14a322a128ecadf3ed208f08c6eb6f67205580fb Mon Sep 17 00:00:00 2001 From: Aden Linday Date: Sat, 13 Jun 2026 19:36:04 +0930 Subject: [PATCH] fix: fix analyitics page for @SethBurkart123 --- .../gradeAnalytics/GradeAnalyticsPage.svelte | 268 +++++++++--------- .../chart/chart-container.svelte | 22 ++ .../built-in/gradeAnalytics/styles.css | 168 ++++++++--- src/plugins/built-in/gradeAnalytics/ui.ts | 14 +- .../gradeAnalytics/utils/accentColor.ts | 80 ++++++ 5 files changed, 381 insertions(+), 171 deletions(-) create mode 100644 src/plugins/built-in/gradeAnalytics/utils/accentColor.ts diff --git a/src/plugins/built-in/gradeAnalytics/GradeAnalyticsPage.svelte b/src/plugins/built-in/gradeAnalytics/GradeAnalyticsPage.svelte index daabf943..9d5590ee 100644 --- a/src/plugins/built-in/gradeAnalytics/GradeAnalyticsPage.svelte +++ b/src/plugins/built-in/gradeAnalytics/GradeAnalyticsPage.svelte @@ -35,6 +35,7 @@ let showSubjectTrends = $state(false); let timestampInterval: ReturnType | null = null; + let contentReady = $state(false); const formattedTimestamp = $derived(() => { if (!lastUpdated) return ""; @@ -170,6 +171,9 @@ analyticsData = []; } finally { loading = false; + requestAnimationFrame(() => { + contentReady = true; + }); } const ttl = getCacheTtlMs(24); @@ -235,7 +239,7 @@

{/if} - {#if loading} + {#if loading || !contentReady}
@@ -261,146 +265,165 @@
-
- Time period -
- - {#if showTimeRangeDropdown} -
- {#each TIME_RANGE_OPTIONS as option (option.value)} - {@const selected = timeRange === option.value} - + {#if showTimeRangeDropdown} +
+ {#each TIME_RANGE_OPTIONS as option (option.value)} + {@const selected = timeRange === option.value} + - {/each} -
- {/if} -
-
- -
- Subjects -
- + {/each} +
{/if} - - {#if showSubjectsDropdown} -
-
+
+ +
+ Subjects +
+ - {#each uniqueSubjects() as subject} - {@const selected = filterSubjects.includes(subject)} + {:else if filterSubjects.length === 1} + {filterSubjects[0]} + {:else} + {filterSubjects.length} selected + {/if} + + {#if showSubjectsDropdown} +
- {/each} -
- {/if} + {#each uniqueSubjects() as subject} + {@const selected = filterSubjects.includes(subject)} + + {/each} +
+ {/if} +
-
-
- Grade range - -
+ - + {#if hasActiveFilters()} + + {/if} - +
+ Grade range + +
+ + +
{#key filteredData().length + "-" + gradeRange.join(",") + filterSearch + filterSubjects.join("|") + timeRange + String(showSubjectTrends)} -
- +
+
+ +
-
- +
+
+ +
{/key}
-
+
@@ -411,15 +434,6 @@ ({gradedFiltered().length} with grades) {/if} - {#if hasActiveFilters()} - - {/if} {:else}
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", + }; +}