feat: replace sidebar with svelte alternative

This commit is contained in:
2026-07-20 16:33:23 +09:30
parent 824cedca2f
commit c231a00db3
25 changed files with 1884 additions and 187 deletions
@@ -66,11 +66,16 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
label.textContent = "Overview";
gridItem.appendChild(label);
menu.insertBefore(gridItem, menu.firstChild);
window.dispatchEvent(new CustomEvent("bsplus-native-menu-updated"));
let loadRequestId = 0;
const menuObserver = new MutationObserver(() => {
const before = menu.firstElementChild;
ensureOverviewMenuPosition(menu, gridItem);
if (menu.firstElementChild !== before) {
window.dispatchEvent(new CustomEvent("bsplus-native-menu-updated"));
}
});
menuObserver.observe(menu, { childList: true });
@@ -1,87 +0,0 @@
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 {
ensureAnalyticsMenuOrder,
insertMenuItemAfterKey,
processMenuItemNode,
} from "@/seqta/utils/sidebarMenuIcons";
import {
ChangeMenuItemPositions,
MenuOptionsOpen,
} from "@/seqta/utils/Openers/OpenMenuOptions";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { applyMenuItemVisibility } from "@/seqta/utils/menuItemVisibility";
import { loadAnalyticsPage } from "../loadAnalyticsPage";
import styles from "../styles.css?inline";
const ANALYTICS_MENU_ICON = MenuitemSVGKey.analytics;
const ANALYTICS_MENU_CLASS = "betterseqta-grade-analytics-item";
const gradeAnalyticsPlugin: Plugin<{}> = {
id: "grade-analytics",
name: "Grade Analytics",
description:
"Adds an analytics page with grade trends, distribution charts, and assessment history",
version: "1.0.0",
settings: {},
disableToggle: true,
styles,
run: async () => {
if (isSeqtaEngageExperience()) {
return () => {};
}
const menuList = (await waitForElm("#menu > ul, #menu ul", true, 100, 60)) as HTMLElement;
const analyticsItem = document.createElement("li");
analyticsItem.className = "item";
analyticsItem.classList.add(ANALYTICS_MENU_CLASS);
analyticsItem.id = "analyticsbutton";
analyticsItem.dataset.key = "analytics";
analyticsItem.dataset.path = "/analytics";
analyticsItem.dataset.betterseqta = "true";
analyticsItem.innerHTML = `<label>${ANALYTICS_MENU_ICON}<span>Analytics</span></label>`;
const syncAnalyticsMenu = () => {
insertMenuItemAfterKey(menuList, analyticsItem, "courses");
ensureAnalyticsMenuOrder();
if (settingsState.menuorder.length > 0) {
ChangeMenuItemPositions(settingsState.menuorder);
}
processMenuItemNode(analyticsItem);
applyMenuItemVisibility();
};
syncAnalyticsMenu();
const menuObserver = new MutationObserver(() => {
if (MenuOptionsOpen || menuList.contains(analyticsItem)) return;
syncAnalyticsMenu();
});
menuObserver.observe(menuList, { childList: true });
analyticsItem.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
if (
MenuOptionsOpen ||
analyticsItem.classList.contains("draggable") ||
target.closest(".onoffswitch, .editmenuoption-container")
) {
return;
}
e.preventDefault();
window.history.pushState({}, "", "/#?page=/analytics");
void loadAnalyticsPage();
});
return () => {
menuObserver.disconnect();
analyticsItem.remove();
};
},
};
export default gradeAnalyticsPlugin;
@@ -0,0 +1,125 @@
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
import { waitForElm } from "@/seqta/utils/waitForElm";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
import {
ensureAnalyticsMenuOrder,
insertMenuItemAfterKey,
} from "@/seqta/utils/Openers/analyticsMenuOrder";
import { processMenuItemNode } from "@/seqta/utils/sidebarMenuIcons";
import { ChangeMenuItemPositions } from "@/seqta/utils/Openers/menuOrder";
import { isMenuOptionsOpen } from "@/seqta/utils/Openers/menuOptionsState";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { applyMenuItemVisibility } from "@/seqta/utils/menuItemVisibility";
const ANALYTICS_MENU_ICON = MenuitemSVGKey.analytics;
export const ANALYTICS_MENU_CLASS = "betterseqta-grade-analytics-item";
export const ANALYTICS_MENU_KEY = "analytics";
function getNativeMenuList(): HTMLElement | null {
const menu = document.getElementById("menu");
if (!menu) return null;
return menu.querySelector(
":scope > ul:not(#bsplus-sidebar-root)",
) as HTMLElement | null;
}
function loadAnalyticsPageLazy() {
void import("./loadAnalyticsPage").then((m) => m.loadAnalyticsPage());
}
/**
* Inject the Analytics sidebar row immediately (eager).
* The analytics page chunk stays lazy until click / direct navigation.
*/
export async function injectAnalyticsMenuItem(): Promise<() => void> {
if (isSeqtaEngageExperience()) {
return () => {};
}
document
.querySelectorAll(
`#menu .${ANALYTICS_MENU_CLASS}, #menu [data-key="${ANALYTICS_MENU_KEY}"]`,
)
.forEach((node) => {
// Only touch the native list copy — custom sidebar mirrors via sync.
if (node.closest("#bsplus-sidebar-root")) return;
node.remove();
});
await waitForElm(
"#menu > ul:not(#bsplus-sidebar-root), #menu > ul",
true,
50,
120,
);
let menuList = getNativeMenuList();
if (!menuList) {
menuList = (await waitForElm(
"#menu > ul:not(#bsplus-sidebar-root), #menu ul",
true,
50,
120,
)) as HTMLElement;
}
const analyticsItem = document.createElement("li");
analyticsItem.className = "item";
analyticsItem.classList.add(ANALYTICS_MENU_CLASS);
analyticsItem.id = "analyticsbutton";
analyticsItem.dataset.key = ANALYTICS_MENU_KEY;
analyticsItem.dataset.path = "/analytics";
analyticsItem.dataset.betterseqta = "true";
analyticsItem.innerHTML = `<label>${ANALYTICS_MENU_ICON}<span>Analytics</span></label>`;
const syncAnalyticsMenu = () => {
const list = getNativeMenuList() ?? menuList;
if (!list) return;
if (list.querySelector(':scope > [data-key="courses"]')) {
insertMenuItemAfterKey(list, analyticsItem, "courses");
} else if (list.querySelector(':scope > [data-key="home"]')) {
insertMenuItemAfterKey(list, analyticsItem, "home");
} else if (!list.contains(analyticsItem)) {
list.insertBefore(analyticsItem, list.firstChild);
}
ensureAnalyticsMenuOrder();
if (settingsState.menuorder.length > 0) {
ChangeMenuItemPositions(settingsState.menuorder);
}
processMenuItemNode(analyticsItem);
applyMenuItemVisibility();
window.dispatchEvent(new CustomEvent("bsplus-native-menu-updated"));
};
syncAnalyticsMenu();
const menuObserver = new MutationObserver(() => {
const list = getNativeMenuList() ?? menuList;
if (!list || isMenuOptionsOpen() || list.contains(analyticsItem)) return;
syncAnalyticsMenu();
});
menuObserver.observe(menuList, { childList: true });
const onClick = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (
isMenuOptionsOpen() ||
analyticsItem.classList.contains("draggable") ||
target.closest(".onoffswitch, .editmenuoption-container")
) {
return;
}
e.preventDefault();
window.history.pushState({}, "", "/#?page=/analytics");
loadAnalyticsPageLazy();
};
analyticsItem.addEventListener("click", onClick);
return () => {
menuObserver.disconnect();
analyticsItem.removeEventListener("click", onClick);
analyticsItem.remove();
};
}
+14 -4
View File
@@ -1,6 +1,7 @@
import { defineLazyPlugin } from "../../core/dynamicLoader";
import { defineSettings, numberSetting } from "../../core/settingsHelpers";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
import { injectAnalyticsMenuItem } from "./injectAnalyticsMenuItem";
import styles from "./styles.css?inline";
const settings = defineSettings({
@@ -13,6 +14,10 @@ const settings = defineSettings({
}),
});
/**
* Shell loads immediately so the sidebar Analytics icon can mount.
* The page UI (`loadAnalyticsPage` / charts) stays in a separate lazy chunk.
*/
const gradeAnalyticsPluginLazy = defineLazyPlugin({
id: "grade-analytics",
name: "Grade Analytics",
@@ -23,16 +28,21 @@ const gradeAnalyticsPluginLazy = defineLazyPlugin({
disableToggle: true,
defaultEnabled: true,
styles,
// Kept for API compatibility; menu injection no longer waits on this chunk.
loader: () => import("./core/index"),
});
const runGradeAnalytics = gradeAnalyticsPluginLazy.run!;
gradeAnalyticsPluginLazy.run = async (api) => {
gradeAnalyticsPluginLazy.run = async () => {
if (isSeqtaEngageExperience()) {
return () => {};
}
return runGradeAnalytics(api);
// Eager: sidebar icon / native menu row only.
const cleanupMenu = await injectAnalyticsMenuItem();
return () => {
cleanupMenu();
};
};
export default gradeAnalyticsPluginLazy;