Compare commits

...

2 Commits

Author SHA1 Message Date
Guy Sandler
759deeed75 quiz safe mode 2026-08-24 21:18:37 -07:00
Guy Sandler
8b9a1ded43 I don't even know
bug is there, then not there
2026-08-24 20:10:43 -07:00
8 changed files with 329 additions and 61 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.zip
.vscode
.vscode
.pi

View File

@ -56,6 +56,12 @@
"better_sidebar": {
"message": "Better Sidebar"
},
"quiz_safe_mode": {
"message": "Quiz Safe Mode"
},
"quiz_safe_mode_desc": {
"message": "Turns off most Canvas Refined features on quiz pages so you get the default Canvas quiz experience."
},
"opacity_options": {
"message": "Opacity & Blur"
},

View File

@ -217,4 +217,52 @@
.canvasrefined-notes-editor::placeholder {color:var(--bctext-2,#9aa3ab);}
.canvasrefined-dashboard-notes:not(.is-editing) .canvasrefined-notes-toolbar {display:none;}
.canvasrefined-dashboard-notes.is-editing .canvasrefined-notes-rendered {display:none;}
.canvasrefined-dashboard-notes:not(.is-editing) .canvasrefined-notes-editor {display:none;}
.canvasrefined-dashboard-notes:not(.is-editing) .canvasrefined-notes-editor {display:none;}
/* Quiz safe mode banner (pre-quiz pages) */
.canvasrefined-quiz-safe-banner {
box-sizing: border-box;
margin: 16px 0;
padding: 14px 16px;
border: 1px solid #d4a017;
border-left: 4px solid #d4a017;
border-radius: 6px;
background: #fff8e1;
color: #333;
font-family: inherit;
font-size: 14px;
line-height: 1.45;
max-width: 760px;
}
.canvasrefined-quiz-safe-title {
font-weight: 700;
font-size: 15px;
margin-bottom: 6px;
color: #7a5a00;
}
.canvasrefined-quiz-safe-info { margin: 0 0 10px; }
.canvasrefined-quiz-safe-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.canvasrefined-quiz-safe-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
.canvasrefined-quiz-safe-toggle input { width: 16px; height: 16px; cursor: pointer; }
.canvasrefined-quiz-safe-toggle-label { font-weight: 600; }
.canvasrefined-quiz-safe-dismiss {
margin-left: auto;
background: #f5f5f5;
border: 1px solid #c7cdd1;
border-radius: 6px;
padding: 4px 10px;
cursor: pointer;
font-size: 13px;
color: #000 !important;
}
.canvasrefined-quiz-safe-dismiss:hover { background: #ececec; }

View File

@ -287,4 +287,44 @@ input[type="checkbox"]:checked {background: #8dd28d;}
.search-result.empty { text-align: center; color: #8d8d8d; font-size: 12px; cursor: default; padding: 12px; }
.search-result.empty:hover { background: none; }
.search-highlight { outline: 2px solid #56Caf0; outline-offset: 2px; border-radius: 8px; animation: search-pulse 1.8s ease-out; }
@keyframes search-pulse { 0% { box-shadow: 0 0 0 0 #56Caf099; } 40% { box-shadow: 0 0 0 6px #56Caf000; } 100% { box-shadow: 0 0 0 0 #56Caf000; } }
@keyframes search-pulse { 0% { box-shadow: 0 0 0 0 #56Caf099; } 40% { box-shadow: 0 0 0 6px #56Caf000; } 100% { box-shadow: 0 0 0 0 #56Caf000; } }
/* ===== Quiz Safe Mode info bubble ===== */
.cr-info-bubble {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 6px;
width: 16px;
height: 16px;
color: #8d8d8d;
cursor: help;
position: relative;
vertical-align: middle;
}
.cr-info-bubble:hover,
.cr-info-bubble:focus { color: #56Caf0; }
.cr-info-bubble-text {
visibility: hidden;
opacity: 0;
pointer-events: none;
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
width: max-content;
max-width: 220px;
background: #0e0e0ef0;
color: #e2e2e2;
border: 1px solid #ffffff1a;
border-radius: 8px;
padding: 8px 11px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
box-shadow: 0 8px 24px rgba(0,0,0,.45);
transition: opacity .15s ease, visibility .15s ease;
z-index: 1000;
text-align: left;
}
.cr-info-bubble:hover .cr-info-bubble-text,
.cr-info-bubble:focus .cr-info-bubble-text { visibility: visible; opacity: 1; }

View File

@ -190,6 +190,20 @@
<span class="option-name" data-i18n="todo_remind">Todo Reminders</span>
</div>
</div>
<div class="option-container">
<div class="option" id="quiz_safe_mode">
<input type="radio" id="off" name="quiz_safe_mode">
<input type="radio" id="on" name="quiz_safe_mode">
<div class="slider">
<div class="sliderknob"></div>
<div class="sliderbg"></div>
</div><span class="option-name" data-i18n="quiz_safe_mode">Quiz Safe Mode</span>
<span class="cr-info-bubble" tabindex="0" role="button" aria-label="Quiz Safe Mode info">
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
<span class="cr-info-bubble-text" data-i18n="quiz_safe_mode_desc">Turns off most Canvas Refined features on quiz pages so you get the default Canvas quiz experience.</span>
</span>
</div>
</div>
<div class="social-buttons">
<a class="discord-button" href="https://discord.gg/GjFh4JA3wh" target="_blank" rel="noopener noreferrer" title="Join our Discord">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.369a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.331c-1.182 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>

View File

@ -76,6 +76,7 @@ chrome.runtime.onInstalled.addListener(function () {
"equal_height_cards": false,
"hide_feedback": false,
"hide_new_canvas": true,
"quiz_safe_mode": false,
"dark_mode_fix": [],
"assignment_states": {},
"tab_icons": false,

View File

@ -38,6 +38,23 @@ function isProfilePage() {
return /^\/profile(?:\/|$)/.test(current_page);
}
// Quiz pages: /courses/123/quizzes/456 (pre-take/intro) and
// /courses/123/quizzes/456/take (the actual quiz).
function isQuizPage() {
return /^\/courses\/\d+\/quizzes\/\d+(?:\/|$)/.test(current_page);
}
function isQuizTakePage() {
return /^\/courses\/\d+\/quizzes\/\d+\/take(?:\/|$)/.test(current_page);
}
function isQuizPreTakePage() {
return isQuizPage() && !isQuizTakePage();
}
// "Quiz safe mode" disables features that interfere with the default Canvas
// quiz experience. Only active on quiz pages when the user has opted in.
function quizSafeModeActive() {
return isQuizPage() && options.quiz_safe_mode === true;
}
function getSubmissionAssignmentLink() {
const match = current_page.match(/^\/courses\/(\d+)\/assignments\/(\d+)\/submissions\/(\d+)(?:\/|$)/);
if (!match) return null;
@ -558,7 +575,10 @@ function startExtension() {
toggleDarkMode();
chrome.storage.sync.get(["better_sidebar", "sidebar_scale"], result => {
// Include bg_opacity/bg_blur so setupBetterSidebar (called below) tints the
// course-content panel with the user's slider values on first load, instead
// of falling back to the defaults until a slider is touched.
chrome.storage.sync.get(["better_sidebar", "sidebar_scale", "bg_opacity", "bg_blur"], result => {
options = { ...options, ...result };
ensureBetterSidebar();
});
@ -581,6 +601,8 @@ function startExtension() {
watchSubmissionPageButton();
watchProfileLogoutPageButton();
setupQuizSafeModeBanner();
setTimeout(() => runDarkModeFixer(false), 800);
setTimeout(() => runDarkModeFixer(false), 4500);
@ -777,6 +799,11 @@ function applyOptionsChanges(changes) {
resetBetterSidebarLayout();
}
break;
case "quiz_safe_mode":
// Toggling safe mode changes which features run on quiz pages; reload
// so the gating is applied cleanly.
if (isQuizPage()) window.location.reload();
break;
case "sidebar_scale": {
const existingSidebar = document.getElementById("better-sidebar-container");
if (existingSidebar) {
@ -823,12 +850,16 @@ function resetBetterSidebarLayout() {
function ensureBetterSidebar() {
if (!options.better_sidebar) return;
// Quiz safe mode: don't replace the Canvas sidebar on quiz pages.
if (quizSafeModeActive()) return;
if (document.querySelector("#better-sidebar-container")) return;
if (!document.querySelector("#wrapper") || !document.querySelector(".ic-Layout-contentWrapper")) return;
setupBetterSidebar(getSidebarLayoutMode());
}
async function applyCustomBackground() {
// Quiz safe mode: leave the quiz page background untouched.
if (quizSafeModeActive()) return;
// let style = document.querySelector("#DashboardCard_Container")
let style = document.querySelector("#canvasrefined-background") || document.createElement('style');
style.id = "canvasrefined-background";
@ -1017,6 +1048,23 @@ async function applyCustomBackground() {
box-sizing: border-box !important;
}
` : ""}
/* Course content pages (sidebar layout "course": /courses/:id/* and
/profile): tint the #content.ic-Layout-contentMain panel so the
bg_opacity/bg_blur sliders have a surface to control even without Better
Sidebar. setupBetterSidebar only adds this panel when Better Sidebar is
on; this mirrors its inline values so the panel shows regardless. When
Better Sidebar is on, its inline !important overrides these (same values),
so this rule is inert in that case. */
${getSidebarLayoutMode() === "course" ? `
.ic-Layout-contentMain {
margin: 26px 38px 38px !important;
padding: 10px !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important;
backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important;
border-radius: 10px !important;
}
` : ""}
${isConversationsPage() ? `
.css-1nh4pc4-view-flexItem {
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important;
@ -1690,7 +1738,7 @@ function renderProgressRainbow(wrapper, shown, totalAll, completedAll, percent)
// sweep-flag 1 => arc bulges UPWARD (top semicircle), drawn left -> right
const arcPath = `M ${cx - radius} ${baseY} A ${radius} ${radius} 0 0 1 ${cx + radius} ${baseY}`;
const prog = entry.total === 0 ? 0 : entry.completed / entry.total;
const color = `hsl(${Math.round((idx * 360) / Math.max(1, ringCount))},70%,55%)`;
const color = courseRingColor(entry.courseId, idx);
// track: full semicircle, faded class color
let track = svg.querySelector(`path[data-idx='${idx}'][data-role='bg']`);
@ -3194,66 +3242,85 @@ function createConfettiBurst(targetElement, opts = {}) {
function markAs(item, element) {
const csrfToken = CSRFtoken();
const completeState = item.planner_override ? !item.planner_override.marked_complete : true;
fetch(domain + "/api/v1/planner/overrides" + (item.planner_override ? "/" + item.planner_override.id : ""), {
method: item.planner_override ? "PUT" : "POST",
headers: {
"content-type":"application/json",
"accept":"application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify({
id: item.planner_override ? item.planner_override.id : null,
marked_complete: completeState,
plannable_id: item.plannable_id,
plannable_type: item.plannable_type
})
})
.then(resp => {
if (resp.status == 200 || resp.status == 201 || resp.status == 204) {
console.log("marked as complete");
item.planner_override = item.planner_override || {};
item.planner_override.marked_complete = completeState;
element.style.transform = "translate(100%)";
element.style.opacity = "0";
// fire confetti only when marking complete (not when unmarking)
if (completeState) {
try { createConfettiBurst(element); } catch (e) { console.error('confetti trigger error', e); }
// --- Optimistic UI ---
// Canvas's /planner/overrides endpoint occasionally returns 400 (Bad
// Request) for both custom tasks and normal tasks, even though the action
// is actually applied server-side shortly after. When that happens the
// item would neither visually mark nor animate, and would later appear
// "secretly complete" on reload. To avoid that confusing UX, we update the
// UI as if the request succeeded immediately, and fire the actual API
// call in the background for persistence.
item.planner_override = item.planner_override || {};
item.planner_override.marked_complete = completeState;
element.style.transform = "translate(100%)";
element.style.opacity = "0";
// fire confetti only when marking complete (not when unmarking)
if (completeState) {
try { createConfettiBurst(element); } catch (e) { console.error('confetti trigger error', e); }
}
// update progress rings immediately so they animate while the item slides/fades
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
if (progressPlaceholder && typeof assignments?.then === 'function' && progressRingsEnabled()) {
assignments.then(data => {
const courseId = getCurrentCourseId();
const scopedData = courseId
? data.map(d => Object.assign({}, d)) // shallow copy
.filter(d => {
const itemCourseId = parseInt(d.course_id || d.context_id || d?.plannable?.course_id);
return itemCourseId === courseId;
})
: data.map(d => Object.assign({}, d));
// reflect the updated state for this item in the snapshot
for (let i = 0; i < scopedData.length; i++) {
if (scopedData[i].plannable_id === item.plannable_id && scopedData[i].plannable_type === item.plannable_type) {
scopedData[i].planner_override = scopedData[i].planner_override || {};
scopedData[i].planner_override.marked_complete = item.planner_override.marked_complete;
break;
}
}
// update progress rings immediately so they animate while the item slides/fades
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
if (progressPlaceholder && typeof assignments?.then === 'function' && progressRingsEnabled()) {
assignments.then(data => {
const courseId = getCurrentCourseId();
const scopedData = courseId
? data.map(d => Object.assign({}, d)) // shallow copy
.filter(d => {
const itemCourseId = parseInt(d.course_id || d.context_id || d?.plannable?.course_id);
return itemCourseId === courseId;
})
: data.map(d => Object.assign({}, d));
renderProgressRings(progressPlaceholder, scopedData);
});
}
// reflect the updated state for this item in the snapshot
for (let i = 0; i < scopedData.length; i++) {
if (scopedData[i].plannable_id === item.plannable_id && scopedData[i].plannable_type === item.plannable_type) {
scopedData[i].planner_override = scopedData[i].planner_override || {};
scopedData[i].planner_override.marked_complete = item.planner_override.marked_complete;
break;
}
}
setTimeout(() => {
clearTodoList();
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
}, 400);
renderProgressRings(progressPlaceholder, scopedData);
});
// --- Persistence (background, best-effort) ---
// A 400/non-OK response is treated as a soft success because Canvas often
// completes the override server-side anyway; we already reflected the
// change in the UI, so just log it.
fetch(domain + "/api/v1/planner/overrides" + (item.planner_override && item.planner_override.id ? "/" + item.planner_override.id : ""), {
method: item.planner_override && item.planner_override.id ? "PUT" : "POST",
headers: {
"content-type":"application/json",
"accept":"application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify({
id: item.planner_override && item.planner_override.id ? item.planner_override.id : null,
marked_complete: completeState,
plannable_id: item.plannable_id,
plannable_type: item.plannable_type
})
})
.then(resp => {
if (resp.ok) {
console.log("marked as complete");
} else {
// Non-OK (e.g. 400) is logged but not surfaced: the UI already
// reflects the intended state, and Canvas typically applies the
// override server-side regardless.
console.warn("planner override request returned", resp.status, "— UI already updated optimistically");
}
setTimeout(() => {
clearTodoList();
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
}, 400);
}
})
.catch(err => console.error("error marking as complete", err));
})
.catch(err => console.error("error marking as complete", err));
}
@ -3273,6 +3340,8 @@ function createTodoViewMore(location, type) {
// better todo init
function setupBetterTodo() {
// Better Todo list is removed from quizzes (it interferes with the quiz page).
if (isQuizPage()) return;
if (options.better_todo !== true || isGradesPage()) return;
if (document.querySelector('#canvasrefined-todo-list')) return;
let list = document.querySelector("#right-side");
@ -4056,6 +4125,8 @@ function toggleDarkMode() {
}
function runDarkModeFixer(override = false) {
// Quiz safe mode: never auto-run the dark mode fixer on quiz pages.
if (quizSafeModeActive()) return { "path": "canvasrefined-none", "time": "" };
if (options.dark_mode !== true) return { "path": "canvasrefined-darkmode_off", "time": "" };
if (override === false && !options["dark_mode_fix"].includes(window.location.pathname)) return { "path": "canvasrefined-none", "time": "" };
let output = inspectDarkMode();
@ -5029,6 +5100,8 @@ Custom font
*/
function loadCustomFont() {
// Quiz safe mode: don't override fonts on quiz pages.
if (quizSafeModeActive()) return;
let link = document.querySelector("#custom_font_link");
let style = document.querySelector("#custom_font");
@ -5069,6 +5142,8 @@ Smaller features
*/
function applyAestheticChanges() {
// Quiz safe mode: don't inject custom layout/aesthetic CSS on quiz pages.
if (quizSafeModeActive()) return;
let style = document.querySelector("#canvasrefined-aesthetics") || document.createElement('style');
style.id = "canvasrefined-aesthetics";
style.textContent = "";
@ -5091,6 +5166,88 @@ function applyAestheticChanges() {
document.documentElement.appendChild(style);
}
/*
Quiz safe mode banner (pre-quiz pages only).
Shows an info box explaining the extension hasn't been approved by all teachers,
with a toggle for Quiz Safe Mode and a "Don't show again" button.
*/
function setupQuizSafeModeBanner() {
if (!isQuizPreTakePage()) return;
if (document.getElementById("canvasrefined-quiz-safe-banner")) return;
chrome.storage.local.get("quiz_safe_mode_reminder_dismissed", local => {
if (local && local.quiz_safe_mode_reminder_dismissed === true) return;
chrome.storage.sync.get("quiz_safe_mode", sync => {
const safeModeOn = sync && sync.quiz_safe_mode === true;
injectQuizSafeModeBanner(safeModeOn);
});
});
}
function injectQuizSafeModeBanner(safeModeOn) {
// Only inject into a real Canvas content container — never <body>, which
// would place the banner outside the content area if it renders too early.
const findContainer = () =>
document.querySelector(".ic-Layout-contentMain") ||
document.querySelector("#content") ||
document.querySelector("#main");
const insertInto = (container) => {
if (!container) return false;
if (document.getElementById("canvasrefined-quiz-safe-banner")) return true;
const banner = makeElement("div", container, {
id: "canvasrefined-quiz-safe-banner",
className: "canvasrefined-quiz-safe-banner",
}, true);
makeElement("div", banner, {
className: "canvasrefined-quiz-safe-title",
textContent: "Canvas Refined — Quiz Safe Mode",
});
makeElement("p", banner, {
className: "canvasrefined-quiz-safe-info",
textContent: "This extension hasn't been 100% approved by all teachers. Quiz Safe Mode turns off most Canvas Refined features that could interfere with this quiz page, giving you the default Canvas quiz experience.",
});
const toggleRow = makeElement("div", banner, { className: "canvasrefined-quiz-safe-row" });
const toggleWrap = makeElement("label", toggleRow, { className: "canvasrefined-quiz-safe-toggle" });
const checkbox = makeElement("input", toggleWrap, { type: "checkbox" });
checkbox.checked = !!safeModeOn;
checkbox.addEventListener("change", () => {
chrome.storage.sync.set({ quiz_safe_mode: checkbox.checked });
// The storage.onChanged listener (applyOptionsChanges) reloads quiz pages.
});
makeElement("span", toggleWrap, {
className: "canvasrefined-quiz-safe-toggle-label",
textContent: "Enable Quiz Safe Mode",
});
const dismissBtn = makeElement("button", toggleRow, {
className: "canvasrefined-quiz-safe-dismiss",
type: "button",
textContent: "Don't show again",
title: "Hides this reminder permanently. You can still toggle Quiz Safe Mode in the extension popup.",
});
dismissBtn.addEventListener("click", () => {
chrome.storage.local.set({ quiz_safe_mode_reminder_dismissed: true });
banner.remove();
});
return true;
};
if (insertInto(findContainer())) return;
// Content container not ready yet; wait for it (never fall back to <body>).
const obs = new MutationObserver(() => {
if (insertInto(findContainer())) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => obs.disconnect(), 15000);
}
function changeGradientCards() {
if (options.gradient_cards === true) {

View File

@ -1,4 +1,4 @@
const syncedSwitches = ['remind', 'tab_icons', 'hide_feedback', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'better_todo', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'center_cards'];
const syncedSwitches = ['remind', 'tab_icons', 'hide_feedback', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'better_todo', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'center_cards', 'quiz_safe_mode'];
const syncedSubOptions = [
"todo_hide_feedback",
"todo_full_height",
@ -54,7 +54,7 @@ const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_c
const exportBackground = ["customBackgroundLink", "customBackgroundScale", "customBackgroundDaily", "customBackgroundNasaDaily", "fitImageToScreen", "bg_opacity", "sidebar_opacity", "bg_blur", "sidebar_blur"];
// Master "On/off toggles" = every visual toggle (no GPA, no dark-mode schedule,
// no personal productivity features).
const exportToggles = ["dark_mode"].concat(exportCardColorToggles, exportLayout, exportSidebar, exportTodo);
const exportToggles = ["dark_mode", "quiz_safe_mode"].concat(exportCardColorToggles, exportLayout, exportSidebar, exportTodo);
const fontsDropdownStateKey = "fonts_dropdown_open";
const apiurl = "none";
@ -140,6 +140,7 @@ const defaultOptions = {
"equal_height_cards": false,
"hide_feedback": false,
"hide_new_canvas": true,
"quiz_safe_mode": false,
"dark_mode_fix": [],
"assignment_states": {},
"tab_icons": false,