diff --git a/src/plugins/built-in/assessmentsOverview/AssessmentsOverview.svelte b/src/plugins/built-in/assessmentsOverview/AssessmentsOverview.svelte index c30c1835..354c1c20 100644 --- a/src/plugins/built-in/assessmentsOverview/AssessmentsOverview.svelte +++ b/src/plugins/built-in/assessmentsOverview/AssessmentsOverview.svelte @@ -3,6 +3,12 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"; import { buildEngageAssessmentPagePath } from "@/seqta/utils/engageAssessmentStudent"; + import OverviewIcon from "./OverviewIcon.svelte"; + import { + GROUP_SORT_ICONS, + STATUS_COLUMN_ICONS, + type OverviewIconName, + } from "./icons"; import confetti from "canvas-confetti"; export let data: any; @@ -50,7 +56,12 @@ let filteredAssessments: any[] = []; let statusGroups: Record = {}; - let columns: { key: string; title: string; className: string; icon: string }[] = []; + let columns: { + key: string; + title: string; + className: string; + icon: OverviewIconName; + }[] = []; function getAssessmentYear(a: any): number { const dateStr = a.due || a.date || a.dueDate || a.created; @@ -89,14 +100,23 @@ return new Date(a.due || a.date || 0).getTime() - new Date(b.due || b.date || 0).getTime(); } - const STATUS_COLUMNS = [ - { key: "UPCOMING", title: "Upcoming", className: "column-upcoming", icon: "πŸ“…" }, - { key: "DUE_SOON", title: "Due Soon", className: "column-due-soon", icon: "⏰" }, - { key: "OVERDUE", title: "Overdue", className: "column-overdue", icon: "🚨" }, - { key: "SUBMITTED", title: "Submitted", className: "column-submitted", icon: "πŸ“" }, - { key: "MARKS_RELEASED", title: "Marked", className: "column-marked", icon: "βœ…" }, + const STATUS_COLUMNS: { + key: string; + title: string; + className: string; + icon: OverviewIconName; + }[] = [ + { key: "UPCOMING", title: "Upcoming", className: "column-upcoming", icon: "calendar-days" }, + { key: "DUE_SOON", title: "Due Soon", className: "column-due-soon", icon: "clock" }, + { key: "OVERDUE", title: "Overdue", className: "column-overdue", icon: "exclamation-triangle" }, + { key: "SUBMITTED", title: "Submitted", className: "column-submitted", icon: "document-check" }, + { key: "MARKS_RELEASED", title: "Marked", className: "column-marked", icon: "check-circle" }, ]; + function groupSortIcon(): OverviewIconName { + return GROUP_SORT_ICONS[currentFilters.sortBy] ?? "queue-list"; + } + function buildGroupsAndColumns() { if (!data?.assessments) return { filteredAssessments: [], statusGroups: {}, columns: [] }; const subjectFilters = settingsState.subjectfilters || {}; @@ -131,18 +151,19 @@ groups[key].sort(sortCompare); }); - let cols: { key: string; title: string; className: string; icon: string }[]; + let cols: { key: string; title: string; className: string; icon: OverviewIconName }[]; if (currentFilters.sortBy === "due") { cols = STATUS_COLUMNS; } else { const keys = Object.keys(groups).filter((k) => groups[k]?.length > 0); + const sortIcon = groupSortIcon(); if (currentFilters.sortBy === "year") { - cols = keys.sort((a, b) => Number(b) - Number(a)).map((k) => ({ key: k, title: k, className: "column-custom", icon: "πŸ“†" })); + cols = keys.sort((a, b) => Number(b) - Number(a)).map((k) => ({ key: k, title: k, className: "column-custom", icon: sortIcon })); } else if (currentFilters.sortBy === "subject") { const subjectTitles = new Map(data?.subjects?.map((s: any) => [s.code, `${s.code} - ${s.title}`]) || []); - cols = keys.sort().map((k) => ({ key: k, title: subjectTitles.get(k) || k, className: "column-custom", icon: "πŸ“š" })); + cols = keys.sort().map((k) => ({ key: k, title: subjectTitles.get(k) || k, className: "column-custom", icon: sortIcon })); } else { - cols = keys.sort().map((k) => ({ key: k, title: k, className: "column-custom", icon: "πŸ“‹" })); + cols = keys.sort().map((k) => ({ key: k, title: k, className: "column-custom", icon: sortIcon })); } } @@ -379,10 +400,13 @@ -
-
-

Assessments

-
+
+
+
+

Assessments

+

Track upcoming tasks, submissions, and released marks

