diff --git a/src/css/injected.scss b/src/css/injected.scss
index dea7df9a..358da60e 100644
--- a/src/css/injected.scss
+++ b/src/css/injected.scss
@@ -2794,39 +2794,9 @@ body {
.defaultWelcomeWrapper {
background: unset !important;
}
-.clr-swatches button::after,
-.clr-dark .clr-preview::after,
-.clr-field button::after {
- opacity: unset;
- padding-top: unset;
- -webkit-transform: unset;
- transform: unset;
- -webkit-transform-origin: unset;
- transform-origin: unset;
- visibility: unset;
- -webkit-animation-name: unset !important;
- animation-name: unset !important;
- background-color: currentColor !important;
-}
-.clr-swatches button {
- align-items: unset;
- display: block;
- padding: unset;
- transition: none;
-}
-.clr-clear {
- display: none !important;
-}
-.clr-preview::before,
-.clr-preview::after {
- visibility: unset;
- -webkit-transform-origin: unset;
- transform-origin: unset;
- -webkit-transform: unset;
- transform: unset;
- padding-top: unset;
- opacity: unset;
-}
+
+/* Coloris (timetable subject colours): cosmetic only — do not unset
+ transforms/animations on ::after (breaks picker reopen). */
#clr-color-preview {
margin: 15px 0 20px 20px;
border: 0;
@@ -2835,6 +2805,10 @@ body {
cursor: pointer;
}
+.clr-swatches button {
+ border-radius: 4px;
+}
+
.dark
[class*="MessageList__MessageList___"]
> ol
diff --git a/src/plugins/built-in/gradeAnalytics/core/index.ts b/src/plugins/built-in/gradeAnalytics/core/index.ts
index 43313b88..e32da32f 100644
--- a/src/plugins/built-in/gradeAnalytics/core/index.ts
+++ b/src/plugins/built-in/gradeAnalytics/core/index.ts
@@ -2,8 +2,14 @@ import type { Plugin } from "@/plugins/core/types";
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
import { waitForElm } from "@/seqta/utils/waitForElm";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
-import { processMenuItemNode } from "@/seqta/utils/sidebarMenuIcons";
+import {
+ ChangeMenuItemPositions,
+ ensureAnalyticsMenuOrder,
+ insertMenuItemAfterKey,
+ processMenuItemNode,
+} from "@/seqta/utils/sidebarMenuIcons";
import { MenuOptionsOpen } from "@/seqta/utils/Openers/OpenMenuOptions";
+import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import {
applyMenuItemVisibility,
isMenuItemHidden,
@@ -45,11 +51,14 @@ const gradeAnalyticsPlugin: Plugin<{}> = {
analyticsItem.dataset.betterseqta = "true";
analyticsItem.innerHTML = ``;
- const homeButton = document.getElementById("homebutton");
- if (homeButton?.parentElement === menuList) {
- homeButton.insertAdjacentElement("afterend", analyticsItem);
- } else {
- menuList.insertBefore(analyticsItem, menuList.firstChild);
+ const placeAnalyticsItem = () => {
+ insertMenuItemAfterKey(menuList, analyticsItem, "courses");
+ };
+
+ placeAnalyticsItem();
+ ensureAnalyticsMenuOrder();
+ if (settingsState.menuorder.length > 0) {
+ ChangeMenuItemPositions(settingsState.menuorder);
}
processMenuItemNode(analyticsItem);
@@ -59,10 +68,10 @@ const gradeAnalyticsPlugin: Plugin<{}> = {
if (MenuOptionsOpen) return;
if (isMenuItemHidden("analytics")) return;
if (!menuList.contains(analyticsItem)) {
- if (homeButton?.parentElement === menuList) {
- homeButton.insertAdjacentElement("afterend", analyticsItem);
- } else {
- menuList.insertBefore(analyticsItem, menuList.firstChild);
+ placeAnalyticsItem();
+ ensureAnalyticsMenuOrder();
+ if (settingsState.menuorder.length > 0) {
+ ChangeMenuItemPositions(settingsState.menuorder);
}
processMenuItemNode(analyticsItem);
applyMenuItemVisibility();
diff --git a/src/plugins/built-in/timetable/index.ts b/src/plugins/built-in/timetable/index.ts
index 99c9bd0a..950e06bc 100644
--- a/src/plugins/built-in/timetable/index.ts
+++ b/src/plugins/built-in/timetable/index.ts
@@ -1,6 +1,9 @@
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import type { Plugin } from "../../core/types";
-import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
+import {
+ attachTimetableColorisRecovery,
+ resetStuckColorisPicker,
+} from "@/seqta/utils/timetableColoris";
import { waitForElm } from "@/seqta/utils/waitForElm";
const timetablePlugin: Plugin<{}, {}> = {
@@ -63,6 +66,9 @@ function resetTimetableStyles(): void {
}
async function handleTimetable(): Promise {
+ attachTimetableColorisRecovery();
+ resetStuckColorisPicker();
+
// SEQTA uses `.times` blocks on entries, not necessarily `.time`; avoid infinite polling on a missing selector.
try {
await waitForElm(".timetablepage .times, .timetablepage .entry.class", true, 50, 200);
diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts
index da224f48..afb53e55 100644
--- a/src/plugins/monofile.ts
+++ b/src/plugins/monofile.ts
@@ -30,6 +30,7 @@ import { loadAnalyticsPage } from "@/plugins/built-in/gradeAnalytics/loadAnalyti
import { runStartupPopupQueue } from "@/seqta/utils/Openers/StartupPopupQueue";
import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes";
+import { attachTimetableColorisRecovery } from "@/seqta/utils/timetableColoris";
// JSON content
import { observeMenuItemPosition } from "@/seqta/utils/sidebarMenuIcons";
@@ -247,6 +248,7 @@ async function LoadPageElements(): Promise {
className: "timetablepage",
},
async () => {
+ attachTimetableColorisRecovery();
await updateTimetableTimes();
},
);
diff --git a/src/seqta/utils/Openers/OpenMenuOptions.ts b/src/seqta/utils/Openers/OpenMenuOptions.ts
index 157efbe5..ac774238 100644
--- a/src/seqta/utils/Openers/OpenMenuOptions.ts
+++ b/src/seqta/utils/Openers/OpenMenuOptions.ts
@@ -1,6 +1,7 @@
import type { SettingsState } from "@/types/storage";
import { settingsState } from "../listeners/SettingsState";
import { applyMenuItemVisibility } from "../menuItemVisibility";
+import { insertKeyAfterInOrder } from "@/seqta/utils/sidebarMenuIcons";
import stringToHTML from "../stringToHTML";
import Sortable from "sortablejs";
@@ -57,9 +58,18 @@ export function OpenMenuOptions() {
key &&
settingsState.defaultmenuorder.indexOf(key) === -1
) {
- let newdefaultmenuorder = settingsState.defaultmenuorder;
- newdefaultmenuorder.push(key);
- settingsState.defaultmenuorder = newdefaultmenuorder;
+ if (key === "analytics") {
+ settingsState.defaultmenuorder = insertKeyAfterInOrder(
+ settingsState.defaultmenuorder,
+ key,
+ "courses",
+ );
+ } else {
+ settingsState.defaultmenuorder = [
+ ...settingsState.defaultmenuorder,
+ key,
+ ];
+ }
}
}
}
diff --git a/src/seqta/utils/sidebarMenuIcons.ts b/src/seqta/utils/sidebarMenuIcons.ts
index 48b49875..b275174d 100644
--- a/src/seqta/utils/sidebarMenuIcons.ts
+++ b/src/seqta/utils/sidebarMenuIcons.ts
@@ -12,6 +12,58 @@ import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
const BETTERSEQTA_ICON_ATTR = "data-betterseqta-icon";
+export function insertMenuItemAfterKey(
+ menuList: HTMLElement,
+ item: HTMLElement,
+ afterKey: string,
+): void {
+ const after = menuList.querySelector(
+ `:scope > li[data-key="${afterKey}"], :scope > section[data-key="${afterKey}"]`,
+ );
+ if (after instanceof HTMLElement) {
+ after.insertAdjacentElement("afterend", item);
+ } else {
+ menuList.appendChild(item);
+ }
+}
+
+export function insertKeyAfterInOrder(
+ order: string[],
+ key: string,
+ afterKey: string,
+): string[] {
+ if (order.includes(key)) return order;
+ const copy = [...order];
+ const afterIdx = copy.indexOf(afterKey);
+ if (afterIdx >= 0) {
+ copy.splice(afterIdx + 1, 0, key);
+ } else {
+ copy.push(key);
+ }
+ return copy;
+}
+
+/** Default Analytics immediately below Courses in saved menu order. */
+export function ensureAnalyticsMenuOrder(): void {
+ if (!settingsState.defaultmenuorder.includes("analytics")) {
+ settingsState.defaultmenuorder = insertKeyAfterInOrder(
+ settingsState.defaultmenuorder,
+ "analytics",
+ "courses",
+ );
+ }
+ if (
+ settingsState.menuorder.length > 0 &&
+ !settingsState.menuorder.includes("analytics")
+ ) {
+ settingsState.menuorder = insertKeyAfterInOrder(
+ settingsState.menuorder,
+ "analytics",
+ "courses",
+ );
+ }
+}
+
function getMenuLabel(element: HTMLElement): HTMLElement | null {
const label = element.querySelector(":scope > label");
return label instanceof HTMLElement ? label : null;
diff --git a/src/seqta/utils/timetableColoris.ts b/src/seqta/utils/timetableColoris.ts
new file mode 100644
index 00000000..f9adcd7e
--- /dev/null
+++ b/src/seqta/utils/timetableColoris.ts
@@ -0,0 +1,47 @@
+/**
+ * SEQTA timetable uses Coloris for subject colours. Extension CSS previously
+ * unset Coloris ::after animations, which left the picker unable to reopen.
+ * This module clears stuck `clr-open` / hidden picker state after each use.
+ */
+
+let attached = false;
+
+export function resetStuckColorisPicker(): void {
+ document.body.classList.remove("clr-open");
+ document.documentElement.classList.remove("clr-open");
+
+ for (const picker of document.querySelectorAll(".clr-picker")) {
+ picker.classList.remove("clr-open");
+ if (picker instanceof HTMLElement) {
+ picker.style.removeProperty("display");
+ picker.style.removeProperty("pointer-events");
+ picker.style.removeProperty("visibility");
+ }
+ }
+
+ for (const field of document.querySelectorAll(".clr-field")) {
+ field.classList.remove("clr-open");
+ }
+}
+
+export function attachTimetableColorisRecovery(): void {
+ if (attached) return;
+ attached = true;
+
+ const afterColorisEvent = () => {
+ requestAnimationFrame(() => resetStuckColorisPicker());
+ };
+
+ document.addEventListener("coloris:pick", afterColorisEvent);
+ document.addEventListener("coloris:close", afterColorisEvent);
+
+ document.addEventListener(
+ "click",
+ (event) => {
+ const target = event.target as HTMLElement;
+ if (!target.closest(".timetablepage [title='Choose a colour']")) return;
+ resetStuckColorisPicker();
+ },
+ true,
+ );
+}