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
+19 -36
View File
@@ -1,11 +1,17 @@
import type { SettingsState } from "@/types/storage";
import { settingsState } from "../listeners/SettingsState";
import { applyMenuItemVisibility } from "../menuItemVisibility";
import { ensureAnalyticsMenuOrder } from "@/seqta/utils/sidebarMenuIcons";
import { ensureAnalyticsMenuOrder } from "@/seqta/utils/Openers/analyticsMenuOrder";
import {
isMenuOptionsOpen,
setMenuOptionsOpen,
} from "@/seqta/utils/Openers/menuOptionsState";
import { ChangeMenuItemPositions } from "@/seqta/utils/Openers/menuOrder";
import stringToHTML from "../stringToHTML";
import Sortable from "sortablejs";
export let MenuOptionsOpen = false;
export { MenuOptionsOpen, setMenuOptionsOpen } from "@/seqta/utils/Openers/menuOptionsState";
export { ChangeMenuItemPositions } from "@/seqta/utils/Openers/menuOrder";
function escapeHtmlAttr(value: string): string {
return value
@@ -190,14 +196,22 @@ function restoreMenuItemsFromEditMode(
}
export function OpenMenuOptions() {
if (MenuOptionsOpen) return;
if (isMenuOptionsOpen()) return;
// Custom Svelte sidebar owns edit mode when mounted.
if (document.getElementById("bsplus-sidebar-root")) {
void import("@/seqta/ui/sidebar/mountCustomSidebar").then((mod) => {
if (mod.openCustomSidebarEditor()) setMenuOptionsOpen(true);
});
return;
}
const container = document.getElementById("container");
const menu = document.getElementById("menu");
if (!container || !menu) return;
syncDefaultMenuOrder(menu);
MenuOptionsOpen = true;
setMenuOptionsOpen(true);
menu.classList.add("bsplus-sidebar-edit-mode");
const { cover, menusettings, defaultbutton, savebutton } =
@@ -220,7 +234,7 @@ export function OpenMenuOptions() {
const closeAll = () => {
menusettings.remove();
cover.remove();
MenuOptionsOpen = false;
setMenuOptionsOpen(false);
menu.classList.remove("bsplus-sidebar-edit-mode");
menu.style.setProperty("--menuHidden", "none");
restoreMenuItemsFromEditMode(menu, listItems);
@@ -267,34 +281,3 @@ function cloneAttributes(target: any, source: any) {
target.setAttribute(attr.nodeName, attr.nodeValue);
});
}
export function ChangeMenuItemPositions(menuorder: SettingsState["menuorder"]) {
var menuList = document.querySelector("#menu")!.firstChild!.childNodes;
let listorder = [];
for (let i = 0; i < menuList.length; i++) {
const menu = menuList[i] as HTMLElement;
let a = menuorder.indexOf(menu.dataset.key);
listorder.push(a);
}
var newArr = [];
for (var i = 0; i < listorder.length; i++) {
const index = listorder[i];
if (index >= 0) {
newArr[index] = menuList[i];
}
}
let listItemsDOM = document.getElementById("menu")!.firstChild;
for (let i = 0; i < newArr.length; i++) {
const element = newArr[i];
if (element) {
const elem = element as HTMLElement;
elem.setAttribute("data-checked", "true");
listItemsDOM!.appendChild(element);
}
}
}
@@ -0,0 +1,44 @@
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
/** Insert a menu DOM node after the top-level item with `afterKey`. */
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 {
for (const key of ["defaultmenuorder", "menuorder"] as const) {
const order = settingsState[key];
if (key === "menuorder" && order.length === 0) continue;
if (!order.includes("analytics")) {
settingsState[key] = insertKeyAfterInOrder(order, "analytics", "courses");
}
}
}
@@ -0,0 +1,13 @@
/**
* Tiny flag module with no imports — breaks the OpenMenuOptions ↔ sidebarMenuIcons
* cycle that caused Vite HMR: "does not provide an export named 'MenuOptionsOpen'".
*/
export let MenuOptionsOpen = false;
export function setMenuOptionsOpen(open: boolean) {
MenuOptionsOpen = open;
}
export function isMenuOptionsOpen(): boolean {
return MenuOptionsOpen;
}
+32
View File
@@ -0,0 +1,32 @@
import type { SettingsState } from "@/types/storage";
/** Reorder `#menu > ul` children to match a saved `menuorder` key list. */
export function ChangeMenuItemPositions(menuorder: SettingsState["menuorder"]) {
const menuRoot = document.querySelector("#menu")?.firstChild;
if (!menuRoot) return;
const menuList = menuRoot.childNodes;
const listorder: number[] = [];
for (let i = 0; i < menuList.length; i++) {
const menu = menuList[i] as HTMLElement;
listorder.push(menuorder.indexOf(menu.dataset.key));
}
const newArr: ChildNode[] = [];
for (let i = 0; i < listorder.length; i++) {
const index = listorder[i];
if (index >= 0) {
newArr[index] = menuList[i];
}
}
for (let i = 0; i < newArr.length; i++) {
const element = newArr[i];
if (element) {
const elem = element as HTMLElement;
elem.setAttribute("data-checked", "true");
menuRoot.appendChild(element);
}
}
}
+3 -5
View File
@@ -1,10 +1,8 @@
import browser from "webextension-polyfill";
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup";
import {
MenuOptionsOpen,
OpenMenuOptions,
} from "@/seqta/utils/Openers/OpenMenuOptions";
import { OpenMenuOptions } from "@/seqta/utils/Openers/OpenMenuOptions";
import { isMenuOptionsOpen } from "@/seqta/utils/Openers/menuOptionsState";
import sendThemeUpdate from "@/seqta/utils/sendThemeUpdate";
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent";
@@ -144,7 +142,7 @@ export class MessageHandler {
}
editSidebar() {
if (!MenuOptionsOpen) {
if (!isMenuOptionsOpen()) {
OpenMenuOptions();
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { renderShortcuts } from "@/seqta/utils/Render/renderShortcuts";
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
import { registerHomeUpcomingSettingsListeners } from "@/seqta/utils/Loaders/LoadHomePage";
import { applyMenuItemVisibility } from "@/seqta/utils/menuItemVisibility";
import { ChangeMenuItemPositions } from "@/seqta/utils/Openers/OpenMenuOptions";
import { ChangeMenuItemPositions } from "@/seqta/utils/Openers/menuOrder";
import browser from "webextension-polyfill";
import type { CustomShortcut } from "@/types/storage";
+3 -1
View File
@@ -10,7 +10,9 @@ export function applyMenuItemVisibility(): void {
let css = "";
for (const [menuItem, config] of Object.entries(settingsState.menuitems ?? {})) {
if (config && !config.toggle) {
css += `li[data-key=${menuItem}],section[data-key=${menuItem}]{display:var(--menuHidden) !important;transition:1s;}`;
// Scope to the native SEQTA list only — the custom Svelte sidebar
// filters visibility in JS and must not be forced hidden by this rule.
css += `#menu > ul:not(#bsplus-sidebar-root) li[data-key=${menuItem}],#menu > ul:not(#bsplus-sidebar-root) section[data-key=${menuItem}]{display:var(--menuHidden) !important;transition:1s;}`;
}
}
+5 -49
View File
@@ -1,8 +1,6 @@
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
import {
ChangeMenuItemPositions,
MenuOptionsOpen,
} from "@/seqta/utils/Openers/OpenMenuOptions";
import { ChangeMenuItemPositions } from "@/seqta/utils/Openers/menuOrder";
import { isMenuOptionsOpen } from "@/seqta/utils/Openers/menuOptionsState";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import stringToHTML from "@/seqta/utils/stringToHTML";
import { waitForEngageMenuList } from "@/seqta/utils/waitForEngageMenuList";
@@ -12,48 +10,6 @@ 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 {
for (const key of ["defaultmenuorder", "menuorder"] as const) {
const order = settingsState[key];
if (key === "menuorder" && order.length === 0) continue;
if (!order.includes("analytics")) {
settingsState[key] = insertKeyAfterInOrder(order, "analytics", "courses");
}
}
}
function getMenuLabel(element: HTMLElement): HTMLElement | null {
const label = element.querySelector(":scope > label");
return label instanceof HTMLElement ? label : null;
@@ -107,7 +63,7 @@ export function replaceMenuSVG(element: HTMLElement, svg: string) {
}
export function processMenuItemNode(node: HTMLElement) {
if (!isTopLevelSidebarItem(node) || MenuOptionsOpen) return;
if (!isTopLevelSidebarItem(node) || isMenuOptionsOpen()) return;
const key = node.dataset.key as keyof typeof MenuitemSVGKey | undefined;
if (key && MenuitemSVGKey[key]) {
@@ -119,7 +75,7 @@ export function processMenuItemNode(node: HTMLElement) {
}
function processTopLevelMenuItems(reorder = !isSeqtaEngageExperience()) {
if (MenuOptionsOpen) return;
if (isMenuOptionsOpen()) return;
const topList = getTopLevelMenuList();
if (!topList) return;
@@ -186,7 +142,7 @@ export async function observeMenuItemPosition() {
if (!isTopLevelSidebarItem(node)) return;
if ((element as any)[processedSymbol]) return;
if (!MenuOptionsOpen) {
if (!isMenuOptionsOpen()) {
processMenuItemNode(node);
ChangeMenuItemPositions(settingsState.menuorder);
(element as any)[processedSymbol] = true;