fix: fix analyitics page for @SethBurkart123

This commit is contained in:
2026-06-13 19:36:04 +09:30
parent 160407dde6
commit 14a322a128
5 changed files with 381 additions and 171 deletions
@@ -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}"
+126 -42
View File
@@ -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 ─── */
+12 -2
View File
@@ -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",
};
}