diff --git a/_locales/de/messages.json b/_locales/de/messages.json
index 11a2bb3..80ea274 100644
--- a/_locales/de/messages.json
+++ b/_locales/de/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "Flexibles Raster"
},
- "flexible_grid_on": {
- "message": "Das flexible Raster ist aktiv – Karten umbrechen, um in schmale Fenster zu passen, statt abgeschnitten zu werden."
- },
"focus_timer": {
"message": "Fokus-Timer"
},
@@ -860,8 +857,8 @@
"gradient_desc": {
"message": "Wähle 2 Farben, um einen Verlauf zwischen ihnen zu erstellen:"
},
- "grid_includes_gpa": {
- "message": " (einschließlich des GPA-Rechners)"
+ "grid_includes_widgets": {
+ "message": " (einschließlich {{NAMES}})"
},
"grid_warning": {
"message": "Dein Raster bietet Platz für {{TOTAL}} Karte{{TOTAL_S}}, aber du hast {{ACTIVE}} Karte{{ACTIVE_S}}{{GPA_NOTE}} – einige Karten werden möglicherweise abgeschnitten."
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 5e189c0..0218944 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "Flexible grid"
},
- "flexible_grid_on": {
- "message": "Flexible grid is on - cards wrap to fit narrow windows instead of being cut off."
- },
"focus_timer": {
"message": "Focus Timer"
},
@@ -863,8 +860,8 @@
"gradient_desc": {
"message": "Choose 2 colors to create a gradient between them:"
},
- "grid_includes_gpa": {
- "message": " (including the GPA calculator)"
+ "grid_includes_widgets": {
+ "message": " (including {{NAMES}})"
},
"grid_warning": {
"message": "Your grid holds {{TOTAL}} card{{TOTAL_S}} but you have {{ACTIVE}} card{{ACTIVE_S}}{{GPA_NOTE}} - some cards may be cut off."
diff --git a/_locales/es/messages.json b/_locales/es/messages.json
index d992c6e..68fedba 100644
--- a/_locales/es/messages.json
+++ b/_locales/es/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "Cuadrícula flexible"
},
- "flexible_grid_on": {
- "message": "La cuadrícula flexible está activada: las tarjetas se ajustan al ancho de las ventanas estrechas en lugar de quedar cortadas."
- },
"focus_timer": {
"message": "Temporizador de enfoque"
},
@@ -860,8 +857,8 @@
"gradient_desc": {
"message": "Elige 2 colores para crear un degradado entre ellos:"
},
- "grid_includes_gpa": {
- "message": " (incluida la calculadora de GPA)"
+ "grid_includes_widgets": {
+ "message": " (incluidos {{NAMES}})"
},
"grid_warning": {
"message": "Tu cuadrícula admite {{TOTAL}} tarjeta{{TOTAL_S}} pero tienes {{ACTIVE}} tarjeta{{ACTIVE_S}}{{GPA_NOTE}} - algunas tarjetas pueden quedar cortadas."
diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json
index 194de63..21bfdc3 100644
--- a/_locales/fr/messages.json
+++ b/_locales/fr/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "Grille flexible"
},
- "flexible_grid_on": {
- "message": "La grille flexible est active — les cartes s'enroulent pour s'adapter aux fenêtres étroites au lieu d'être coupées."
- },
"focus_timer": {
"message": "Minuteur de concentration"
},
@@ -860,8 +857,8 @@
"gradient_desc": {
"message": "Choisissez 2 couleurs pour créer un dégradé entre elles :"
},
- "grid_includes_gpa": {
- "message": " (y compris le calculateur GPA)"
+ "grid_includes_widgets": {
+ "message": " (y compris {{NAMES}})"
},
"grid_warning": {
"message": "Votre grille contient {{TOTAL}} carte{{TOTAL_S}} mais vous avez {{ACTIVE}} carte{{ACTIVE_S}}{{GPA_NOTE}} — certaines cartes peuvent être coupées."
diff --git a/_locales/it/messages.json b/_locales/it/messages.json
index 64866f8..4d2b2f0 100644
--- a/_locales/it/messages.json
+++ b/_locales/it/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "Griglia flessibile"
},
- "flexible_grid_on": {
- "message": "La griglia flessibile è attiva: le card si adattano alle finestre strette invece di essere tagliate."
- },
"focus_timer": {
"message": "Timer di concentrazione"
},
@@ -860,8 +857,8 @@
"gradient_desc": {
"message": "Scegli 2 colori per creare una sfumatura tra loro:"
},
- "grid_includes_gpa": {
- "message": " (incluso il calcolatore GPA)"
+ "grid_includes_widgets": {
+ "message": " (inclusi {{NAMES}})"
},
"grid_warning": {
"message": "La tua griglia contiene {{TOTAL}} card{{TOTAL_S}} ma ne hai {{ACTIVE}} card{{ACTIVE_S}}{{GPA_NOTE}} - alcune card potrebbero essere tagliate."
diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json
index 0fe785f..886d26a 100644
--- a/_locales/ja/messages.json
+++ b/_locales/ja/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "フレキシブルグリッド"
},
- "flexible_grid_on": {
- "message": "フレキシブルグリッドが有効です。カードは切れずに折り返して狭いウィンドウに収まります。"
- },
"focus_timer": {
"message": "集中タイマー"
},
@@ -860,8 +857,8 @@
"gradient_desc": {
"message": "2色を選んでグラデーションを作成します:"
},
- "grid_includes_gpa": {
- "message": " (GPA計算を含む)"
+ "grid_includes_widgets": {
+ "message": " ({{NAMES}}を含む)"
},
"grid_warning": {
"message": "グリッドには{{TOTAL}}枚のカード{{TOTAL_S}}が入りますが、{{ACTIVE}}枚のカード{{ACTIVE_S}}{{GPA_NOTE}}があります - 一部のカードが切れる可能性があります。"
diff --git a/_locales/pt_PT/messages.json b/_locales/pt_PT/messages.json
index d3a9a0a..737b2ba 100644
--- a/_locales/pt_PT/messages.json
+++ b/_locales/pt_PT/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "Grelha flexível"
},
- "flexible_grid_on": {
- "message": "A grelha flexível está ativa — os cartões ajustam-se para caber em janelas estreitas em vez de serem cortados."
- },
"focus_timer": {
"message": "Temporizador de foco"
},
@@ -860,8 +857,8 @@
"gradient_desc": {
"message": "Escolhe 2 cores para criar um gradiente entre elas:"
},
- "grid_includes_gpa": {
- "message": " (incluindo a calculadora de GPA)"
+ "grid_includes_widgets": {
+ "message": " (incluindo {{NAMES}})"
},
"grid_warning": {
"message": "A tua grelha comporta {{TOTAL}} vaga{{TOTAL_S}}, mas tens {{ACTIVE}} vaga{{ACTIVE_S}}{{GPA_NOTE}} - alguns cartões podem ficar cortados."
diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json
index c0e7269..f3944f8 100644
--- a/_locales/ru/messages.json
+++ b/_locales/ru/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "Гибкая сетка"
},
- "flexible_grid_on": {
- "message": "Включена гибкая сетка — карточки переносятся, чтобы помещаться в узкие окна, а не обрезаются."
- },
"focus_timer": {
"message": "Таймер фокуса"
},
@@ -860,8 +857,8 @@
"gradient_desc": {
"message": "Выберите 2 цвета, чтобы создать между ними градиент:"
},
- "grid_includes_gpa": {
- "message": " (включая калькулятор GPA)"
+ "grid_includes_widgets": {
+ "message": " (включая {{NAMES}})"
},
"grid_warning": {
"message": "В вашей сетке помещается {{TOTAL}} карточек{{TOTAL_S}}, но у вас {{ACTIVE}} карточка{{ACTIVE_S}}{{GPA_NOTE}} — некоторые карточки могут обрезаться."
diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json
index a6591da..7db89cb 100644
--- a/_locales/sv/messages.json
+++ b/_locales/sv/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "Flexibelt rutnät"
},
- "flexible_grid_on": {
- "message": "Flexibelt rutnät är på – korten radbryts för att passa smala fönster i stället för att klippas av."
- },
"focus_timer": {
"message": "Fokustimer"
},
@@ -860,8 +857,8 @@
"gradient_desc": {
"message": "Välj 2 färger för att skapa en gradient mellan dem:"
},
- "grid_includes_gpa": {
- "message": " (inklusive GPA-kalkylatorn)"
+ "grid_includes_widgets": {
+ "message": " (inklusive {{NAMES}})"
},
"grid_warning": {
"message": "Ditt rutnät har plats för {{TOTAL}} kort{{TOTAL_S}} men du har {{ACTIVE}} kort{{ACTIVE_S}}{{GPA_NOTE}} – vissa kort kan klippas av."
diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json
index 7e30d6b..d805ba5 100644
--- a/_locales/zh_CN/messages.json
+++ b/_locales/zh_CN/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "弹性网格"
},
- "flexible_grid_on": {
- "message": "弹性网格已开启 - 卡片会自动换行以适应窄窗口,而不会被裁切。"
- },
"focus_timer": {
"message": "专注计时器"
},
@@ -860,8 +857,8 @@
"gradient_desc": {
"message": "选择 2 种颜色以在它们之间创建渐变:"
},
- "grid_includes_gpa": {
- "message": " (包括 GPA 计算器)"
+ "grid_includes_widgets": {
+ "message": " (包括 {{NAMES}})"
},
"grid_warning": {
"message": "你的网格可容纳 {{TOTAL}} 张卡片{{TOTAL_S}},但你有 {{ACTIVE}} 张卡片{{ACTIVE_S}}{{GPA_NOTE}} - 部分卡片可能会被裁切。"
diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json
index d17c7ab..bcac043 100644
--- a/_locales/zh_TW/messages.json
+++ b/_locales/zh_TW/messages.json
@@ -578,9 +578,6 @@
"flexible_grid": {
"message": "彈性格線"
},
- "flexible_grid_on": {
- "message": "彈性格線已開啟——卡片會自動換行以配合狹窄視窗,而不會被裁切。"
- },
"focus_timer": {
"message": "專注計時器"
},
@@ -860,8 +857,8 @@
"gradient_desc": {
"message": "選擇 2 種顏色,在它們之間建立漸層:"
},
- "grid_includes_gpa": {
- "message": " (包含 GPA 計算器)"
+ "grid_includes_widgets": {
+ "message": " (包含 {{NAMES}})"
},
"grid_warning": {
"message": "你的格線可容納 {{TOTAL}} 張卡片{{TOTAL_S}},但你目前有 {{ACTIVE}} 張卡片{{ACTIVE_S}}{{GPA_NOTE}}——部分卡片可能會被裁切。"
diff --git a/html/popup.html b/html/popup.html
index 8d9ca54..41b730e 100644
--- a/html/popup.html
+++ b/html/popup.html
@@ -1149,8 +1149,13 @@
diff --git a/js/background.js b/js/background.js
index 64dcc79..8614cb3 100644
--- a/js/background.js
+++ b/js/background.js
@@ -58,6 +58,8 @@ chrome.runtime.onInstalled.addListener(function () {
"quick_links_position": "cards_end",
"heatmap": false,
"heatmap_position": "below",
+ "widget_top_height": 0,
+ "widget_bottom_height": 0,
"widget_row_height": 0,
"dashboard_notes_height": 0,
"gpa_calc_height": 0,
@@ -180,6 +182,16 @@ chrome.runtime.onInstalled.addListener(function () {
newSyncOptions["fitImageToScreen"] = sync["nasaFitToScreen"];
}
+ // The Top/Bottom widget rows used to share one height: give both rows the
+ // user's old value instead of dropping it to "auto".
+ if (sync["widget_top_height"] === undefined && sync["widget_bottom_height"] === undefined) {
+ const legacyRowHeight = Number(sync["widget_row_height"]);
+ if (Number.isFinite(legacyRowHeight) && legacyRowHeight > 0) {
+ newSyncOptions["widget_top_height"] = legacyRowHeight;
+ newSyncOptions["widget_bottom_height"] = legacyRowHeight;
+ }
+ }
+
if (Object.keys(newLocalOptions).length > 0) {
chrome.storage.local.set(newLocalOptions);
}
diff --git a/js/content.js b/js/content.js
index 19f0805..b513f1e 100644
--- a/js/content.js
+++ b/js/content.js
@@ -1090,6 +1090,7 @@ function applyOptionsChanges(changes) {
case "dashboard_notes_position":
case "dashboard_notes_height":
loadDashboardNotes();
+ refreshCardGridLayout();
break;
case "focus_timer":
case "focus_timer_position":
@@ -1098,6 +1099,7 @@ function applyOptionsChanges(changes) {
case "focus_timer_preset_2":
case "focus_timer_preset_3":
loadFocusTimer();
+ refreshCardGridLayout();
break;
case "breathing":
case "breathing_position":
@@ -1106,21 +1108,25 @@ function applyOptionsChanges(changes) {
case "breathing_preset_2":
case "breathing_preset_3":
loadBreathingWidget();
+ refreshCardGridLayout();
break;
case "daily_quotes":
case "daily_quotes_position":
case "daily_quotes_height":
loadDailyQuotesWidget();
+ refreshCardGridLayout();
break;
case "quick_links":
case "quick_links_position":
case "quick_links_height":
loadQuickLinksWidget();
+ refreshCardGridLayout();
break;
case "heatmap":
case "heatmap_position":
case "heatmap_height":
loadHeatmapWidget();
+ refreshCardGridLayout();
break;
case "quick_links_font_size":
case "quick_links_icon_size":
@@ -1144,7 +1150,10 @@ function applyOptionsChanges(changes) {
if (options.dashboard_notes === true) loadDashboardNotes();
if (options.gpa_calc === true && current_page === "/") setupGPACalc();
loadAllSimpleWidgets();
+ refreshCardGridLayout();
break;
+ case "widget_top_height":
+ case "widget_bottom_height":
case "widget_row_height":
document.querySelectorAll(".canvasrefined-widget-row").forEach(row => {
applyWidgetRowLayout(row, row.dataset.area);
@@ -1267,7 +1276,7 @@ function applyOptionsChanges(changes) {
setupGPACalc();
updateGpaHoverBlur();
// GPA elements take part in the grid layout, so regenerate the grid CSS.
- if (options.card_grid === true) debouncedApplyAestheticChanges();
+ refreshCardGridLayout();
break;
case "gpa_calc_bounds":
calculateGPA2();
@@ -7369,8 +7378,7 @@ function widgetContainerAnchor(container) {
return document.querySelector("#DashboardCard_Container") || container;
}
-function ensureWidgetRow(area, container) {
- const parent = container.parentNode;
+function ensureWidgetRow(area, container) { const parent = container.parentNode;
if (!parent) return null;
let row = parent.querySelector(":scope > .canvasrefined-widget-row[data-area='" + area + "']");
if (!row) {
@@ -7383,12 +7391,24 @@ function ensureWidgetRow(area, container) {
return row;
}
+// Row height in px for the Top / Bottom widget rows (0 = auto). The two rows have
+// their own setting now; older builds shared one value (widget_row_height), which is
+// still honoured until storage has been migrated. Row areas are "top"/"below", the
+// stored settings are "Top"/"Bottom".
+const WIDGET_ROW_HEIGHT_KEYS = { top: "widget_top_height", below: "widget_bottom_height" };
+function widgetRowHeight(area) {
+ const own = parseInt(options[WIDGET_ROW_HEIGHT_KEYS[area]], 10);
+ if (Number.isFinite(own)) return Math.max(0, own);
+ const legacy = parseInt(options.widget_row_height, 10);
+ return Number.isFinite(legacy) ? Math.max(0, legacy) : 0;
+}
+
function applyWidgetRowLayout(row, area) {
const slots = [...row.querySelectorAll(":scope > .canvasrefined-widget-slot")];
if (!slots.length) { row.remove(); return; }
// Shared row height (px; 0 = auto) so every widget in the row matches.
- const rowHeight = parseInt(options.widget_row_height, 10);
- if (Number.isFinite(rowHeight) && rowHeight > 0) row.style.height = rowHeight + "px";
+ const rowHeight = widgetRowHeight(area);
+ if (rowHeight > 0) row.style.height = rowHeight + "px";
else row.style.height = "";
const entries = containerWidgetOrder(area);
const byId = {};
@@ -9493,6 +9513,74 @@ function debouncedApplyAestheticChanges(delay = 150) {
}, delay);
}
+// In-card widgets take grid cells and rows, so the template has to be rebuilt when
+// one is toggled, moved or resized.
+function refreshCardGridLayout() {
+ if (options.card_grid === true) debouncedApplyAestheticChanges();
+}
+
+/*
+Card Grid geometry (shared by the layout CSS and centerUnevenGridRows)
+
+The grid has to know about every child that takes a cell or a row: the course
+cards, the widgets placed Before/After cards (notes, focus timer, breathing,
+quotes, quick links, heatmap) and the GPA summary card. The calculator itself
+spans a full row when it is open inside the grid.
+*/
+
+const CARD_GRID_HOST = ".ic-DashboardCard__box__container";
+
+// One card-sized cell: course cards, in-card widgets and the GPA summary card.
+// (The GPA calculator `.canvasrefined-gpa` is not one: it spans a whole row.)
+// `:not(.container-mode)` on the GPA keeps these rules above the card-style margins
+// that follow them in the sheet.
+const CARD_GRID_CELLS = [
+ `${CARD_GRID_HOST} > .ic-DashboardCard`,
+ `${CARD_GRID_HOST} > .canvasrefined-widget-card.in-cards`,
+ `${CARD_GRID_HOST} > .canvasrefined-dashboard-notes.in-cards`,
+ `${CARD_GRID_HOST} > .canvasrefined-gpa-card:not(.container-mode)`,
+].join(",");
+
+function isCardGridCell(el) {
+ if (!el || !el.classList) return false;
+ if (el.classList.contains("ic-DashboardCard") || el.classList.contains("canvasrefined-gpa-card")) return true;
+ return el.classList.contains("in-cards")
+ && (el.classList.contains("canvasrefined-widget-card") || el.classList.contains("canvasrefined-dashboard-notes"));
+}
+
+// Enabled widgets rendered before/after the cards, i.e. inside the grid.
+function cardGridWidgets() {
+ return CR_SIMPLE_WIDGET_IDS.map(id => ({ id, position: crWidgetPosition(id) }))
+ .concat([{ id: "dashboard_notes", position: crWidgetPosition("dashboard_notes") }])
+ .filter(w => options[w.id] === true
+ && (w.position === "cards_start" || w.position === "cards_end"));
+}
+
+function cardGridMetrics() {
+ const cols = Math.max(1, parseInt(options.card_grid_columns, 10) || 4);
+ const rows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
+ const colGap = Math.max(0, !isNaN(parseInt(options.card_grid_column_gap, 10)) ? parseInt(options.card_grid_column_gap, 10) : 12);
+ const rowGap = Math.max(0, !isNaN(parseInt(options.card_grid_row_gap, 10)) ? parseInt(options.card_grid_row_gap, 10) : 12);
+ const gpaPos = gpaCalcPosition();
+ const gpaInGrid = options.gpa_calc === true && (gpaPos === "cards_start" || gpaPos === "cards_end");
+ const widgets = cardGridWidgets();
+ // Every widget and the GPA card need a cell, and the calculator a whole row.
+ // Nothing may rely on implicit rows: the template clips them to zero height.
+ const extraRows = Math.ceil((widgets.length + (gpaInGrid ? 1 : 0)) / cols) + (gpaInGrid ? 1 : 0);
+ return {
+ cols,
+ rows,
+ colGap,
+ rowGap,
+ widgets,
+ startWidgets: widgets.filter(w => w.position === "cards_start"),
+ gpaInGrid,
+ gpaFirst: gpaPos === "cards_start",
+ extraRows,
+ totalRows: rows + extraRows,
+ };
+}
+
function applyAestheticChanges() {
if (quizSafeModeActive()) return;
let style = document.querySelector("#canvasrefined-aesthetics") || document.createElement('style');
@@ -9504,44 +9592,58 @@ function applyAestheticChanges() {
if (options.full_width === true) style.textContent += "#wrapper,.ic-Layout-wrapper{max-width:100%!important}";
if (options.center_cards === true) style.textContent += ".ic-DashboardCard__box__container{display:flex!important;flex-wrap:wrap!important;justify-content:center!important;align-items:flex-start!important}";
// Card Grid: strict columns x rows layout, emitted after center_cards to win the
- // !important tie-break. Columns are doubled into sub-columns (each card spans 2) so
- // "center uneven rows" can shift a partial row by half a card; two spare rows keep
- // the GPA card/calculator out of the clipped implicit rows.
+ // !important tie-break. Columns are doubled into sub-columns (each cell spans 2) so
+ // "center uneven rows" can shift a partial row by half a card; spare rows keep the
+ // in-grid widgets (notes, timers, quotes, links, heatmap, GPA card/calculator) out
+ // of the clipped implicit rows.
if (options.card_grid === true) {
- const gridCols = Math.max(1, parseInt(options.card_grid_columns, 10) || 4);
- const gridRows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
- const gridColGap = Math.max(0, !isNaN(parseInt(options.card_grid_column_gap, 10)) ? parseInt(options.card_grid_column_gap, 10) : 12);
- const gridRowGap = Math.max(0, !isNaN(parseInt(options.card_grid_row_gap, 10)) ? parseInt(options.card_grid_row_gap, 10) : 12);
- // strip Canvas' natural top/left card margins; the grid supplies its own gaps
- style.textContent += `.ic-DashboardCard__box__container{margin-top:0!important;margin-left:0!important}`;
- style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{margin-top:0!important;margin-left:0!important}`;
- // CanvasRefined's own widgets carry 36px outer margins meant for the flex
- // card flow; in the grid every side is spacing the gaps already provide
- style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-widget-card,.ic-DashboardCard__box__container > .canvasrefined-dashboard-notes,.ic-DashboardCard__box__container > .canvasrefined-gpa-card,.ic-DashboardCard__box__container > .canvasrefined-gpa{margin:0!important}`;
+ const grid = cardGridMetrics();
+ const gridCols = grid.cols;
+ const gridColGap = grid.colGap;
+ const gridRowGap = grid.rowGap;
const centerGrid = options.center_cards === true;
- const gpaOn = options.gpa_calc === true;
- const gpaPos = gpaCalcPosition();
- const gpaInsideGrid = gpaPos === "cards_start" || gpaPos === "cards_end";
- const gpaFirst = gpaPos === "cards_start";
const flexGrid = options.card_grid_flex === true;
+ // content.css keeps this container a flex row whenever an in-card widget exists
+ // (so widgets line up with Canvas' loading skeletons). Those selectors carry more
+ // specificity than a bare class, so they are repeated here: grid mode has to
+ // out-specify that fallback instead of silently losing to it.
+ const gridHost = [
+ CARD_GRID_HOST,
+ `${CARD_GRID_HOST}:has(> .canvasrefined-widget-card.in-cards)`,
+ `${CARD_GRID_HOST}:has(> .canvasrefined-dashboard-notes.in-cards)`,
+ `${CARD_GRID_HOST}:has(> .canvasrefined-gpa-card:not(.container-mode))`,
+ ].join(",");
+ // strip Canvas' natural top/left card margins; the grid supplies its own gaps
+ style.textContent += `${CARD_GRID_HOST}{margin-top:0!important;margin-left:0!important}`;
+ // CanvasRefined's own widgets carry 36px outer margins meant for the flex
+ // card flow (plus the card-style spacing); in the grid every side is spacing
+ // the gaps already provide
+ style.textContent += `${CARD_GRID_CELLS}{margin:0!important}`;
+ style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa{margin:0!important}`;
if (flexGrid) {
// flexible grid: fixed-width tracks; @container queries step the column count
// down when the next column stops fitting (no window-width guessing)
- const cardW = options.customCardStyles === true && parseInt(options.cardWidth, 10) > 0
+ // content-box card padding grows the cards past the set width, so the track
+ // (and the widget cards in it) has to follow
+ const cardPad = options.customCardStyles === true ? Math.max(0, parseInt(options.cardPadding, 10) || 0) : 0;
+ const cardW = (options.customCardStyles === true && parseInt(options.cardWidth, 10) > 0
? parseInt(options.cardWidth, 10)
- : 262;
+ : 262) + 2 * cardPad;
// container queries need a size container; the card wrapper is full-width
style.textContent += `#DashboardCard_Container{container-type:inline-size}`;
- style.textContent += `.ic-DashboardCard__box__container{display:grid!important;grid-template-columns:repeat(${gridCols},${cardW}px)!important;grid-auto-rows:auto!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important${centerGrid ? ";justify-content:center!important" : ""}}`;
+ style.textContent += `${gridHost}{display:grid!important;grid-template-columns:repeat(${gridCols},${cardW}px)!important;grid-auto-rows:auto!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important${centerGrid ? ";justify-content:center!important" : ""}}`;
// grid gaps alone control spacing (beats the later customCardStyles margins)
- style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{margin-right:0!important;margin-bottom:0!important}`;
- if (gpaOn && gpaInsideGrid) {
- if (gpaFirst) {
- style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1!important}`;
- style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
+ style.textContent += `${CARD_GRID_CELLS}{margin-right:0!important;margin-bottom:0!important}`;
+ if (grid.gpaInGrid) {
+ // Pin only while the GPA is alone in that slot: with another before-card
+ // widget present, auto-placement in DOM order follows the saved widget
+ // order, which the pin would override.
+ if (grid.gpaFirst && !grid.startWidgets.length) {
+ style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1!important}`;
+ style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
} else {
// auto-placed: flows into the next open slot after the last card
- style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-column:1/-1!important}`;
+ style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa{grid-column:1/-1!important}`;
}
}
// step down to m columns once m+1 no longer fit; widest-first so the
@@ -9549,35 +9651,49 @@ function applyAestheticChanges() {
for (let m = gridCols - 1; m >= 1; m--) {
const tooWideFor = (m + 1) * cardW + m * gridColGap;
if (m === 1) {
- style.textContent += `@container (max-width:${tooWideFor - 1}px){.ic-DashboardCard__box__container{grid-template-columns:minmax(0,1fr)!important}.ic-DashboardCard__box__container > .ic-DashboardCard${gpaInsideGrid ? ",.ic-DashboardCard__box__container > .canvasrefined-gpa-card" : ""}{width:100%!important;min-width:0!important}}`;
+ style.textContent += `@container (max-width:${tooWideFor - 1}px){${gridHost}{grid-template-columns:minmax(0,1fr)!important}${CARD_GRID_CELLS}{width:100%!important;min-width:0!important}}`;
} else {
- style.textContent += `@container (max-width:${tooWideFor - 1}px){.ic-DashboardCard__box__container{grid-template-columns:repeat(${m},${cardW}px)!important}}`;
+ style.textContent += `@container (max-width:${tooWideFor - 1}px){${gridHost}{grid-template-columns:repeat(${m},${cardW}px)!important}}`;
}
}
} else {
- const totalRows = gridRows + (gpaOn && gpaInsideGrid ? 2 : 0);
- style.textContent += `.ic-DashboardCard__box__container{display:grid!important;grid-template-columns:repeat(${gridCols * 2},minmax(0,max-content))!important;grid-template-rows:repeat(${totalRows},minmax(0,auto))!important;grid-auto-rows:0!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important;overflow:hidden!important${centerGrid ? ";justify-content:center!important" : ""}}`;
- style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-column:span 2!important}`;
- if (gpaOn && gpaInsideGrid) {
+ style.textContent += `${gridHost}{display:grid!important;grid-template-columns:repeat(${gridCols * 2},minmax(0,max-content))!important;grid-template-rows:repeat(${grid.totalRows},minmax(0,auto))!important;grid-auto-rows:0!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important;overflow:hidden!important${centerGrid ? ";justify-content:center!important" : ""}}`;
+ // every cell-sized child spans one card (two sub-columns)
+ style.textContent += `${CARD_GRID_CELLS}{grid-column:span 2!important}`;
+ if (grid.gpaInGrid) {
// GPA card pinned to the first slot, or auto-placed after the cards; the
// expanded calculator always takes a full-width row
- if (gpaFirst) {
- style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1/span 2!important}`;
- style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
+ if (grid.gpaFirst && !grid.startWidgets.length) {
+ style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1/span 2!important}`;
+ style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
} else {
- style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-column:1/-1!important}`;
+ style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa{grid-column:1/-1!important}`;
}
}
}
}
if (options.customCardStyles === true) {
+ // CanvasRefined's own widget cards are card faces too, so the card width, padding,
+ // roundness and spacing settings reach them as well. Only the faces that sit in
+ // the card flow are resized/spaced: a Top/Bottom row widget is sized by the
+ // Widgets tab, so it just inherits the face styling.
+ const widgetFace = ".canvasrefined-widget-card, .canvasrefined-dashboard-notes, .canvasrefined-gpa-card";
+ const widgetCard = ".canvasrefined-widget-card.in-cards, .canvasrefined-dashboard-notes.in-cards, .canvasrefined-gpa-card:not(.container-mode)";
+ const cardRoundness = parseInt(options.cardRoundness, 10);
+ const cardSpacing = parseInt(options.cardSpacing, 10);
+ const cardWidth = parseInt(options.cardWidth, 10);
+ const cardPadding = parseInt(options.cardPadding, 10);
if (options.imageSize !== undefined && options.imageSize !== 100) style.textContent += `.ic-DashboardCard__header_image {transform: scale(${options.imageSize / 100})!important; }`;
if (options.cardRoundness !== undefined && options.cardRoundness !== 5) style.textContent += `.ic-DashboardCard {border-radius: ${options.cardRoundness}px!important;}`;
+ // widgets default to 4px, cards to 5px: follow the setting instead of the stock value
+ if (Number.isFinite(cardRoundness) && cardRoundness !== 5) style.textContent += `${widgetFace} {border-radius: ${cardRoundness}px!important;}`;
// the hero overlay sits on top of the photo, so both need the radius — rounding
// one alone leaves the other's square corners showing
if (options.imageRoundness !== undefined && options.imageRoundness !== 0) style.textContent += `.ic-DashboardCard__header_image, .ic-DashboardCard__header_hero {border-radius: ${options.imageRoundness}px!important;}`;
if (options.cardSpacing !== undefined && options.cardSpacing !== 0) style.textContent += `.ic-DashboardCard {margin-right: ${options.cardSpacing / 2}px!important; margin-bottom: ${options.cardSpacing / 2}px!important;}`;
+ if (Number.isFinite(cardSpacing) && cardSpacing !== 0) style.textContent += `${widgetCard} {margin-right: ${cardSpacing / 2}px!important; margin-bottom: ${cardSpacing / 2}px!important;}`;
if (options.cardWidth !== undefined && options.cardWidth !== 262) style.textContent += `.ic-DashboardCard {width: ${options.cardWidth}px!important;}`;
+ if (Number.isFinite(cardWidth) && cardWidth > 0 && cardWidth !== 262) style.textContent += `${widgetCard} {width: ${cardWidth}px!important;}`;
// card height goes on the hero band (146px stock): the card and header are
// content-sized, so a height on either just clips or adds dead space
if (options.condensed_cards !== true && options.cardHeight !== undefined && options.cardHeight !== null && options.cardHeight !== "") {
@@ -9588,6 +9704,11 @@ function applyAestheticChanges() {
if (options.cardPadding !== undefined && Number(options.cardPadding) > 0) {
style.textContent += `.ic-DashboardCard {padding: ${options.cardPadding}px!important; box-sizing: content-box!important;}`;
}
+ if (Number.isFinite(cardPadding) && cardPadding > 0) {
+ style.textContent += `${widgetFace} {padding: ${cardPadding}px!important; box-sizing: border-box!important;}`;
+ // fixed-width faces grow like Canvas cards; a row widget must stay inside its slot
+ style.textContent += `${widgetCard} {box-sizing: content-box!important;}`;
+ }
}
// margins always cleared so the bar lines up with the content column; hiding it is opt-in
@@ -9604,16 +9725,22 @@ Center uneven grid rows.
CSS grid always packs rows from the left, so a last row with fewer cards
than the chosen column count sits flush-left while the rows above are full.
-Pure CSS can't center a partial row, so we offset the first card of the last
+Pure CSS can't center a partial row, so we offset the first cell of the last
partial row with grid-column-start.
-The grid runs on double sub-columns (each card spans 2 — see
+The grid runs on double sub-columns (each cell spans 2 — see
applyAestheticChanges), which is what makes this possible at all: with 3
cards under a 4-card row the leftover space is one column, and centering
needs a HALF-column shift — impossible on plain column lines. On sub-columns
the shift is a whole sub-column (half a card + half a gap), so every
remainder can be centered exactly.
+In-card widgets are cells just like cards, so they count here too (a grid
+filled by cards plus widgets has a partial last row all the same). Rows are
+detected from the rendered boxes, not from a flat index: a full-width
+calculator row (or a hidden card) leaves holes that index math would read as
+the wrong row.
+
Only runs while Card Grid is on. Skips when the cards overflow the explicit
template (those extra rows are clipped anyway, and the last visible row is
full). Any previously applied offset is cleared first so re-runs (card
@@ -9622,14 +9749,14 @@ hidden/unhidden, dashboard re-render, setting changes) start clean.
function centerUnevenGridRows() {
const container = document.querySelector(".ic-DashboardCard__box__container");
if (!container) return;
- // DOM order matches auto-placement order, so chunking this list matches the real rows
- const items = Array.from(container.children).filter(el => el.classList && (el.classList.contains("ic-DashboardCard") || el.classList.contains("canvasrefined-gpa-card")));
+ // DOM order matches auto-placement order, so this list is in visual order
+ const items = Array.from(container.children).filter(isCardGridCell);
items.forEach(el => el.style.removeProperty("grid-column"));
if (options.card_grid !== true || options.card_grid_center_rows !== true) return;
// flexible grid changes the column count live, so fixed offsets would be wrong
if (options.card_grid_flex === true) return;
- const cols = Math.max(1, parseInt(options.card_grid_columns, 10) || 4);
- const rows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
+ const grid = cardGridMetrics();
+ const cols = grid.cols;
const visible = items.filter(el => getComputedStyle(el).display !== "none");
const count = visible.length;
// overflow past the template is clipped and the last visible row is full — nothing to center
@@ -9637,15 +9764,23 @@ function centerUnevenGridRows() {
const expanded = container.querySelector(":scope > .canvasrefined-gpa");
return !!expanded && getComputedStyle(expanded).display !== "none";
})();
- const capacity = cols * (rows + (options.gpa_calc === true ? 2 : 0)) - (gpaExpanded ? cols : 0);
+ const capacity = cols * grid.totalRows - (gpaExpanded ? cols : 0);
if (count === 0 || count > capacity) return;
- const lastRowStart = Math.floor((count - 1) / cols) * cols;
- const inLastRow = count - lastRowStart;
+ // Group by rendered row: cells of one row share a top edge.
+ const rows = [];
+ visible.forEach(el => {
+ const top = Math.round(el.getBoundingClientRect().top);
+ const row = rows.find(r => r.top === top);
+ if (row) row.els.push(el);
+ else rows.push({ top, els: [el] });
+ });
+ const lastRow = rows[rows.length - 1];
+ const inLastRow = lastRow ? lastRow.els.length : 0;
if (inLastRow >= cols) return;
// half the leftover, in sub-columns, paired with an explicit span; !important because
// the stylesheet span rule is !important too
const offset = cols - inLastRow;
- if (offset > 0) visible[lastRowStart].style.setProperty("grid-column", `${offset + 1} / span 2`, "important");
+ if (offset > 0) lastRow.els[0].style.setProperty("grid-column", `${offset + 1} / span 2`, "important");
}
/*
diff --git a/js/popup.js b/js/popup.js
index 53dd8ce..a30bf61 100644
--- a/js/popup.js
+++ b/js/popup.js
@@ -31,6 +31,8 @@ const syncedSubOptions = [
"daily_quotes_position",
"quick_links_position",
"heatmap_position",
+ "widget_top_height",
+ "widget_bottom_height",
"widget_row_height",
"dashboard_notes_height",
"gpa_calc_height",
@@ -278,7 +280,10 @@ const defaultOptions = {
"quick_links_position": "cards_end",
"heatmap": false,
"heatmap_position": "below",
- // Height in px (0 = auto); widget_row_height is the shared Top/Bottom row.
+ // Row height in px (0 = auto) for the Top and Bottom widget rows.
+ // widget_row_height is the pre-split shared value (legacy only).
+ "widget_top_height": 0,
+ "widget_bottom_height": 0,
"widget_row_height": 0,
"dashboard_notes_height": 0,
"gpa_calc_height": 0,
@@ -405,6 +410,12 @@ chrome.storage.onChanged.addListener((changes) => {
}
updateCardGridPreview();
scheduleGridPreviewFollowUp();
+ return;
+ }
+ // Widget toggles/positions decide which widgets take grid cells in the preview.
+ if (cardGridWidgetKeys().some(key => changes[key])) {
+ updateCardGridPreview();
+ scheduleGridPreviewFollowUp();
}
});
@@ -909,6 +920,40 @@ function countActiveDashboardCards(cards) {
return Object.values(cards || {}).filter(c => c && typeof c === "object" && !c.hidden && !c.hide).length;
}
+// Card cells are blue, in-grid widgets yellow, empty slots grey.
+const CARD_GRID_CARD_COLOR = "#56Caf0";
+const CARD_GRID_WIDGET_COLOR = "#f5c451";
+const CARD_GRID_EMPTY_COLOR = "#3c3c3c";
+
+// Storage keys the preview needs to know which widgets land inside the grid.
+function cardGridWidgetKeys() {
+ return WIDGET_LIST
+ .reduce((keys, w) => keys.concat([w.id, w.id + "_position"]), [])
+ .concat(["gpa_calc_prepend", "widget_order"]);
+}
+
+// Enabled widgets rendered before/after the cards, split by anchor. They take grid
+// cells just like course cards (see content.js cardGridWidgets).
+function cardGridWidgetCells(sync) {
+ const saved = Array.isArray(sync.widget_order && sync.widget_order.cards) ? sync.widget_order.cards : [];
+ const savedIds = saved.map(e => (typeof e === "string" ? e : (e && e.id) || null)).filter(Boolean);
+ const rank = id => {
+ const i = savedIds.indexOf(id);
+ return i === -1 ? savedIds.length : i;
+ };
+ const cells = { start: [], end: [] };
+ WIDGET_LIST.forEach(w => {
+ if (sync[w.id] !== true) return;
+ const pos = widgetPosFromSync(w.id, sync);
+ if (pos === "cards_start") cells.start.push(w.id);
+ else if (pos === "cards_end") cells.end.push(w.id);
+ });
+ // The saved order wins; WIDGET_LIST order breaks ties (sort is stable).
+ cells.start.sort((a, b) => rank(a) - rank(b));
+ cells.end.sort((a, b) => rank(a) - rank(b));
+ return cells;
+}
+
function updateCardGridPreview() {
const panel = document.getElementById("card-grid-options");
const preview = document.getElementById("card-grid-preview");
@@ -917,33 +962,58 @@ function updateCardGridPreview() {
const rows = Math.max(1, parseInt(document.getElementById("card_grid_rows").value, 10) || 1);
const colGapRaw = parseInt(document.getElementById("card_grid_column_gap").value, 10);
const colGap = Math.max(0, Math.min(10, isNaN(colGapRaw) ? 12 : colGapRaw));
- const total = cols * rows;
preview.style.gridTemplateColumns = `repeat(${cols}, 18px)`;
// Clamped so extreme values don't blow the layout out.
preview.style.columnGap = `${colGap}px`;
preview.style.rowGap = "4px";
preview.innerHTML = "";
- chrome.storage.sync.get(["custom_cards", "gpa_calc"], async s => {
+ chrome.storage.sync.get(["custom_cards", "card_grid_flex"].concat(cardGridWidgetKeys()), async s => {
if (document.getElementById("card-grid-options").style.display === "none") return;
+ // Widgets in the cards area occupy cells too, so they count against the grid.
+ const widgetCells = cardGridWidgetCells(s);
+ const widgetIds = widgetCells.start.concat(widgetCells.end);
+ const gpaInGrid = widgetIds.includes("gpa_calc");
+ // Same row budget as content.js cardGridMetrics: nothing may rely on implicit
+ // rows (the grid clips those), so each in-grid widget gets rows of its own.
+ // The flexible grid grows rows on its own, so there it is the plain rows x cols.
+ const spareRows = s.card_grid_flex === true
+ ? 0
+ : Math.ceil((widgetIds.filter(id => id !== "gpa_calc").length + (gpaInGrid ? 1 : 0)) / cols) + (gpaInGrid ? 1 : 0);
+ const total = cols * (rows + spareRows);
// Prefer the live count from a dashboard tab; it knows which cards are
// actually rendered. Falls back to the storage-derived count.
const live = await sendFromPopup("getGridCardCount");
- let active = (live && typeof live.count === "number" && live.count >= 0)
+ const cardCount = (live && typeof live.count === "number" && live.count >= 0)
? live.count
: countActiveDashboardCards(s.custom_cards);
- if (s.gpa_calc === true) active += 1;
+ // Auto-placement order: before-card widgets, the cards, after-card widgets.
+ const cells = widgetCells.start.map(id => ({ kind: "widget", id }))
+ .concat(Array.from({ length: cardCount }, () => ({ kind: "card" })),
+ widgetCells.end.map(id => ({ kind: "widget", id })));
+ const active = cells.length;
+ // The flexible grid grows rows on its own, so nothing is cut off there: draw
+ // every cell. The fixed grid clips, so it stays within its template.
+ const cellCount = s.card_grid_flex === true ? Math.max(total, active) : total;
let lastRowStart = -1, offset = 0;
if (document.getElementById("card_grid_center_rows")?.checked === true && active > 0 && active < total) {
lastRowStart = Math.floor((active - 1) / cols) * cols;
const inLastRow = active - lastRowStart;
if (inLastRow < cols) offset = cols - inLastRow;
}
- for (let i = 0; i < total; i++) {
+ for (let i = 0; i < cellCount; i++) {
const inShiftedRow = lastRowStart !== -1 && i >= lastRowStart;
if (inShiftedRow && i >= active) continue;
+ const cell = cells[i];
const square = document.createElement("div");
square.style.cssText = "width:18px;height:18px;border-radius:4px;box-sizing:border-box;";
- square.style.background = i < active ? "#56Caf0" : "#3c3c3c";
+ if (!cell) {
+ square.style.background = CARD_GRID_EMPTY_COLOR;
+ } else if (cell.kind === "widget") {
+ square.style.background = CARD_GRID_WIDGET_COLOR;
+ square.title = widgetFullName(cell.id);
+ } else {
+ square.style.background = CARD_GRID_CARD_COLOR;
+ }
if (inShiftedRow && offset > 0) {
square.style.marginLeft = `${offset * (18 + colGap) / 2}px`;
}
@@ -951,12 +1021,13 @@ function updateCardGridPreview() {
}
const warning = document.getElementById("card-grid-warning");
if (warning) {
- if (s.card_grid_flex === true) {
- warning.textContent = getMsg("flexible_grid_on") || "Flexible grid is on - cards wrap to fit narrow windows instead of being cut off.";
- warning.style.display = "block";
- } else if (active > total) {
- const gpaNote = s.gpa_calc === true ? getMsg("grid_includes_gpa", " (including the GPA calculator)") : "";
- warning.textContent = "\u26A0 " + (getMsg("grid_warning") || "Your grid holds {{TOTAL}} card{{TOTAL_S}} but you have {{ACTIVE}} card{{ACTIVE_S}}{{GPA_NOTE}} - some cards may be cut off.").replace("{{TOTAL}}", total).replace("{{TOTAL_S}}", total === 1 ? "" : "s").replace("{{ACTIVE}}", active).replace("{{ACTIVE_S}}", active === 1 ? "" : "s").replace("{{GPA_NOTE}}", gpaNote);
+ // The flexible grid wraps instead of clipping, so only the fixed grid can
+ // overflow its template.
+ if (s.card_grid_flex !== true && active > total) {
+ const widgetNote = widgetIds.length
+ ? (getMsg("grid_includes_widgets", " (including {{NAMES}})")).replace("{{NAMES}}", widgetIds.map(widgetFullName).join(", "))
+ : "";
+ warning.textContent = "\u26A0 " + (getMsg("grid_warning") || "Your grid holds {{TOTAL}} card{{TOTAL_S}} but you have {{ACTIVE}} card{{ACTIVE_S}}{{GPA_NOTE}} - some cards may be cut off.").replace("{{TOTAL}}", total).replace("{{TOTAL_S}}", total === 1 ? "" : "s").replace("{{ACTIVE}}", active).replace("{{ACTIVE_S}}", active === 1 ? "" : "s").replace("{{GPA_NOTE}}", widgetNote);
warning.style.display = "block";
} else {
warning.style.display = "none";
@@ -1140,6 +1211,13 @@ function widgetDisplayName(id) {
return id.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
}
+// Unabbreviated name, for sentences (the preview warning lists widget names).
+function widgetFullName(id) {
+ const info = WIDGET_LIST.find(w => w.id === id);
+ const msg = info && getMsg(info.nameKey);
+ return msg || widgetDisplayName(id);
+}
+
let refreshWidgetOrder = null;
function setupWidgetOrder() {
@@ -1338,14 +1416,25 @@ function setupWidgetPosition() {
});
}
-function setupWidgetHeightInput(id, initial) {
+// Height fields (px; empty = auto). `legacyKey` seeds an empty field from a key that
+// has since been split in two (the Top/Bottom row height used to be shared), so users
+// who set it keep their value until they edit the field.
+function setupWidgetHeightInput(id, initial, legacyKey) {
const el = document.getElementById(id);
if (!el) return;
const parse = (v) => {
const n = parseInt(v, 10);
return Number.isFinite(n) && n > 0 ? n : 0;
};
- el.value = parse(initial) || "";
+ const apply = (v) => { el.value = parse(v) || ""; };
+ apply(initial);
+ if (legacyKey) {
+ chrome.storage.sync.get([id, legacyKey], sync => {
+ // A stored 0 means "auto" and must not be overwritten by the old value.
+ if (sync[id] !== undefined) { if (!parse(el.value)) apply(sync[id]); return; }
+ if (!parse(el.value)) apply(sync[legacyKey]);
+ });
+ }
el.addEventListener("change", function () {
const n = Math.min(2000, Math.max(0, parseInt(this.value, 10) || 0));
this.value = n > 0 ? n : "";
@@ -1979,8 +2068,12 @@ async function setup() {
setup: (initial) => setupAnonProgressInput("anon_progress_total", initial, false),
},
{
- identifier: "widget_row_height",
- setup: (initial) => setupWidgetHeightInput("widget_row_height", initial),
+ identifier: "widget_top_height",
+ setup: (initial) => setupWidgetHeightInput("widget_top_height", initial, "widget_row_height"),
+ },
+ {
+ identifier: "widget_bottom_height",
+ setup: (initial) => setupWidgetHeightInput("widget_bottom_height", initial, "widget_row_height"),
},
{
identifier: "dashboard_notes_height",