diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 7f346f8..1a1262a 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1877,6 +1877,9 @@
"todo_separate_scrollbar": {
"message": "Separate Scrollbar"
},
+ "todo_sections_persist": {
+ "message": "Remember collapsed sections"
+ },
"todo_show_scores": {
"message": "Show assignment scores"
},
diff --git a/css/content.css b/css/content.css
index 400204c..7aee149 100644
--- a/css/content.css
+++ b/css/content.css
@@ -103,6 +103,15 @@
#canvasrefined-todo-list, #canvasrefined-announcement-list {font-size: 12px}
.canvasrefined-todo-container, .canvasrefined-todo-container:hover {color:#777}
.canvasrefined-todo-container p {margin: 0!important;}
+/* Better Todo: collapsible due-date sections (chevron + live item count) */
+.better-todo-section-header {display:flex;align-items:center;gap:6px;font-size:12px;color:var(--bctext-0);cursor:pointer;user-select:none;}
+.better-todo-section-header:hover {color:var(--bclinks);}
+.better-todo-section-header .better-todo-chevron {flex:none;transform:rotate(90deg);transition:transform .2s ease;}
+.better-todo-dueheader.better-todo-collapsed .better-todo-section-header .better-todo-chevron {transform:rotate(0deg);}
+.better-todo-dueheader.better-todo-collapsed .todo-group-list {display:none!important;}
+/* count badge: theme "Background Alt" surface with the theme's Text Main number;
+ in light mode (no --bc vars) it falls back to the dark-mode palette pill */
+.better-todo-section-count {flex:none;margin-left:auto;background:var(--bcbackground-1,#161616);color:var(--bctext-0,#f5f5f5);border-radius:8px;padding:1px 7px;font-size:10.5px;font-weight:700;}
.canvasrefined-todo-icon svg {height: 18px; width: 18px;margin:0}
.canvasrefined-todoitem-course {overflow: hidden; text-overflow: ellipsis;white-space: nowrap;}
.canvasrefined-todoitem-title {font-size: 14px;font-weight:bold;display:inline;margin-top:2px;}
diff --git a/html/popup.html b/html/popup.html
index 50f48c8..5010ec1 100644
--- a/html/popup.html
+++ b/html/popup.html
@@ -333,6 +333,10 @@
+
+
+
+
diff --git a/js/content.js b/js/content.js
index d86f89d..2edca3a 100644
--- a/js/content.js
+++ b/js/content.js
@@ -1288,6 +1288,13 @@ function applyOptionsChanges(changes) {
case "custom_cards_3":
customizeCards();
break;
+ case "todo_sections_persist":
+ // Persistence is opt-in: the moment it turns on, adopt the session's collapses;
+ // turned off, stored state stays put but is simply not read on the next load.
+ if (options.todo_sections_persist === true) {
+ chrome.storage.sync.set({ "todo_collapsed_sections": { ...todoCollapsedSections } });
+ }
+ break;
case "todo_hr24":
case "todo_separate_scrollbar":
case "num_todo_items":
@@ -3818,10 +3825,20 @@ async function createTodoSections(location) {
else if (key >= 7 && key < 30) label = replaceCountPlaceholder(getMsg("due_weeks", "Due {{COUNT}} weeks"), Math.floor(key / 7));
else if (key == "30") label = getMsg("due_one_month", "Due 1 month");
else label = "" + dueGroupLabel(key) + "";
- makeElement("div", wrapper, {
- innerHTML: "" + label + "",
- style: "display:flex;flex-direction:column;gap:10px;font-size:12px;color:var(--bctext-0);"
- })
+ // Clickable section header: chevron + label + live item count (user suggestion:
+ // collapsible due-date sections with a per-section item indicator).
+ const sectionHeader = makeElement("div", wrapper, { "className": "better-todo-section-header" });
+ sectionHeader.setAttribute("role", "button");
+ sectionHeader.setAttribute("tabindex", "0");
+ sectionHeader.setAttribute("aria-expanded", "true");
+ sectionHeader.innerHTML =
+ '' +
+ '' + label + '' +
+ '';
+ sectionHeader.addEventListener("click", () => toggleTodoSection(key, wrapper));
+ sectionHeader.addEventListener("keydown", (e) => {
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleTodoSection(key, wrapper); }
+ });
let listContainer = makeElement("div", wrapper, { className: "todo-group-list" });
listContainer.style = "display:flex;flex-direction:column;gap:10px;";
@@ -3840,6 +3857,8 @@ async function createTodoSections(location) {
populateAssignments(true);
}
+ refreshTodoSections();
+
const feedbackElement = location.querySelector(".recent_feedback");
// populate progress rings placeholder (respect user toggle)
@@ -3954,6 +3973,61 @@ function clearTodoList() {
});
}
+/*
+Collapsible due-date sections
+
+The collapse state is session-only by default: it lives in todoCollapsedSections and
+dies with the reload. "Remember collapsed sections" (todo_sections_persist, off by
+default) seeds the map from chrome.storage once per load and writes every toggle
+back, so collapses survive reloads only while that checkbox is on.
+*/
+let todoCollapsedSections = {};
+let todoCollapsedSeeded = false;
+
+function seedTodoCollapsedSections() {
+ if (todoCollapsedSeeded) return;
+ todoCollapsedSeeded = true;
+ if (options.todo_sections_persist === true && options.todo_collapsed_sections && typeof options.todo_collapsed_sections === "object") {
+ todoCollapsedSections = { ...options.todo_collapsed_sections };
+ }
+}
+
+function toggleTodoSection(key, wrapper) {
+ const collapsed = !wrapper.classList.contains("better-todo-collapsed");
+ applyTodoSectionCollapsed(key, wrapper, collapsed);
+ if (collapsed) todoCollapsedSections[key] = true;
+ else delete todoCollapsedSections[key];
+ if (options.todo_sections_persist === true) {
+ chrome.storage.sync.set({ "todo_collapsed_sections": { ...todoCollapsedSections } });
+ }
+}
+
+function applyTodoSectionCollapsed(key, wrapper, collapsed) {
+ wrapper.classList.toggle("better-todo-collapsed", collapsed);
+ const header = wrapper.querySelector(".better-todo-section-header");
+ if (header) header.setAttribute("aria-expanded", collapsed ? "false" : "true");
+}
+
+// Re-apply collapse state and refresh the per-section item counts after each render.
+// Headers are rebuilt on every render, so this is what carries the state across the
+// clearTodoList/createTodoSections cycle.
+function refreshTodoSections() {
+ seedTodoCollapsedSections();
+ Object.keys(domContainers).forEach(key => {
+ const entry = domContainers[key];
+ if (!entry || !entry.wrapper || !entry.wrapper.isConnected) return;
+ const collapsed = todoCollapsedSections[key] === true;
+ applyTodoSectionCollapsed(key, entry.wrapper, collapsed);
+ // Every row the section holds (hidden "view more" rows included). The rows are
+ // created with a `class:` property assignment that never becomes a real class
+ // attribute, so query the list's children instead of the row classes; the
+ // celebration preview's disposable card must not bump the count.
+ const count = [...entry.listContainer.children].filter(el => el.id !== "canvasrefined-celebration-preview").length;
+ const countEl = entry.wrapper.querySelector(".better-todo-section-count");
+ if (countEl) countEl.textContent = count > 0 ? String(count) : "";
+ });
+}
+
// "Alternate colors" (light mode only): icon fill goes white for lighter course strips.
const TODO_ALT_ICON_COLOR = "#ffffff";
let todoAltStyleEl = null;
@@ -5026,9 +5100,12 @@ function runCelebrationPreview() {
wrapper: todayContainer.wrapper,
display: todayContainer.wrapper.style.display,
hadVisible: todayContainer.wrapper.hasAttribute("data-has-visible"),
+ wasCollapsed: todayContainer.wrapper.classList.contains("better-todo-collapsed"),
};
todayContainer.wrapper.style.display = "block";
todayContainer.wrapper.setAttribute("data-has-visible", "true");
+ // A collapsed section would hide the preview card; expand it for the run.
+ todayContainer.wrapper.classList.remove("better-todo-collapsed");
} else {
card.style.cssText += "position:fixed;top:70px;right:16px;width:300px;z-index:2147483646;box-shadow:0 2px 10px rgba(0,0,0,.25);";
document.body.appendChild(card);
@@ -5046,6 +5123,7 @@ function runCelebrationPreview() {
if (restoreWrapper) {
try {
restoreWrapper.wrapper.style.display = restoreWrapper.display;
+ restoreWrapper.wrapper.classList.toggle("better-todo-collapsed", restoreWrapper.wasCollapsed);
if (!restoreWrapper.hadVisible) restoreWrapper.wrapper.removeAttribute("data-has-visible");
} catch (e) { /* wrapper re-rendered away already */ }
}
diff --git a/js/popup.js b/js/popup.js
index e02838c..79db0ae 100644
--- a/js/popup.js
+++ b/js/popup.js
@@ -55,6 +55,7 @@ const syncedSubOptions = [
"assignment_date_format",
"todo_hr24",
"todo_separate_scrollbar",
+ "todo_sections_persist",
"todo_alternate_colors",
"todo_ignore_card_colors",
"todo_remove_icons",
@@ -220,7 +221,7 @@ const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "imageRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding", "card_grid", "card_grid_columns", "card_grid_rows", "card_grid_column_gap", "card_grid_row_gap", "card_grid_center_rows", "card_grid_flex"];
const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "hide_navbar", "hide_sidebar", "tab_icons"];
const exportSidebar = ["better_sidebar", "sidebar_scale"];
-const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_celebrations_enabled", "todo_celebrations", "todo_celebration_amount", "todo_progress_rings", "todo_timeframe", "todo_timeframe_custom_past", "todo_timeframe_custom_future", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "todo_show_scores", "hover_preview"];
+const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_celebrations_enabled", "todo_celebrations", "todo_celebration_amount", "todo_progress_rings", "todo_timeframe", "todo_timeframe_custom_past", "todo_timeframe_custom_future", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "todo_show_scores", "hover_preview", "todo_sections_persist"];
const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted", "gpa_calc_show_on_hover", "gpa_calc_align"];
// Widgets tab: every dashboard widget's layout (toggles, positions, heights, row
// heights, order, timer presets, quick link sizing). Personal data (quick link items,
@@ -335,6 +336,7 @@ const defaultOptions = {
"card_opacity": 80,
"card_blur": 8,
"todo_hr24": false,
+ "todo_sections_persist": false,
"anon_grade_level": "high",
"anon_grade_seed": 0,
"anon_fake_todo": false,
@@ -2055,6 +2057,7 @@ async function setup() {
"custom_font_skip_p",
"todo_hr24",
"todo_separate_scrollbar",
+ "todo_sections_persist",
"todo_alternate_colors",
"todo_ignore_card_colors",
"todo_remove_icons",