+
+
{#if showStudentFilter} @@ -9,17 +30,20 @@
-
+ -
+
{#each columns as column}
- {column.icon} {column.title} - ... + + + {column.title} + + …
@@ -43,36 +67,3 @@
- - \ No newline at end of file diff --git a/src/plugins/built-in/assessmentsOverview/api.ts b/src/plugins/built-in/assessmentsOverview/api.ts index 91acfaf3..82fd8ed8 100644 --- a/src/plugins/built-in/assessmentsOverview/api.ts +++ b/src/plugins/built-in/assessmentsOverview/api.ts @@ -1,9 +1,10 @@ -interface Subject { - code: string; - programme: number; - metaclass: number; - title: string; -} +import { + activeSubjectsFromLearnPayload, + assessmentBelongsToActiveSubjects, + filterAssessmentsForActiveSubjects, + type OverviewSubject, +} from "./utils"; + interface PrefItem { name: string; value: string; @@ -31,11 +32,9 @@ async function fetchJSON(url: string, body: any) { return res.json(); } -async function loadSubjects() { +async function loadSubjects(): Promise { const res = await fetchJSON("/seqta/student/load/subjects?", {}); - return res.payload - .filter((s: any) => s.active === 1) - .flatMap((s: any) => s.subjects); + return activeSubjectsFromLearnPayload(res.payload); } async function loadPrefs(student: number) { @@ -61,7 +60,7 @@ async function loadUpcoming(student: number) { return res.payload; } -function normalizeAssessmentDates(t: any, subject: Subject): any { +function normalizeAssessmentDates(t: any, subject: OverviewSubject): any { const normalized = { ...t }; if (!normalized.due && (t.date || t.dueDate || t.created || t.submittedDate)) { normalized.due = t.date || t.dueDate || t.created || t.submittedDate; @@ -72,7 +71,7 @@ function normalizeAssessmentDates(t: any, subject: Subject): any { return normalized; } -async function loadPast(student: number, subjects: Subject[]) { +async function loadPast(student: number, subjects: OverviewSubject[]) { const map: Record = {}; await Promise.all( subjects.map(async (s) => { @@ -141,14 +140,20 @@ async function getLearnAssessmentsData(studentId: number) { const pastMap = await loadPast(studentId, subjects); const map: Record = {}; upcoming.forEach((a: any) => { - map[a.id] = { ...a }; + if (assessmentBelongsToActiveSubjects(a, subjects)) { + map[a.id] = { ...a }; + } }); Object.values(pastMap).forEach((t: any) => { + if (!assessmentBelongsToActiveSubjects(t, subjects)) return; if (map[t.id]) Object.assign(map[t.id], t); else map[t.id] = t; }); - const allAssessments = Object.values(map); + const allAssessments = filterAssessmentsForActiveSubjects( + Object.values(map), + subjects, + ); const submissions = await loadSubmissions(studentId, allAssessments); allAssessments.forEach((assessment: any) => { diff --git a/src/plugins/built-in/assessmentsOverview/engageApi.ts b/src/plugins/built-in/assessmentsOverview/engageApi.ts index b148e75a..98ea8b37 100644 --- a/src/plugins/built-in/assessmentsOverview/engageApi.ts +++ b/src/plugins/built-in/assessmentsOverview/engageApi.ts @@ -1,11 +1,10 @@ import { getEngageAssessmentStudentId } from "@/seqta/utils/engageAssessmentStudent"; - -interface Subject { - code: string; - programme: number; - metaclass: number; - title: string; -} +import { + activeSubjectsFromEngageChild, + assessmentBelongsToActiveSubjects, + filterAssessmentsForActiveSubjects, + type OverviewSubject, +} from "./utils"; interface PrefItem { name: string; @@ -58,17 +57,8 @@ export async function resolveEngageStudentId(): Promise { throw new Error("Could not resolve Engage student ID"); } -function subjectsFromChild(child: EngageChildPayload): Subject[] { - return (child.terms ?? []) - .filter((term) => term.active === 1) - .flatMap((term) => - (term.subjects ?? []).map((subject) => ({ - code: subject.code ?? "", - programme: subject.programme ?? 0, - metaclass: subject.metaclass ?? 0, - title: subject.title ?? subject.description ?? subject.code ?? "", - })), - ); +function subjectsFromChild(child: EngageChildPayload): OverviewSubject[] { + return activeSubjectsFromEngageChild(child); } async function loadEngagePrefs(): Promise> { @@ -94,7 +84,7 @@ async function loadEngageUpcoming(studentId: number) { return res.payload ?? []; } -function normalizeAssessmentDates(t: any, subject: Subject): any { +function normalizeAssessmentDates(t: any, subject: OverviewSubject): any { const normalized = { ...t }; if (!normalized.due && (t.date || t.dueDate || t.created || t.submittedDate)) { normalized.due = t.date || t.dueDate || t.created || t.submittedDate; @@ -105,7 +95,7 @@ function normalizeAssessmentDates(t: any, subject: Subject): any { return normalized; } -async function loadEngagePast(studentId: number, subjects: Subject[]) { +async function loadEngagePast(studentId: number, subjects: OverviewSubject[]) { const map: Record = {}; await Promise.all( @@ -179,14 +169,20 @@ async function loadEngageAssessmentsForStudent( const map: Record = {}; upcoming.forEach((assessment: any) => { - map[assessment.id] = { ...assessment }; + if (assessmentBelongsToActiveSubjects(assessment, subjects)) { + map[assessment.id] = { ...assessment }; + } }); Object.values(pastMap).forEach((task: any) => { + if (!assessmentBelongsToActiveSubjects(task, subjects)) return; if (map[task.id]) Object.assign(map[task.id], task); else map[task.id] = task; }); - const assessments = Object.values(map).map((assessment) => ({ + const assessments = filterAssessmentsForActiveSubjects( + Object.values(map), + subjects, + ).map((assessment) => ({ ...assessment, studentId, studentName, @@ -218,7 +214,7 @@ export async function getEngageAssessmentsData() { Promise.all(childrenPayload.map((child) => loadEngageAssessmentsForStudent(child))), ]); - const subjectsMap = new Map(); + const subjectsMap = new Map(); childrenPayload.forEach((child) => { subjectsFromChild(child).forEach((subject) => { if (!subjectsMap.has(subject.code)) { diff --git a/src/plugins/built-in/assessmentsOverview/icons.ts b/src/plugins/built-in/assessmentsOverview/icons.ts new file mode 100644 index 00000000..c8d0ba8e --- /dev/null +++ b/src/plugins/built-in/assessmentsOverview/icons.ts @@ -0,0 +1,65 @@ +/** Heroicons v2 outline paths (https://heroicons.com) */ +export type OverviewIconName = + | "calendar-days" + | "clock" + | "exclamation-triangle" + | "document-check" + | "check-circle" + | "book-open" + | "calendar" + | "chart-bar" + | "queue-list" + | "eye" + | "clipboard-document-list" + | "ellipsis-vertical" + | "exclamation-circle"; + +export const OVERVIEW_ICON_PATHS: Record< + OverviewIconName, + string | string[] +> = { + "calendar-days": + "M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5", + clock: "M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z", + "exclamation-triangle": + "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z", + "document-check": + "M10.125 2.25h-4.5c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9zm3.75 8.625a2.625 2.625 0 100-5.25 2.625 2.625 0 000 5.25zm0 0l-3 3m3-3l3 3", + "check-circle": + "M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z", + "book-open": + "M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25", + calendar: + "M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5", + "chart-bar": + "M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z", + "queue-list": + "M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z", + eye: [ + "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z", + "M15 12a3 3 0 11-6 0 3 3 0 016 0z", + ], + "clipboard-document-list": [ + "M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 17.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z", + "M8.25 6.75V4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V6.75H8.25z", + ], + "ellipsis-vertical": + "M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z", + "exclamation-circle": + "M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z", +}; + +export const STATUS_COLUMN_ICONS: Record = { + UPCOMING: "calendar-days", + DUE_SOON: "clock", + OVERDUE: "exclamation-triangle", + SUBMITTED: "document-check", + MARKS_RELEASED: "check-circle", +}; + +export const GROUP_SORT_ICONS: Record = { + year: "calendar", + subject: "book-open", + grade: "chart-bar", + title: "queue-list", +}; diff --git a/src/plugins/built-in/assessmentsOverview/index.ts b/src/plugins/built-in/assessmentsOverview/index.ts index 75d1d0e8..c23264ba 100644 --- a/src/plugins/built-in/assessmentsOverview/index.ts +++ b/src/plugins/built-in/assessmentsOverview/index.ts @@ -110,7 +110,7 @@ const assessmentsOverviewPlugin: Plugin<{}> = { .querySelector('[data-key="assessments"]') ?.classList.add("active"); - main.innerHTML = '
'; + main.innerHTML = '
'; const container = document.getElementById( "grid-view-container", ) as HTMLElement; diff --git a/src/plugins/built-in/assessmentsOverview/styles.css b/src/plugins/built-in/assessmentsOverview/styles.css index b9757114..b6c8398a 100644 --- a/src/plugins/built-in/assessmentsOverview/styles.css +++ b/src/plugins/built-in/assessmentsOverview/styles.css @@ -1,60 +1,148 @@ -#grid-view-container { +/* ─── Design tokens (aligned with Grade Analytics) ─── */ +.bsplus-overview-host, +.bsplus-overview-root, +#grid-view-container.bsplus-overview-root { + --bsplus-overview-radius: 16px; + --bsplus-overview-radius-sm: 12px; + --bsplus-overview-ease: cubic-bezier(0.4, 0, 0.2, 1); + --bsplus-overview-surface: var(--background-primary, #ffffff); + --bsplus-overview-surface-2: var(--background-secondary, #f8fafc); + --bsplus-overview-text: var(--text-primary, #1a1a1a); + --bsplus-overview-muted: color-mix( + in srgb, + var(--bsplus-overview-text) 55%, + transparent + ); + --bsplus-overview-border: color-mix( + in srgb, + var(--theme-offset-bg, var(--background-secondary, #e2e8f0)) 78%, + transparent + ); + --bsplus-overview-shadow: 0 5px 16px 6px rgba(0, 0, 0, 0.08); + --bsplus-overview-shadow-hover: 0 8px 24px 8px rgba(0, 0, 0, 0.12); + --bsplus-overview-accent: var(--better-main, #007bff); +} + +.bsplus-overview-root.dark { + --bsplus-overview-shadow: 0 5px 20px 6px rgba(0, 0, 0, 0.35); + --bsplus-overview-shadow-hover: 0 10px 28px 10px rgba(0, 0, 0, 0.45); +} + +@keyframes bsplus-overview-fade-in-up { + from { + opacity: 0; + transform: translateY(14px) scale(0.99); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.bsplus-overview-animate { + animation: bsplus-overview-fade-in-up 0.5s var(--bsplus-overview-ease) forwards; +} + +@media (prefers-reduced-motion: reduce) { + .bsplus-overview-animate { + animation: none; + } +} + +.bsplus-overview-delay-1 { + animation-delay: 80ms; +} + +.bsplus-overview-icon { + flex-shrink: 0; +} + +#grid-view-container, +.bsplus-overview-root { background: transparent; height: 100%; display: flex; flex-direction: column; overflow: hidden; + width: 100%; + min-height: min(100%, calc(100vh - 6rem)); + box-sizing: border-box; + padding: 1.5rem 1.25rem 2rem; + font-family: Rubik, system-ui, sans-serif; + font-size: 0.875rem; + color: var(--bsplus-overview-text); + gap: 1.25rem; } .grid-view-header { display: flex; + flex-wrap: wrap; justify-content: space-between; - align-items: center; - padding: 1rem; + align-items: flex-start; + gap: 1.25rem; flex-shrink: 0; + padding: 0; +} + +.grid-view-header-text { + min-width: 0; } .grid-view-title { font-size: 1.875rem !important; font-weight: 700; - color: #1a1a1a; + letter-spacing: -0.02em; + line-height: 1.2; + color: var(--bsplus-overview-text); + margin: 0 0 0.35rem; +} + +.grid-view-subtitle { margin: 0; + font-size: 0.9375rem; + line-height: 1.5; + color: var(--bsplus-overview-muted); } -/* Dark mode support */ -.dark .grid-view-title { - color: #f8fafc; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); -} - -.grid-view-filters { +.grid-view-filters, +.bsplus-overview-toolbar { display: flex; + flex-wrap: wrap; gap: 0.75rem; align-items: center; + padding: 0.85rem 1rem; + border-radius: var(--bsplus-overview-radius); + background: var(--bsplus-overview-surface); + border: 1px solid var(--bsplus-overview-border); + box-shadow: var(--bsplus-overview-shadow); } .filter-select { appearance: none; -webkit-appearance: none; -moz-appearance: none; - background: #ffffff !important; + background: var(--bsplus-overview-surface-2) !important; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%2364748b'%3E%3Cpath fill-rule='evenodd' d='M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z' clip-rule='evenodd'/%3E%3C/svg%3E") !important; - background-position: right 0.9rem center !important; + background-position: right 0.75rem center !important; background-repeat: no-repeat !important; background-size: 1rem !important; - border: 2px solid #e2e8f0; - border-radius: 10px; - color: #1a1a1a; + border: 2px solid var(--bsplus-overview-border); + border-radius: var(--bsplus-overview-radius-sm); + color: var(--bsplus-overview-text); color-scheme: light; - padding: 0.75rem 2.5rem 0.75rem 1rem; + padding: 0.65rem 2.25rem 0.65rem 0.9rem; font-size: 0.875rem; font-weight: 500; - font-family: Rubik, sans-serif; + font-family: inherit; line-height: 1.2; - transition: all 0.2s ease; + transition: + border-color 0.2s ease, + box-shadow 0.2s ease, + transform 0.2s var(--bsplus-overview-ease); cursor: pointer; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - min-width: 180px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); + min-width: 11rem; + min-height: 2.75rem; } .filter-select::-ms-expand { @@ -62,45 +150,42 @@ } .filter-select option { - background: #ffffff; - color: #1a1a1a; + background: var(--bsplus-overview-surface); + color: var(--bsplus-overview-text); } .filter-select:focus { outline: none; - border-color: #d41e3a; - box-shadow: 0 0 0 3px rgba(212, 30, 58, 0.1); + border-color: var(--bsplus-overview-accent); + box-shadow: 0 0 0 3px + color-mix(in srgb, var(--bsplus-overview-accent) 22%, transparent); } .filter-select:hover { - border-color: #cbd5e1; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-color: color-mix( + in srgb, + var(--bsplus-overview-accent) 35%, + var(--bsplus-overview-border) + ); } -/* Dark mode dropdowns */ .dark .filter-select { - background: var(--background-primary) !important; + background: var(--bsplus-overview-surface-2) !important; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='rgba(255,255,255,0.72)'%3E%3Cpath fill-rule='evenodd' d='M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z' clip-rule='evenodd'/%3E%3C/svg%3E") !important; - border-color: var(--background-secondary); - color: var(--text-primary); color-scheme: dark; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); -} - -.dark .filter-select:focus { - border-color: #d41e3a; - box-shadow: 0 0 0 3px rgba(212, 30, 58, 0.2); -} - -.dark .filter-select:hover { - border-color: var(--background-secondary); - background: var(--background-secondary) !important; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='rgba(255,255,255,0.72)'%3E%3Cpath fill-rule='evenodd' d='M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z' clip-rule='evenodd'/%3E%3C/svg%3E") !important; } .dark .filter-select option { - background: var(--background-primary); - color: var(--text-primary); + background: var(--bsplus-overview-surface); + color: var(--bsplus-overview-text); +} + +.bsplus-overview-page { + display: flex; + flex-direction: column; + gap: 1.25rem; + flex: 1; + min-height: 0; } #main-grid-content { @@ -121,67 +206,62 @@ .kanban-column-parent { flex: 0 0 320px; - } .kanban-column { max-height: 100%; - background: #f8fafc; - border-radius: 12px; + background: color-mix(in srgb, var(--bsplus-overview-surface-2) 88%, transparent); + border-radius: var(--bsplus-overview-radius); display: flex; flex-direction: column; min-height: 0; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid var(--bsplus-overview-border); + box-shadow: var(--bsplus-overview-shadow); + overflow: hidden; + transition: + box-shadow 0.25s var(--bsplus-overview-ease), + transform 0.25s var(--bsplus-overview-ease); } -/* Dark mode columns */ -.dark .kanban-column { - background: var(--background-primary); - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1); +.kanban-column:hover { + box-shadow: var(--bsplus-overview-shadow-hover); } .column-header { - padding: 1rem 1.25rem; - border-bottom: 2px solid #e2e8f0; - background: #ffffff; - border-radius: 12px 12px 0 0; + padding: 1rem 1.15rem; + border-bottom: 1px solid var(--bsplus-overview-border); + border-radius: var(--bsplus-overview-radius) var(--bsplus-overview-radius) 0 0; position: sticky; top: 0; z-index: 10; } -/* Dark mode column headers */ -.dark .column-header { - background: var(--background-secondary); - border-bottom-color: rgba(255, 255, 255, 0.1); -} - .column-title { - font-size: 1rem; - font-weight: 600; - color: #1a1a1a; + font-size: 0.9375rem; + font-weight: 700; + color: var(--bsplus-overview-text); margin: 0; display: flex; align-items: center; justify-content: space-between; + gap: 0.75rem; } -.dark .column-title { - color: var(--text-primary); +.column-title-main { + display: inline-flex; + align-items: center; + gap: 0.5rem; + min-width: 0; } .column-count { - background: #e2e8f0; - color: #64748b; - padding: 0.25rem 0.5rem; - border-radius: 6px; + background: color-mix(in srgb, var(--bsplus-overview-muted) 18%, transparent); + color: var(--bsplus-overview-muted); + padding: 0.2rem 0.55rem; + border-radius: 999px; font-size: 0.75rem; font-weight: 600; -} - -.dark .column-count { - background: var(--background-secondary); - color: var(--text-primary); + flex-shrink: 0; } .column-cards { @@ -195,33 +275,28 @@ /* Assessment Cards */ .assessment-card { - background: #ffffff; - border-radius: 8px; + background: var(--bsplus-overview-surface); + border-radius: var(--bsplus-overview-radius-sm); padding: 1rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transition: all 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); + transition: + transform 0.2s var(--bsplus-overview-ease), + box-shadow 0.2s var(--bsplus-overview-ease), + border-color 0.2s ease; cursor: pointer; position: relative; - border-left: 4px solid var(--subject-color, #d41e3a); - border: 1px solid #e2e8f0; + border: 1px solid var(--bsplus-overview-border); + border-left: 4px solid var(--subject-color, var(--bsplus-overview-accent)); } .assessment-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - transform: translateY(-1px); - border-color: #cbd5e1; -} - -/* Dark mode cards */ -.dark .assessment-card { - background: var(--background-secondary); - border-color: rgba(255, 255, 255, 0.1); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); -} - -.dark .assessment-card:hover { - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3); - border-color: rgba(255, 255, 255, 0.15); + box-shadow: var(--bsplus-overview-shadow-hover); + transform: translateY(-2px); + border-color: color-mix( + in srgb, + var(--bsplus-overview-accent) 22%, + var(--bsplus-overview-border) + ); } .card-labels { @@ -242,7 +317,7 @@ } .label-subject { - background: var(--subject-color, #d41e3a); + background: color-mix(in srgb, var(--subject-color, var(--bsplus-overview-accent)) 88%, transparent); } .label-student { @@ -266,14 +341,17 @@ border: none !important; padding: 0.25rem !important; cursor: pointer; - border-radius: 4px; - color: #64748b; - transition: all 0.2s ease; + border-radius: 8px; + color: var(--bsplus-overview-muted); + transition: + background 0.2s ease, + color 0.2s ease, + transform 0.2s var(--bsplus-overview-ease); display: flex !important; align-items: center; justify-content: center; - width: 24px !important; - height: 24px !important; + width: 28px !important; + height: 28px !important; margin: 0 !important; position: static !important; transform: none !important; @@ -282,44 +360,32 @@ } .menu-button:hover { - background: #f1f5f9 !important; - color: #1a1a1a; + background: color-mix( + in srgb, + var(--bsplus-overview-surface-2) 90%, + transparent + ) !important; + color: var(--bsplus-overview-text); + transform: scale(1.05) !important; } -.menu-button svg { - width: 16px !important; - height: 16px !important; - fill: currentColor !important; - display: block !important; -} - -.dark .menu-button { - color: var(--text-primary); - opacity: 0.7; -} - -.dark .menu-button:hover { - background: rgba(255, 255, 255, 0.1) !important; - opacity: 1; +.menu-button:focus-visible { + box-shadow: 0 0 0 2px + color-mix(in srgb, var(--bsplus-overview-accent) 35%, transparent) !important; } .menu-dropdown { position: absolute; top: 100%; right: 0; - background: #ffffff; - border: 1px solid #e2e8f0; - border-radius: 6px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - min-width: 140px; + background: var(--bsplus-overview-surface); + border: 1px solid var(--bsplus-overview-border); + border-radius: var(--bsplus-overview-radius-sm); + box-shadow: var(--bsplus-overview-shadow-hover); + min-width: 160px; z-index: 30; margin-top: 4px; -} - -.dark .menu-dropdown { - background: var(--background-secondary); - border-color: rgba(255, 255, 255, 0.1); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + padding: 0.25rem; } .menu-item { @@ -330,22 +396,19 @@ border: none; text-align: left; cursor: pointer; - font-size: 0.875rem; - color: #1a1a1a; - transition: background-color 0.2s ease; + font-size: 0.8125rem; + color: var(--bsplus-overview-text); + transition: background-color 0.15s ease; white-space: nowrap; + border-radius: 8px; } .menu-item:hover { - background: #f8fafc; -} - -.dark .menu-item { - color: var(--text-primary); -} - -.dark .menu-item:hover { - background: rgba(255, 255, 255, 0.05); + background: color-mix( + in srgb, + var(--bsplus-overview-surface-2) 90%, + transparent + ); } .menu-item.mark-completed { @@ -376,68 +439,65 @@ } .visibility-toggle { - padding: 0.5rem 0.75rem; + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.65rem 0.9rem; font-size: 0.875rem; - font-weight: 500; - border-radius: 8px; - border: 2px solid #e2e8f0; - background: #ffffff; - color: #64748b; + font-weight: 600; + border-radius: var(--bsplus-overview-radius-sm); + border: 2px solid var(--bsplus-overview-border); + background: var(--bsplus-overview-surface-2); + color: var(--bsplus-overview-muted); cursor: pointer; - transition: all 0.2s ease; + transition: + border-color 0.2s ease, + background 0.2s ease, + color 0.2s ease, + transform 0.2s var(--bsplus-overview-ease); + min-height: 2.75rem; } .visibility-toggle:hover { - border-color: #cbd5e1; - color: #1a1a1a; + border-color: color-mix( + in srgb, + var(--bsplus-overview-accent) 35%, + var(--bsplus-overview-border) + ); + color: var(--bsplus-overview-text); + transform: scale(1.02); } .visibility-toggle.active { - border-color: #d41e3a; - background: rgba(212, 30, 58, 0.08); - color: #d41e3a; -} - -.dark .visibility-toggle { - background: var(--background-primary); - border-color: var(--background-secondary); - color: var(--text-primary); -} - -.dark .visibility-toggle:hover { - border-color: rgba(255, 255, 255, 0.2); -} - -.dark .visibility-toggle.active { - border-color: #d41e3a; - background: rgba(212, 30, 58, 0.15); - color: #d41e3a; + border-color: color-mix( + in srgb, + var(--bsplus-overview-accent) 45%, + var(--bsplus-overview-border) + ); + background: color-mix( + in srgb, + var(--bsplus-overview-accent) 10%, + var(--bsplus-overview-surface-2) + ); + color: var(--bsplus-overview-accent); } .visibility-panel { - padding: 1rem 1.25rem; - margin: 0 1rem 1rem; - background: #f8fafc; - border-radius: 8px; - border: 1px solid #e2e8f0; -} - -.dark .visibility-panel { - background: var(--background-secondary); - border-color: rgba(255, 255, 255, 0.1); + padding: 1rem 1.15rem; + margin: 0; + background: var(--bsplus-overview-surface); + border-radius: var(--bsplus-overview-radius); + border: 1px solid var(--bsplus-overview-border); + box-shadow: var(--bsplus-overview-shadow); } .visibility-panel-title { font-size: 0.875rem; - font-weight: 600; - color: #1a1a1a; + font-weight: 700; + color: var(--bsplus-overview-text); margin: 0 0 0.75rem; } -.dark .visibility-panel-title { - color: var(--text-primary); -} - .visibility-section { margin-bottom: 0.5rem; } @@ -447,16 +507,13 @@ } .visibility-label { - font-size: 0.75rem; - font-weight: 500; - color: #64748b; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--bsplus-overview-muted); display: block; - margin-bottom: 0.25rem; -} - -.dark .visibility-label { - color: var(--text-primary); - opacity: 0.7; + margin-bottom: 0.35rem; } .visibility-chips { @@ -470,48 +527,40 @@ align-items: center; gap: 0.5rem; padding: 0.25rem 0.5rem; - background: #e2e8f0; - border-radius: 6px; + background: color-mix(in srgb, var(--bsplus-overview-surface-2) 92%, transparent); + border: 1px solid var(--bsplus-overview-border); + border-radius: 999px; font-size: 0.8125rem; - color: #1a1a1a; - max-width: 200px; + color: var(--bsplus-overview-text); + max-width: 220px; overflow: hidden; text-overflow: ellipsis; } -.dark .visibility-chip { - background: rgba(255, 255, 255, 0.1); - color: var(--text-primary); -} - .visibility-unhide { - padding: 0.125rem 0.5rem; + padding: 0.125rem 0.55rem; font-size: 0.75rem; - font-weight: 500; - border-radius: 4px; + font-weight: 600; + border-radius: 999px; border: none; - background: #d41e3a; - color: white; + background: var(--bsplus-overview-accent); + color: var(--text-color, #fff); cursor: pointer; - transition: all 0.2s ease; + transition: transform 0.2s var(--bsplus-overview-ease); flex-shrink: 0; } .visibility-unhide:hover { - background: #b91c33; + transform: scale(1.05); } .assessment-title { font-size: 0.875rem; font-weight: 600; - color: #1a1a1a; + color: var(--bsplus-overview-text); margin: 0 0 0.75rem; - line-height: 1.4; - padding-right: 2rem; /* Make room for menu button */ -} - -.dark .assessment-title { - color: var(--text-primary); + line-height: 1.45; + padding-right: 2rem; } .assessment-meta { @@ -520,12 +569,7 @@ justify-content: space-between; margin-top: 0.75rem; font-size: 0.75rem; - color: #64748b; -} - -.dark .assessment-meta { - color: var(--text-primary); - opacity: 0.7; + color: var(--bsplus-overview-muted); } .due-date { @@ -553,11 +597,7 @@ justify-content: flex-start; margin-top: 0.75rem; padding-top: 0.75rem; - border-top: 1px solid #e5e7eb; -} - -.dark .card-footer { - border-top-color: rgba(255, 255, 255, 0.1); + border-top: 1px solid var(--bsplus-overview-border); } .grade-display { @@ -600,54 +640,101 @@ border-color: var(--background-secondary); } -/* Column-specific styling */ +/* Column header gradients β€” softer tints, same status colours */ .column-upcoming .column-header { - background: linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, #38bdf8 9%, var(--bsplus-overview-surface)) 100% + ); } .column-due-soon .column-header { - background: linear-gradient(135deg, #ffffff 0%, #fffbeb 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, #fbbf24 10%, var(--bsplus-overview-surface)) 100% + ); } .column-overdue .column-header { - background: linear-gradient(135deg, #ffffff 0%, #fef2f2 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, #f87171 10%, var(--bsplus-overview-surface)) 100% + ); } .column-submitted .column-header { - background: linear-gradient(135deg, #ffffff 0%, #fef3c7 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, #f59e0b 9%, var(--bsplus-overview-surface)) 100% + ); } .column-marked .column-header { - background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, #34d399 10%, var(--bsplus-overview-surface)) 100% + ); } .column-custom .column-header { - background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, var(--bsplus-overview-accent) 8%, var(--bsplus-overview-surface)) 100% + ); } -/* Dark mode column headers */ .dark .column-upcoming .column-header { - background: linear-gradient(135deg, var(--background-secondary) 0%, #1e3a8a 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, #3b82f6 14%, var(--bsplus-overview-surface-2)) 100% + ); } .dark .column-due-soon .column-header { - background: linear-gradient(135deg, var(--background-secondary) 0%, #92400e 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, #f59e0b 14%, var(--bsplus-overview-surface-2)) 100% + ); } .dark .column-overdue .column-header { - background: linear-gradient(135deg, var(--background-secondary) 0%, #991b1b 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, #ef4444 13%, var(--bsplus-overview-surface-2)) 100% + ); } .dark .column-submitted .column-header { - background: linear-gradient(135deg, var(--background-secondary) 0%, #92400e 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, #d97706 12%, var(--bsplus-overview-surface-2)) 100% + ); } .dark .column-marked .column-header { - background: linear-gradient(135deg, var(--background-secondary) 0%, #065f46 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, #10b981 13%, var(--bsplus-overview-surface-2)) 100% + ); } .dark .column-custom .column-header { - background: linear-gradient(135deg, var(--background-secondary) 0%, #1e3a5f 100%); + background: linear-gradient( + 135deg, + var(--bsplus-overview-surface) 0%, + color-mix(in srgb, var(--bsplus-overview-accent) 12%, var(--bsplus-overview-surface-2)) 100% + ); } /* Subject filter view */ @@ -703,73 +790,54 @@ } /* Loading and error states */ -.loading-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 4rem 0; - background: #ffffff; - border-radius: 12px; - border: 2px solid #e2e8f0; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.dark .loading-container { - background: var(--background-primary); - border-color: rgba(255, 255, 255, 0.1); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); -} - -.loading-spinner { - width: 2.5rem; - height: 2.5rem; - border: 3px solid #e2e8f0; - border-top: 3px solid #d41e3a; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -.dark .loading-spinner { - border-color: rgba(255, 255, 255, 0.1); - border-top-color: #d41e3a; -} - -.loading-text { - margin-top: 1rem; - color: #64748b; - font-size: 0.875rem; - font-weight: 500; -} - -.dark .loading-text { - color: var(--text-primary); - opacity: 0.7; -} - +.loading-container, .error-container { display: flex; flex-direction: column; justify-content: center; align-items: center; - padding: 4rem 0; - background: #ffffff; - border-radius: 12px; - border: 2px solid #fecaca; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + padding: 3rem 2rem; + background: var(--bsplus-overview-surface); + border-radius: var(--bsplus-overview-radius); + border: 1px solid var(--bsplus-overview-border); + box-shadow: var(--bsplus-overview-shadow); } -.dark .error-container { - background: var(--background-primary); - border-color: #991b1b; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +.loading-spinner { + width: 3rem; + height: 3rem; + border: 4px solid var(--bsplus-overview-border); + border-top-color: var(--bsplus-overview-accent); + border-radius: 50%; + animation: spin 0.75s linear infinite; +} + +.loading-text { + margin-top: 1rem; + color: var(--bsplus-overview-muted); + font-size: 0.875rem; + font-weight: 500; +} + +.error-icon { + color: #ef4444; + margin-bottom: 0.75rem; + opacity: 0.85; } .error-text { color: #ef4444; font-size: 1rem; - font-weight: 600; - margin-bottom: 0.5rem; + font-weight: 700; + margin: 0 0 0.35rem; +} + +.error-detail { + color: var(--bsplus-overview-muted); + font-size: 0.875rem; + margin: 0; + text-align: center; + max-width: 28rem; } .empty-state { @@ -777,15 +845,20 @@ flex-direction: column; justify-content: center; align-items: center; - padding: 2rem; - color: #64748b; - font-size: 0.875rem; + gap: 0.75rem; + padding: 3rem 2rem; + color: var(--bsplus-overview-muted); + font-size: 0.9375rem; text-align: center; + border-radius: var(--bsplus-overview-radius); + background: var(--bsplus-overview-surface); + border: 1px solid var(--bsplus-overview-border); + box-shadow: var(--bsplus-overview-shadow); } -.dark .empty-state { - color: var(--text-primary); - opacity: 0.7; +.empty-icon { + color: var(--bsplus-overview-muted); + opacity: 0.55; } .empty-column { @@ -805,12 +878,6 @@ opacity: 0.7; } -.empty-icon { - font-size: 2rem; - margin-bottom: 0.5rem; - opacity: 0.5; -} - @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } @@ -886,33 +953,41 @@ /* Responsive design */ @media (max-width: 768px) { - #grid-view-container { - padding: 1rem; + #grid-view-container, + .bsplus-overview-root { + padding: 1.25rem 1rem 1.5rem; + gap: 1rem; } - + .grid-view-header { flex-direction: column; gap: 1rem; align-items: stretch; } - - .grid-view-filters { - justify-content: center; - flex-wrap: wrap; + + .grid-view-filters, + .bsplus-overview-toolbar { + justify-content: stretch; } - + .kanban-board { flex-direction: column; gap: 1rem; + padding: 0; } - - .kanban-column { + + .kanban-column-parent { flex: none; + width: 100%; + } + + .kanban-column { max-height: none; } - + .filter-select { - min-width: 140px; + min-width: 0; + flex: 1 1 140px; } } diff --git a/src/plugins/built-in/assessmentsOverview/ui.ts b/src/plugins/built-in/assessmentsOverview/ui.ts index d2bdae5a..c1d8484d 100644 --- a/src/plugins/built-in/assessmentsOverview/ui.ts +++ b/src/plugins/built-in/assessmentsOverview/ui.ts @@ -1,45 +1,155 @@ import renderSvelte from "@/interface/main"; +import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import AssessmentsOverview from "./AssessmentsOverview.svelte"; import SkeletonLoader from "./SkeletonLoader.svelte"; import ErrorState from "./ErrorState.svelte"; import { unmount } from "svelte"; let currentApp: any = null; +let themeObserver: MutationObserver | null = null; +type ThemeSettingKey = + | "selectedColor" + | "DarkMode" + | "adaptiveThemeColour" + | "adaptiveThemeGradient" + | "selectedTheme"; + +let themeListeners: Array<{ key: ThemeSettingKey; listener: () => void }> = []; + +const THEME_CSS_VARS = [ + "--better-main", + "--better-pale", + "--better-light", + "--text-color", + "--background-primary", + "--background-secondary", + "--text-primary", + "--theme-offset-bg", + "--better-sub", +] as const; + +const ACCENT_CSS_VARS = [ + "--better-main", + "--accent-color-value", + "--accentColor", + "--colour-betterseqta-blue", +] as const; + +function extractSolidColor(value: string): string | null { + const trimmed = value.trim(); + if (!trimmed || trimmed === "initial") return null; + if ( + trimmed.startsWith("#") || + trimmed.startsWith("rgb") || + trimmed.startsWith("hsl") + ) { + return trimmed; + } + if (trimmed.includes("gradient")) { + const match = trimmed.match( + /#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgba?\([^)]+\)/i, + ); + return match?.[0] ?? null; + } + return null; +} + +function resolvePageAccentColor(): string { + const computed = getComputedStyle(document.documentElement); + for (const name of ACCENT_CSS_VARS) { + const solid = extractSolidColor(computed.getPropertyValue(name)); + if (solid) return solid; + } + const fromSettings = settingsState.selectedColor?.trim(); + if (fromSettings) { + const solid = extractSolidColor(fromSettings); + if (solid) return solid; + } + return "#007bff"; +} + +function syncOverviewTheme(target: HTMLElement) { + const computed = getComputedStyle(document.documentElement); + for (const name of THEME_CSS_VARS) { + const value = document.documentElement.style.getPropertyValue(name).trim() + || computed.getPropertyValue(name).trim(); + if (value) target.style.setProperty(name, value); + } + + const accent = resolvePageAccentColor(); + target.style.setProperty("--bsplus-overview-accent", accent); + target.style.setProperty("--better-main", accent); + target.classList.toggle( + "dark", + document.documentElement.classList.contains("dark"), + ); +} + +function watchOverviewTheme(root: HTMLElement) { + for (const { key, listener } of themeListeners) { + settingsState.unregister(key, listener); + } + themeListeners = []; + + const listener = () => syncOverviewTheme(root); + for (const key of [ + "selectedColor", + "DarkMode", + "adaptiveThemeColour", + "adaptiveThemeGradient", + "selectedTheme", + ] satisfies ThemeSettingKey[]) { + settingsState.register(key, listener); + themeListeners.push({ key, listener }); + } + + themeObserver?.disconnect(); + themeObserver = new MutationObserver(() => syncOverviewTheme(root)); + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["style", "class"], + }); +} + +function prepareContainer(container: HTMLElement) { + container.innerHTML = ""; + container.className = "bsplus-overview-host"; + container.classList.add("bsplus-overview-root"); + syncOverviewTheme(container); + watchOverviewTheme(container); +} export function renderGrid(container: HTMLElement, data: any) { - if (currentApp) { - unmount(currentApp); - } - - container.innerHTML = ""; - container.className = ""; - + if (currentApp) unmount(currentApp); + prepareContainer(container); currentApp = renderSvelte(AssessmentsOverview, container, { data }); } export function renderSkeletonLoader(container: HTMLElement) { - if (currentApp) { - unmount(currentApp); - } - - container.innerHTML = ""; - container.className = ""; - + if (currentApp) unmount(currentApp); + prepareContainer(container); currentApp = renderSvelte(SkeletonLoader, container); } - export function renderLoadingState(container: HTMLElement) { renderSkeletonLoader(container); } export function renderErrorState(container: HTMLElement, error: string) { + if (currentApp) unmount(currentApp); + prepareContainer(container); + currentApp = renderSvelte(ErrorState, container, { error }); +} + +export function teardownOverviewUi() { + for (const { key, listener } of themeListeners) { + settingsState.unregister(key, listener); + } + themeListeners = []; + themeObserver?.disconnect(); + themeObserver = null; if (currentApp) { unmount(currentApp); + currentApp = null; } - - container.innerHTML = ""; - container.className = ""; - - currentApp = renderSvelte(ErrorState, container, { error }); -} \ No newline at end of file +} diff --git a/src/plugins/built-in/assessmentsOverview/utils.ts b/src/plugins/built-in/assessmentsOverview/utils.ts index f058fd80..03c95b3c 100644 --- a/src/plugins/built-in/assessmentsOverview/utils.ts +++ b/src/plugins/built-in/assessmentsOverview/utils.ts @@ -1,3 +1,115 @@ +export interface OverviewSubject { + code: string; + programme: number; + metaclass: number; + title: string; +} + +function isActiveTermFlag(active: unknown): boolean { + return active === 1 || active === true; +} + +export function normalizeOverviewSubject(raw: unknown): OverviewSubject | null { + if (!raw || typeof raw !== "object") return null; + + const subject = raw as Record; + const programme = Number(subject.programme ?? subject.programmeID); + const metaclass = Number(subject.metaclass ?? subject.metaclassID); + if (!programme || !metaclass || Number.isNaN(programme) || Number.isNaN(metaclass)) { + return null; + } + + const code = String(subject.code ?? subject.subject ?? "").trim(); + if (!code) return null; + + return { + code, + programme, + metaclass, + title: String(subject.title ?? subject.description ?? code), + }; +} + +/** Subjects from the active programme-year folder(s) in `/seqta/student/load/subjects`. */ +export function activeSubjectsFromLearnPayload(payload: unknown): OverviewSubject[] { + if (!Array.isArray(payload)) return []; + + const subjects: OverviewSubject[] = []; + const seen = new Set(); + + for (const folder of payload) { + if (!folder || typeof folder !== "object") continue; + const term = folder as { active?: unknown; subjects?: unknown[] }; + if (!isActiveTermFlag(term.active) || !Array.isArray(term.subjects)) continue; + + for (const raw of term.subjects) { + const subject = normalizeOverviewSubject(raw); + if (!subject) continue; + const key = `${subject.programme}-${subject.metaclass}`; + if (seen.has(key)) continue; + seen.add(key); + subjects.push(subject); + } + } + + return subjects; +} + +export function activeSubjectsFromEngageChild(child: { + terms?: { active?: number; subjects?: unknown[] }[]; +}): OverviewSubject[] { + const subjects: OverviewSubject[] = []; + const seen = new Set(); + + for (const term of child.terms ?? []) { + if (term.active !== 1) continue; + for (const raw of term.subjects ?? []) { + const subject = normalizeOverviewSubject(raw); + if (!subject) continue; + const key = `${subject.programme}-${subject.metaclass}`; + if (seen.has(key)) continue; + seen.add(key); + subjects.push(subject); + } + } + + return subjects; +} + +export function assessmentBelongsToActiveSubjects( + assessment: Record, + activeSubjects: OverviewSubject[], +): boolean { + if (!activeSubjects.length) return false; + + const programme = Number( + assessment.programmeID ?? assessment.programme, + ); + const metaclass = Number( + assessment.metaclassID ?? assessment.metaclass, + ); + + if (programme && metaclass && !Number.isNaN(programme) && !Number.isNaN(metaclass)) { + return activeSubjects.some( + (subject) => + subject.programme === programme && subject.metaclass === metaclass, + ); + } + + const code = String(assessment.code ?? assessment.subject ?? "").trim(); + if (!code) return false; + return activeSubjects.some((subject) => subject.code === code); +} + +export function filterAssessmentsForActiveSubjects>( + assessments: T[], + activeSubjects: OverviewSubject[], +): T[] { + return assessments.filter((assessment) => + assessmentBelongsToActiveSubjects(assessment, activeSubjects), + ); +} + export function formatDate(dateStr: string, submitted?: boolean): string { const d = new Date(dateStr); const now = new Date();