mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-13 23:24:40 +00:00
fix: fix analyitics page for @SethBurkart123
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
let showSubjectTrends = $state(false);
|
let showSubjectTrends = $state(false);
|
||||||
|
|
||||||
let timestampInterval: ReturnType<typeof setInterval> | null = null;
|
let timestampInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let contentReady = $state(false);
|
||||||
|
|
||||||
const formattedTimestamp = $derived(() => {
|
const formattedTimestamp = $derived(() => {
|
||||||
if (!lastUpdated) return "";
|
if (!lastUpdated) return "";
|
||||||
@@ -170,6 +171,9 @@
|
|||||||
analyticsData = [];
|
analyticsData = [];
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
contentReady = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ttl = getCacheTtlMs(24);
|
const ttl = getCacheTtlMs(24);
|
||||||
@@ -235,7 +239,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if loading}
|
{#if loading || !contentReady}
|
||||||
<div class="bsplus-analytics-loading bsplus-analytics-animate">
|
<div class="bsplus-analytics-loading bsplus-analytics-animate">
|
||||||
<div class="bsplus-analytics-spinner" aria-label="Loading analytics"></div>
|
<div class="bsplus-analytics-spinner" aria-label="Loading analytics"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,146 +265,165 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="bsplus-analytics-toolbar bsplus-analytics-animate bsplus-analytics-delay-2">
|
<div class="bsplus-analytics-toolbar bsplus-analytics-animate bsplus-analytics-delay-2">
|
||||||
<div
|
<div class="bsplus-analytics-toolbar-grid">
|
||||||
class="bsplus-analytics-field bsplus-analytics-toolbar-dropdown-field"
|
<div
|
||||||
data-analytics-dropdown
|
class="bsplus-analytics-field bsplus-analytics-toolbar-dropdown-field"
|
||||||
>
|
data-analytics-dropdown
|
||||||
<span class="bsplus-analytics-field-label">Time period</span>
|
>
|
||||||
<div class="bsplus-analytics-dropdown" data-analytics-dropdown>
|
<span class="bsplus-analytics-field-label">Time period</span>
|
||||||
<button
|
<div class="bsplus-analytics-dropdown" data-analytics-dropdown>
|
||||||
type="button"
|
<button
|
||||||
class="bsplus-analytics-dropdown-trigger"
|
type="button"
|
||||||
onclick={(e) => {
|
class="bsplus-analytics-dropdown-trigger"
|
||||||
e.stopPropagation();
|
onclick={(e) => {
|
||||||
showSubjectsDropdown = false;
|
e.stopPropagation();
|
||||||
showTimeRangeDropdown = !showTimeRangeDropdown;
|
showSubjectsDropdown = false;
|
||||||
}}
|
showTimeRangeDropdown = !showTimeRangeDropdown;
|
||||||
aria-expanded={showTimeRangeDropdown}
|
}}
|
||||||
aria-haspopup="listbox"
|
aria-expanded={showTimeRangeDropdown}
|
||||||
aria-label="Time period for analytics"
|
aria-haspopup="listbox"
|
||||||
>
|
aria-label="Time period for analytics"
|
||||||
{timeRangeLabel()}
|
>
|
||||||
</button>
|
{timeRangeLabel()}
|
||||||
{#if showTimeRangeDropdown}
|
</button>
|
||||||
<div class="bsplus-analytics-dropdown-menu" role="listbox">
|
{#if showTimeRangeDropdown}
|
||||||
{#each TIME_RANGE_OPTIONS as option (option.value)}
|
<div class="bsplus-analytics-dropdown-menu" role="listbox">
|
||||||
{@const selected = timeRange === option.value}
|
{#each TIME_RANGE_OPTIONS as option (option.value)}
|
||||||
<button
|
{@const selected = timeRange === option.value}
|
||||||
type="button"
|
<button
|
||||||
class="bsplus-analytics-dropdown-item"
|
type="button"
|
||||||
class:is-selected={selected}
|
class="bsplus-analytics-dropdown-item"
|
||||||
role="option"
|
class:is-selected={selected}
|
||||||
aria-selected={selected}
|
role="option"
|
||||||
onclick={() => selectTimeRange(option.value)}
|
aria-selected={selected}
|
||||||
>
|
onclick={() => selectTimeRange(option.value)}
|
||||||
<span class="bsplus-analytics-dropdown-check"
|
|
||||||
>{selected ? "✓" : ""}</span
|
|
||||||
>
|
>
|
||||||
<span>{option.label}</span>
|
<span class="bsplus-analytics-dropdown-check"
|
||||||
</button>
|
>{selected ? "✓" : ""}</span
|
||||||
{/each}
|
>
|
||||||
</div>
|
<span>{option.label}</span>
|
||||||
{/if}
|
</button>
|
||||||
</div>
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
class="bsplus-analytics-field bsplus-analytics-toolbar-dropdown-field"
|
|
||||||
data-analytics-dropdown
|
|
||||||
>
|
|
||||||
<span class="bsplus-analytics-field-label">Subjects</span>
|
|
||||||
<div class="bsplus-analytics-dropdown" data-analytics-dropdown>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="bsplus-analytics-dropdown-trigger"
|
|
||||||
onclick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
showTimeRangeDropdown = false;
|
|
||||||
showSubjectsDropdown = !showSubjectsDropdown;
|
|
||||||
}}
|
|
||||||
aria-expanded={showSubjectsDropdown}
|
|
||||||
aria-haspopup="listbox"
|
|
||||||
>
|
|
||||||
{#if filterSubjects.length === 0}
|
|
||||||
All subjects
|
|
||||||
{:else if filterSubjects.length === 1}
|
|
||||||
{filterSubjects[0]}
|
|
||||||
{:else}
|
|
||||||
{filterSubjects.length} selected
|
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</div>
|
||||||
{#if showSubjectsDropdown}
|
</div>
|
||||||
<div class="bsplus-analytics-dropdown-menu" role="listbox">
|
|
||||||
<button
|
<div
|
||||||
type="button"
|
class="bsplus-analytics-field bsplus-analytics-toolbar-dropdown-field"
|
||||||
class="bsplus-analytics-dropdown-item"
|
data-analytics-dropdown
|
||||||
class:is-selected={filterSubjects.length === 0}
|
>
|
||||||
onclick={() => {
|
<span class="bsplus-analytics-field-label">Subjects</span>
|
||||||
filterSubjects = [];
|
<div class="bsplus-analytics-dropdown" data-analytics-dropdown>
|
||||||
showSubjectsDropdown = false;
|
<button
|
||||||
}}
|
type="button"
|
||||||
>
|
class="bsplus-analytics-dropdown-trigger"
|
||||||
<span class="bsplus-analytics-dropdown-check"
|
onclick={(e) => {
|
||||||
>{filterSubjects.length === 0 ? "✓" : ""}</span
|
e.stopPropagation();
|
||||||
>
|
showTimeRangeDropdown = false;
|
||||||
|
showSubjectsDropdown = !showSubjectsDropdown;
|
||||||
|
}}
|
||||||
|
aria-expanded={showSubjectsDropdown}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
>
|
||||||
|
{#if filterSubjects.length === 0}
|
||||||
All subjects
|
All subjects
|
||||||
</button>
|
{:else if filterSubjects.length === 1}
|
||||||
{#each uniqueSubjects() as subject}
|
{filterSubjects[0]}
|
||||||
{@const selected = filterSubjects.includes(subject)}
|
{:else}
|
||||||
|
{filterSubjects.length} selected
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if showSubjectsDropdown}
|
||||||
|
<div class="bsplus-analytics-dropdown-menu" role="listbox">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bsplus-analytics-dropdown-item"
|
class="bsplus-analytics-dropdown-item"
|
||||||
class:is-selected={selected}
|
class:is-selected={filterSubjects.length === 0}
|
||||||
onclick={() => toggleSubject(subject)}
|
onclick={() => {
|
||||||
|
filterSubjects = [];
|
||||||
|
showSubjectsDropdown = false;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span class="bsplus-analytics-dropdown-check"
|
<span class="bsplus-analytics-dropdown-check"
|
||||||
>{selected ? "✓" : ""}</span
|
>{filterSubjects.length === 0 ? "✓" : ""}</span
|
||||||
>
|
>
|
||||||
<span style="overflow:hidden;text-overflow:ellipsis">{subject}</span>
|
All subjects
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{#each uniqueSubjects() as subject}
|
||||||
</div>
|
{@const selected = filterSubjects.includes(subject)}
|
||||||
{/if}
|
<button
|
||||||
|
type="button"
|
||||||
|
class="bsplus-analytics-dropdown-item"
|
||||||
|
class:is-selected={selected}
|
||||||
|
onclick={() => toggleSubject(subject)}
|
||||||
|
>
|
||||||
|
<span class="bsplus-analytics-dropdown-check"
|
||||||
|
>{selected ? "✓" : ""}</span
|
||||||
|
>
|
||||||
|
<span style="overflow:hidden;text-overflow:ellipsis">{subject}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bsplus-analytics-field bsplus-analytics-grade-range">
|
<div class="bsplus-analytics-field bsplus-analytics-toolbar-search">
|
||||||
<span class="bsplus-analytics-field-label">Grade range</span>
|
<span class="bsplus-analytics-field-label">Search</span>
|
||||||
<GradeRangeSlider bind:value={gradeRange} />
|
<input
|
||||||
</div>
|
type="search"
|
||||||
|
class="bsplus-analytics-input"
|
||||||
|
bind:value={filterSearch}
|
||||||
|
placeholder="Search assessments…"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bsplus-analytics-field bsplus-analytics-toolbar-search">
|
{#if hasActiveFilters()}
|
||||||
<span class="bsplus-analytics-field-label">Search</span>
|
<button
|
||||||
<input
|
type="button"
|
||||||
type="search"
|
class="bsplus-analytics-btn bsplus-analytics-btn-ghost bsplus-analytics-toolbar-clear"
|
||||||
class="bsplus-analytics-input"
|
onclick={clearFilters}
|
||||||
bind:value={filterSearch}
|
>
|
||||||
placeholder="Search assessments…"
|
Clear filters
|
||||||
/>
|
</button>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<label class="bsplus-analytics-checkbox">
|
<div class="bsplus-analytics-field bsplus-analytics-grade-range">
|
||||||
<input type="checkbox" bind:checked={showSubjectTrends} />
|
<span class="bsplus-analytics-field-label">Grade range</span>
|
||||||
<span>Show per-subject trends on chart</span>
|
<GradeRangeSlider bind:value={gradeRange} />
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="bsplus-analytics-checkbox bsplus-analytics-toolbar-trends"
|
||||||
|
class:bsplus-analytics-toolbar-trends-top={!hasActiveFilters()}
|
||||||
|
>
|
||||||
|
<input type="checkbox" bind:checked={showSubjectTrends} />
|
||||||
|
<span>Show per-subject trends on chart</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bsplus-analytics-charts">
|
<div class="bsplus-analytics-charts">
|
||||||
{#key filteredData().length + "-" + gradeRange.join(",") + filterSearch + filterSubjects.join("|") + timeRange + String(showSubjectTrends)}
|
{#key filteredData().length + "-" + gradeRange.join(",") + filterSearch + filterSubjects.join("|") + timeRange + String(showSubjectTrends)}
|
||||||
<div class="bsplus-analytics-animate bsplus-analytics-delay-3">
|
<div class="bsplus-analytics-chart-cell">
|
||||||
<AnalyticsAreaChart
|
<div class="bsplus-analytics-animate bsplus-analytics-delay-3">
|
||||||
data={gradedFiltered()}
|
<AnalyticsAreaChart
|
||||||
{timeRange}
|
data={gradedFiltered()}
|
||||||
showSubjectTrends={showSubjectTrends}
|
{timeRange}
|
||||||
/>
|
showSubjectTrends={showSubjectTrends}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bsplus-analytics-animate bsplus-analytics-delay-4">
|
<div class="bsplus-analytics-chart-cell">
|
||||||
<AnalyticsBarChart data={gradedFiltered()} {timeRange} />
|
<div class="bsplus-analytics-animate bsplus-analytics-delay-4">
|
||||||
|
<AnalyticsBarChart data={gradedFiltered()} {timeRange} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bsplus-analytics-animate bsplus-analytics-delay-4" style="animation-delay: 400ms;">
|
<div class="bsplus-analytics-animate bsplus-analytics-delay-5">
|
||||||
<AssessmentTable data={timeScopedData()} />
|
<AssessmentTable data={timeScopedData()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -411,15 +434,6 @@
|
|||||||
({gradedFiltered().length} with grades)
|
({gradedFiltered().length} with grades)
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{#if hasActiveFilters()}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="bsplus-analytics-btn bsplus-analytics-btn-ghost"
|
|
||||||
onclick={clearFilters}
|
|
||||||
>
|
|
||||||
Clear filters
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</footer>
|
</footer>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="bsplus-analytics-empty bsplus-analytics-animate" transition:fade={{ duration: 300 }}>
|
<div class="bsplus-analytics-empty bsplus-analytics-animate" transition:fade={{ duration: 300 }}>
|
||||||
|
|||||||
@@ -25,10 +25,32 @@
|
|||||||
return config;
|
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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
|
use:observeChartResize
|
||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
data-slot="chart"
|
data-slot="chart"
|
||||||
class="bsplus-chart-host {className}"
|
class="bsplus-chart-host {className}"
|
||||||
|
|||||||
@@ -73,8 +73,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-analytics-animate {
|
.bsplus-analytics-animate {
|
||||||
animation: bsplus-analytics-fade-in-up 0.55s var(--bsplus-analytics-ease)
|
opacity: 0;
|
||||||
forwards;
|
animation: bsplus-analytics-fade-in-up 0.55s var(--bsplus-analytics-ease) both;
|
||||||
will-change: opacity, transform;
|
will-change: opacity, transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +98,9 @@
|
|||||||
.bsplus-analytics-delay-4 {
|
.bsplus-analytics-delay-4 {
|
||||||
animation-delay: 320ms;
|
animation-delay: 320ms;
|
||||||
}
|
}
|
||||||
|
.bsplus-analytics-delay-5 {
|
||||||
|
animation-delay: 400ms;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Header ─── */
|
/* ─── Header ─── */
|
||||||
.bsplus-analytics-header {
|
.bsplus-analytics-header {
|
||||||
@@ -165,10 +168,9 @@
|
|||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
background: color-mix(
|
background: var(
|
||||||
in srgb,
|
--bsplus-analytics-accent-subtle,
|
||||||
var(--bsplus-analytics-accent) 18%,
|
color-mix(in srgb, var(--bsplus-analytics-accent) 18%, transparent)
|
||||||
transparent
|
|
||||||
);
|
);
|
||||||
color: var(--bsplus-analytics-accent);
|
color: var(--bsplus-analytics-accent);
|
||||||
}
|
}
|
||||||
@@ -324,10 +326,6 @@
|
|||||||
|
|
||||||
/* ─── Filter toolbar ─── */
|
/* ─── Filter toolbar ─── */
|
||||||
.bsplus-analytics-toolbar {
|
.bsplus-analytics-toolbar {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem 1.15rem;
|
padding: 1rem 1.15rem;
|
||||||
border-radius: var(
|
border-radius: var(
|
||||||
--bsplus-theme-card-radius,
|
--bsplus-theme-card-radius,
|
||||||
@@ -346,6 +344,73 @@
|
|||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bsplus-analytics-toolbar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(9rem, 1fr) minmax(9rem, 1fr) minmax(12rem, 1.4fr) auto;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
gap: 0.9rem 1rem;
|
||||||
|
align-items: end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-analytics-toolbar-clear {
|
||||||
|
grid-column: 4;
|
||||||
|
grid-row: 1;
|
||||||
|
justify-self: end;
|
||||||
|
align-self: end;
|
||||||
|
padding: 0.65rem 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-analytics-toolbar-trends {
|
||||||
|
grid-column: 4;
|
||||||
|
grid-row: 2;
|
||||||
|
justify-self: end;
|
||||||
|
align-self: center;
|
||||||
|
margin-left: 0;
|
||||||
|
max-width: 14rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-analytics-toolbar-trends-top {
|
||||||
|
grid-row: 1;
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.bsplus-analytics-toolbar-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-analytics-toolbar-search {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-analytics-toolbar-clear {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: auto;
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-analytics-grade-range {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-analytics-toolbar-trends {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: auto;
|
||||||
|
justify-self: start;
|
||||||
|
max-width: none;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.bsplus-analytics-toolbar-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bsplus-analytics-toolbar-dropdown-field {
|
.bsplus-analytics-toolbar-dropdown-field {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -370,7 +435,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
margin-left: auto;
|
|
||||||
padding: 0.35rem 0;
|
padding: 0.35rem 0;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -389,13 +453,6 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.bsplus-analytics-checkbox {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-analytics-select,
|
.bsplus-analytics-select,
|
||||||
.bsplus-analytics-input {
|
.bsplus-analytics-input {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
@@ -456,9 +513,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-analytics-grade-range {
|
.bsplus-analytics-grade-range {
|
||||||
flex: 1;
|
grid-column: 1 / 4;
|
||||||
min-width: 12rem;
|
grid-row: 2;
|
||||||
max-width: 20rem;
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-analytics-range-value {
|
.bsplus-analytics-range-value {
|
||||||
@@ -471,17 +529,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-analytics-toolbar-search {
|
.bsplus-analytics-toolbar-search {
|
||||||
margin-left: auto;
|
min-width: 0;
|
||||||
flex: 1 1 14rem;
|
|
||||||
max-width: 18rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.bsplus-analytics-toolbar-search {
|
|
||||||
margin-left: 0;
|
|
||||||
max-width: none;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toolbar custom dropdowns (time period, subjects) */
|
/* Toolbar custom dropdowns (time period, subjects) */
|
||||||
@@ -601,6 +649,29 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-analytics-chart-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-analytics-chart-cell > :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 */
|
/* Fade-in animation must not paint above the filter toolbar / dropdown */
|
||||||
@@ -610,7 +681,7 @@
|
|||||||
|
|
||||||
@media (min-width: 960px) {
|
@media (min-width: 960px) {
|
||||||
.bsplus-analytics-charts {
|
.bsplus-analytics-charts {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -618,6 +689,7 @@
|
|||||||
.bsplus-analytics-card {
|
.bsplus-analytics-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
border-radius: var(
|
border-radius: var(
|
||||||
--bsplus-theme-card-radius,
|
--bsplus-theme-card-radius,
|
||||||
var(--bsplus-analytics-radius)
|
var(--bsplus-analytics-radius)
|
||||||
@@ -645,8 +717,9 @@
|
|||||||
.bsplus-analytics-card-header {
|
.bsplus-analytics-card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-end;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
min-height: 4.75rem;
|
||||||
padding: 1.15rem 1.25rem;
|
padding: 1.15rem 1.25rem;
|
||||||
border-bottom: 1px solid var(--bsplus-analytics-border);
|
border-bottom: 1px solid var(--bsplus-analytics-border);
|
||||||
}
|
}
|
||||||
@@ -657,9 +730,10 @@
|
|||||||
|
|
||||||
.bsplus-analytics-card-controls {
|
.bsplus-analytics-card-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-analytics-card-control {
|
.bsplus-analytics-card-control {
|
||||||
@@ -705,6 +779,8 @@
|
|||||||
.bsplus-analytics-card-body {
|
.bsplus-analytics-card-body {
|
||||||
padding: 1rem 1.15rem;
|
padding: 1rem 1.15rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
background: var(--bsplus-analytics-surface);
|
background: var(--bsplus-analytics-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,20 +810,24 @@
|
|||||||
|
|
||||||
/* ─── Layerchart / SVG (fix default black rects in dark UI) ─── */
|
/* ─── Layerchart / SVG (fix default black rects in dark UI) ─── */
|
||||||
.bsplus-chart-host {
|
.bsplus-chart-host {
|
||||||
display: flex;
|
display: block;
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
color: var(--bsplus-analytics-muted);
|
color: var(--bsplus-analytics-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-analytics-root .bsplus-chart-surface {
|
.bsplus-analytics-root .bsplus-chart-surface {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
height: 280px;
|
height: 280px;
|
||||||
min-height: 280px;
|
min-height: 280px;
|
||||||
max-height: 280px;
|
max-height: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-analytics-root .bsplus-chart-surface-bar {
|
.bsplus-analytics-root .bsplus-chart-surface-bar {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
height: 320px;
|
height: 320px;
|
||||||
min-height: 320px;
|
min-height: 320px;
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
@@ -787,6 +867,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-analytics-root [data-slot="chart"] svg {
|
.bsplus-analytics-root [data-slot="chart"] svg {
|
||||||
|
display: block;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
@@ -964,10 +1047,9 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
background: color-mix(
|
background: var(
|
||||||
in srgb,
|
--bsplus-analytics-accent-subtle,
|
||||||
var(--bsplus-analytics-accent) 14%,
|
color-mix(in srgb, var(--bsplus-analytics-accent) 14%, transparent)
|
||||||
transparent
|
|
||||||
);
|
);
|
||||||
color: var(--bsplus-analytics-accent);
|
color: var(--bsplus-analytics-accent);
|
||||||
}
|
}
|
||||||
@@ -998,9 +1080,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-start;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
|
color: var(--bsplus-analytics-muted);
|
||||||
|
font-size: 0.8125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── States ─── */
|
/* ─── States ─── */
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import pluginStyles from "./styles.css?inline";
|
|||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import { mount, unmount } from "svelte";
|
import { mount, unmount } from "svelte";
|
||||||
import GradeAnalyticsPage from "./GradeAnalyticsPage.svelte";
|
import GradeAnalyticsPage from "./GradeAnalyticsPage.svelte";
|
||||||
|
import { buildContrastAccentPalette } from "./utils/accentColor";
|
||||||
|
|
||||||
type ThemeSettingKey =
|
type ThemeSettingKey =
|
||||||
| "selectedColor"
|
| "selectedColor"
|
||||||
@@ -96,8 +97,17 @@ function syncThemeFromPage(target: HTMLElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const accent = resolvePageAccentColor();
|
const accent = resolvePageAccentColor();
|
||||||
target.style.setProperty("--bsplus-analytics-accent", accent);
|
const surface =
|
||||||
target.style.setProperty("--better-main", accent);
|
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(
|
target.classList.toggle(
|
||||||
"dark",
|
"dark",
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import Color from "color";
|
||||||
|
|
||||||
|
export type ContrastAccentPalette = {
|
||||||
|
accent: string;
|
||||||
|
accentSubtle: string;
|
||||||
|
onAccent: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ColorInstance = ReturnType<typeof Color>;
|
||||||
|
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user