mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
feat: timetable editor plugin
This commit is contained in:
@@ -0,0 +1,338 @@
|
|||||||
|
import type { Plugin } from "../../core/types";
|
||||||
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
|
import styles from "./styles.css?inline";
|
||||||
|
|
||||||
|
interface TimetableEntryData {
|
||||||
|
ci: number;
|
||||||
|
description: string;
|
||||||
|
room: string;
|
||||||
|
staff: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimetableOverrides {
|
||||||
|
[ci: string]: { room?: string; staff?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimetableOverridesBySubject {
|
||||||
|
[description: string]: { room?: string; staff?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimetableStorage {
|
||||||
|
timetableOverrides?: TimetableOverrides;
|
||||||
|
timetableOverridesBySubject?: TimetableOverridesBySubject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SEQTA timetable entries use .teacher and .room as direct children, and data-instance for ci */
|
||||||
|
function getRoomAndTeacherElements(entry: HTMLElement): {
|
||||||
|
roomEl: HTMLElement | null;
|
||||||
|
teacherEl: HTMLElement | null;
|
||||||
|
} {
|
||||||
|
const roomEl = entry.querySelector(".room") as HTMLElement | null;
|
||||||
|
const teacherEl = entry.querySelector(".teacher") as HTMLElement | null;
|
||||||
|
return { roomEl, teacherEl };
|
||||||
|
}
|
||||||
|
|
||||||
|
const EDIT_ICON_SVG =
|
||||||
|
'<svg width="24" height="24" viewBox="0 0 24 24"><g style="fill: currentcolor;"><path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z"/></g></svg>';
|
||||||
|
|
||||||
|
function showEditModal(
|
||||||
|
item: TimetableEntryData,
|
||||||
|
overrides: TimetableOverrides | undefined,
|
||||||
|
overridesBySubject: TimetableOverridesBySubject | undefined,
|
||||||
|
onSave: (
|
||||||
|
ci: number,
|
||||||
|
room: string,
|
||||||
|
staff: string,
|
||||||
|
applyToFuture: boolean,
|
||||||
|
) => void,
|
||||||
|
onClear: (ci: number) => void,
|
||||||
|
): void {
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
overlay.className = "timetable-edit-modal-overlay";
|
||||||
|
|
||||||
|
const modal = document.createElement("div");
|
||||||
|
modal.className = "timetable-edit-modal";
|
||||||
|
|
||||||
|
const override = overrides?.[String(item.ci)] ?? overridesBySubject?.[item.description];
|
||||||
|
|
||||||
|
const roomValue = override?.room ?? item.room ?? "";
|
||||||
|
const staffValue = override?.staff ?? item.staff ?? "";
|
||||||
|
|
||||||
|
const escapeHtml = (s: string) =>
|
||||||
|
s.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
||||||
|
const title = escapeHtml(item.description);
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<h3>Edit ${title}</h3>
|
||||||
|
<label for="timetable-edit-room">Room</label>
|
||||||
|
<input type="text" id="timetable-edit-room" value="${roomValue.replace(/"/g, """)}" placeholder="Room" />
|
||||||
|
<label for="timetable-edit-staff">Teacher</label>
|
||||||
|
<input type="text" id="timetable-edit-staff" value="${staffValue.replace(/"/g, """)}" placeholder="Teacher" />
|
||||||
|
<div class="timetable-edit-modal-checkbox">
|
||||||
|
<input type="checkbox" id="timetable-edit-apply-future" />
|
||||||
|
<label for="timetable-edit-apply-future">Apply to future weeks</label>
|
||||||
|
</div>
|
||||||
|
<div class="timetable-edit-modal-actions">
|
||||||
|
${override ? '<button type="button" class="timetable-edit-btn-clear">Clear</button>' : ""}
|
||||||
|
<button type="button" class="timetable-edit-btn-cancel">Cancel</button>
|
||||||
|
<button type="button" class="timetable-edit-btn-save">Save</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
overlay.appendChild(modal);
|
||||||
|
|
||||||
|
const removeModal = () => {
|
||||||
|
overlay.remove();
|
||||||
|
document.removeEventListener("keydown", handleKeydown);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") removeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
overlay.addEventListener("click", (e) => {
|
||||||
|
if (e.target === overlay) removeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.addEventListener("click", (e) => e.stopPropagation());
|
||||||
|
modal.addEventListener("mousedown", (e) => e.stopPropagation());
|
||||||
|
modal.addEventListener("mouseup", (e) => e.stopPropagation());
|
||||||
|
|
||||||
|
const roomInput = modal.querySelector("#timetable-edit-room") as HTMLInputElement;
|
||||||
|
const staffInput = modal.querySelector("#timetable-edit-staff") as HTMLInputElement;
|
||||||
|
const applyFutureCheckbox = modal.querySelector("#timetable-edit-apply-future") as HTMLInputElement;
|
||||||
|
|
||||||
|
modal.querySelector(".timetable-edit-btn-save")?.addEventListener("click", () => {
|
||||||
|
onSave(
|
||||||
|
item.ci,
|
||||||
|
roomInput.value.trim(),
|
||||||
|
staffInput.value.trim(),
|
||||||
|
applyFutureCheckbox?.checked ?? false,
|
||||||
|
);
|
||||||
|
removeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.querySelector(".timetable-edit-btn-cancel")?.addEventListener("click", removeModal);
|
||||||
|
|
||||||
|
const clearBtn = modal.querySelector(".timetable-edit-btn-clear");
|
||||||
|
if (clearBtn) {
|
||||||
|
clearBtn.addEventListener("click", () => {
|
||||||
|
onClear(item.ci);
|
||||||
|
removeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
document.addEventListener("keydown", handleKeydown);
|
||||||
|
roomInput?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||||
|
id: "timetableEdit",
|
||||||
|
name: "Edit Rooms & Teachers",
|
||||||
|
description: "Edit room and teacher names in timetable classes",
|
||||||
|
version: "1.0.0",
|
||||||
|
settings: {},
|
||||||
|
disableToggle: true,
|
||||||
|
defaultEnabled: true,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
const styleEl = document.createElement("style");
|
||||||
|
styleEl.textContent = styles;
|
||||||
|
document.head.appendChild(styleEl);
|
||||||
|
|
||||||
|
await api.storage.loaded;
|
||||||
|
|
||||||
|
let observer: MutationObserver | null = null;
|
||||||
|
let quickbarObserver: MutationObserver | null = null;
|
||||||
|
let lastClickedCi: number | null = null;
|
||||||
|
let lastClickedEntry: { roomEl: HTMLElement; teacherEl: HTMLElement; item: TimetableEntryData } | null = null;
|
||||||
|
|
||||||
|
const getOverrides = (): TimetableOverrides =>
|
||||||
|
api.storage.timetableOverrides ?? {};
|
||||||
|
const getOverridesBySubject = (): TimetableOverridesBySubject =>
|
||||||
|
api.storage.timetableOverridesBySubject ?? {};
|
||||||
|
|
||||||
|
const getEffectiveOverride = (
|
||||||
|
ci: number,
|
||||||
|
description: string,
|
||||||
|
): { room?: string; staff?: string } | undefined =>
|
||||||
|
getOverrides()[String(ci)] ?? getOverridesBySubject()[description];
|
||||||
|
|
||||||
|
const processEntry = (entry: HTMLElement): void => {
|
||||||
|
if (entry.classList.contains("assessment") || entry.hasAttribute("data-timetable-edit-processed")) return;
|
||||||
|
|
||||||
|
const ciStr = entry.getAttribute("data-instance");
|
||||||
|
if (!ciStr) return;
|
||||||
|
|
||||||
|
const ci = parseInt(ciStr, 10);
|
||||||
|
if (isNaN(ci)) return;
|
||||||
|
|
||||||
|
const { roomEl, teacherEl } = getRoomAndTeacherElements(entry);
|
||||||
|
if (!roomEl && !teacherEl) return;
|
||||||
|
|
||||||
|
const titleEl = entry.querySelector(".title");
|
||||||
|
const description = titleEl?.textContent?.trim() ?? "";
|
||||||
|
const room = roomEl?.textContent?.trim() ?? "";
|
||||||
|
const staff = teacherEl?.textContent?.trim() ?? "";
|
||||||
|
|
||||||
|
const item: TimetableEntryData = { ci, description, room, staff };
|
||||||
|
|
||||||
|
entry.setAttribute("data-timetable-edit-processed", "true");
|
||||||
|
|
||||||
|
const override = getEffectiveOverride(ci, description);
|
||||||
|
if (override) {
|
||||||
|
if (override.room !== undefined && roomEl) roomEl.textContent = override.room;
|
||||||
|
if (override.staff !== undefined && teacherEl) teacherEl.textContent = override.staff;
|
||||||
|
}
|
||||||
|
|
||||||
|
const captureClick = (e: MouseEvent) => {
|
||||||
|
lastClickedCi = ci;
|
||||||
|
lastClickedEntry = { roomEl, teacherEl, item };
|
||||||
|
};
|
||||||
|
entry.addEventListener("click", captureClick, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processAllEntries = () => {
|
||||||
|
document.querySelectorAll(".timetablepage .entry.class").forEach((entry) => {
|
||||||
|
processEntry(entry as HTMLElement);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addEditButtonToQuickbar = (quickbar: HTMLElement) => {
|
||||||
|
if (quickbar.querySelector(".timetable-edit-quickbar-btn")) return;
|
||||||
|
|
||||||
|
const actions = quickbar.querySelector(".actions");
|
||||||
|
if (!actions) return;
|
||||||
|
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "uiButton timetable-edit-quickbar-btn";
|
||||||
|
btn.title = "Edit room and teacher";
|
||||||
|
btn.innerHTML = EDIT_ICON_SVG;
|
||||||
|
|
||||||
|
btn.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const ci = lastClickedCi;
|
||||||
|
const entryData = lastClickedEntry;
|
||||||
|
if (!ci || !entryData) return;
|
||||||
|
|
||||||
|
const qb = (e.currentTarget as HTMLElement).closest(".quickbar");
|
||||||
|
if (!qb) return;
|
||||||
|
const quickbarRoom = qb.querySelector(".meta .room")?.textContent?.trim() ?? "";
|
||||||
|
const quickbarTeacher = qb.querySelector(".meta .teacher")?.textContent?.trim() ?? "";
|
||||||
|
const quickbarTitle = qb.querySelector(".title")?.textContent?.trim() ?? "";
|
||||||
|
const item: TimetableEntryData = {
|
||||||
|
ci,
|
||||||
|
description: quickbarTitle || entryData.item.description,
|
||||||
|
room: quickbarRoom || entryData.item.room,
|
||||||
|
staff: quickbarTeacher || entryData.item.staff,
|
||||||
|
};
|
||||||
|
|
||||||
|
showEditModal(
|
||||||
|
item,
|
||||||
|
getOverrides(),
|
||||||
|
getOverridesBySubject(),
|
||||||
|
(ci, room, staff, applyToFuture) => {
|
||||||
|
if (applyToFuture) {
|
||||||
|
const bySubject = { ...getOverridesBySubject() };
|
||||||
|
bySubject[item.description] = {
|
||||||
|
room: room || undefined,
|
||||||
|
staff: staff || undefined,
|
||||||
|
};
|
||||||
|
api.storage.timetableOverridesBySubject = bySubject;
|
||||||
|
} else {
|
||||||
|
const current = getOverrides();
|
||||||
|
api.storage.timetableOverrides = {
|
||||||
|
...current,
|
||||||
|
[String(ci)]: { room: room || undefined, staff: staff || undefined },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (entryData.roomEl) entryData.roomEl.textContent = room;
|
||||||
|
if (entryData.teacherEl) entryData.teacherEl.textContent = staff;
|
||||||
|
processAllEntries();
|
||||||
|
},
|
||||||
|
(ci) => {
|
||||||
|
const current = getOverrides();
|
||||||
|
delete current[String(ci)];
|
||||||
|
api.storage.timetableOverrides = current;
|
||||||
|
const bySubject = getOverridesBySubject();
|
||||||
|
delete bySubject[item.description];
|
||||||
|
api.storage.timetableOverridesBySubject = bySubject;
|
||||||
|
if (entryData.roomEl) entryData.roomEl.textContent = item.room;
|
||||||
|
if (entryData.teacherEl) entryData.teacherEl.textContent = item.staff;
|
||||||
|
processAllEntries();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.insertBefore(btn, actions.firstChild);
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncQuickbarFromDOM = () => {
|
||||||
|
const quickbar = document.querySelector(".timetablepage .quickbar.visible");
|
||||||
|
if (quickbar && quickbar.getAttribute("data-type") === "class") {
|
||||||
|
const titleEl = quickbar.querySelector(".title");
|
||||||
|
const roomEl = quickbar.querySelector(".meta .room");
|
||||||
|
const teacherEl = quickbar.querySelector(".meta .teacher");
|
||||||
|
if (titleEl && roomEl && teacherEl && lastClickedCi !== null && lastClickedEntry) {
|
||||||
|
addEditButtonToQuickbar(quickbar as HTMLElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupQuickbarObserver = () => {
|
||||||
|
const timetablePage = document.querySelector(".timetablepage");
|
||||||
|
if (!timetablePage || quickbarObserver) return;
|
||||||
|
|
||||||
|
quickbarObserver = new MutationObserver(() => {
|
||||||
|
const quickbar = document.querySelector(".timetablepage .quickbar.visible");
|
||||||
|
if (quickbar?.getAttribute("data-type") === "class") {
|
||||||
|
addEditButtonToQuickbar(quickbar as HTMLElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quickbarObserver.observe(timetablePage, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimetable = async () => {
|
||||||
|
await waitForElm(".timetablepage .entry", true, 10, 100);
|
||||||
|
processAllEntries();
|
||||||
|
setupQuickbarObserver();
|
||||||
|
syncQuickbarFromDOM();
|
||||||
|
|
||||||
|
const timetablePage = document.querySelector(".timetablepage");
|
||||||
|
if (timetablePage && !observer) {
|
||||||
|
observer = new MutationObserver(() => {
|
||||||
|
document.querySelectorAll(".timetablepage .entry.class").forEach((entry) => {
|
||||||
|
if (!entry.hasAttribute("data-timetable-edit-processed")) {
|
||||||
|
processEntry(entry as HTMLElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe(timetablePage, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { unregister } = api.seqta.onMount(".timetablepage", handleTimetable);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregister();
|
||||||
|
observer?.disconnect();
|
||||||
|
quickbarObserver?.disconnect();
|
||||||
|
styleEl.remove();
|
||||||
|
document.querySelectorAll("[data-timetable-edit-processed]").forEach((el) => {
|
||||||
|
el.removeAttribute("data-timetable-edit-processed");
|
||||||
|
});
|
||||||
|
document.querySelectorAll(".timetable-edit-quickbar-btn").forEach((el) => el.remove());
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default timetableEditPlugin;
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
/* Timetable Edit Plugin - BetterSEQTA Plus style */
|
||||||
|
|
||||||
|
/* Edit button in quickbar */
|
||||||
|
.timetable-edit-quickbar-btn {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-quickbar-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-quickbar-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-quickbar-btn svg {
|
||||||
|
fill: currentColor;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edit modal animations */
|
||||||
|
@keyframes timetable-edit-overlay-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes timetable-edit-modal-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edit modal overlay - fix click-through with proper stacking */
|
||||||
|
.timetable-edit-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2147483647;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
pointer-events: auto;
|
||||||
|
animation: timetable-edit-overlay-in 0.2s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin: 0 1rem;
|
||||||
|
min-width: 18rem;
|
||||||
|
max-width: 24rem;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--background-primary);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
pointer-events: auto;
|
||||||
|
border: 1px solid var(--background-secondary);
|
||||||
|
animation: timetable-edit-modal-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal h3 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.5rem 1rem 0.5rem 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border: 1px solid var(--background-secondary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
|
||||||
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--better-main, #007bff);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-checkbox input {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-checkbox label {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-actions button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-actions .timetable-edit-btn-clear {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--background-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-actions .timetable-edit-btn-clear:hover {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-actions .timetable-edit-btn-cancel {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--background-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-actions .timetable-edit-btn-cancel:hover {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-actions .timetable-edit-btn-save {
|
||||||
|
background: var(--better-main, #007bff);
|
||||||
|
border: none;
|
||||||
|
color: var(--text-color, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-actions .timetable-edit-btn-save:hover {
|
||||||
|
transform: scale(1.03) translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-edit-modal-actions .timetable-edit-btn-save:active {
|
||||||
|
transform: scale(0.98) translateY(0);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { PluginManager } from "./core/manager";
|
|||||||
|
|
||||||
// Lightweight plugins (load immediately)
|
// Lightweight plugins (load immediately)
|
||||||
import timetablePlugin from "./built-in/timetable";
|
import timetablePlugin from "./built-in/timetable";
|
||||||
|
import timetableEditPlugin from "./built-in/timetableEdit";
|
||||||
import notificationCollectorPlugin from "./built-in/notificationCollector";
|
import notificationCollectorPlugin from "./built-in/notificationCollector";
|
||||||
import themesPlugin from "./built-in/themes";
|
import themesPlugin from "./built-in/themes";
|
||||||
import animatedBackgroundPlugin from "./built-in/animatedBackground";
|
import animatedBackgroundPlugin from "./built-in/animatedBackground";
|
||||||
@@ -23,6 +24,7 @@ pluginManager.registerPlugin(animatedBackgroundPlugin);
|
|||||||
pluginManager.registerPlugin(assessmentsAveragePlugin);
|
pluginManager.registerPlugin(assessmentsAveragePlugin);
|
||||||
pluginManager.registerPlugin(notificationCollectorPlugin);
|
pluginManager.registerPlugin(notificationCollectorPlugin);
|
||||||
pluginManager.registerPlugin(timetablePlugin);
|
pluginManager.registerPlugin(timetablePlugin);
|
||||||
|
pluginManager.registerPlugin(timetableEditPlugin);
|
||||||
pluginManager.registerPlugin(profilePicturePlugin);
|
pluginManager.registerPlugin(profilePicturePlugin);
|
||||||
pluginManager.registerPlugin(assessmentsOverviewPlugin);
|
pluginManager.registerPlugin(assessmentsOverviewPlugin);
|
||||||
pluginManager.registerPlugin(backgroundMusicPlugin);
|
pluginManager.registerPlugin(backgroundMusicPlugin);
|
||||||
|
|||||||
Reference in New Issue
Block a user