diff --git a/html/popup.html b/html/popup.html index 2db6a44..5a46ab9 100644 --- a/html/popup.html +++ b/html/popup.html @@ -860,6 +860,13 @@ px +
+
+ Image Roundness + + px +
+
Extra Card Spacing diff --git a/js/background.js b/js/background.js index 3765103..7a3a5ab 100644 --- a/js/background.js +++ b/js/background.js @@ -57,7 +57,7 @@ chrome.runtime.onInstalled.addListener(function () { "full_width": null, "remlogo": null, "gpa_calc_bounds": { - "A+": { "cutoff": 97, "gpa": 4.3 }, + "A+": { "cutoff": 97, "gpa": 4.0 }, "A": { "cutoff": 93, "gpa": 4 }, "A-": { "cutoff": 90, "gpa": 3.7 }, "B+": { "cutoff": 87, "gpa": 3.3 }, @@ -103,9 +103,10 @@ chrome.runtime.onInstalled.addListener(function () { "custom_styles": "", "imageSize": 100, "cardRoundness": 5, + "imageRoundness": 0, 'cardSpacing': 0, "cardWidth": 262, - "cardHeight": 250, + "cardHeight": 146, "customCardStyles": false, "customBackgroundLink": "", "customBackgroundScale": 100, diff --git a/js/content.js b/js/content.js index 77a5d67..2ed885d 100644 --- a/js/content.js +++ b/js/content.js @@ -995,6 +995,7 @@ function applyOptionsChanges(changes) { break; case "imageSize": case "cardRoundness": + case "imageRoundness": case "cardSpacing": case "cardWidth": case "cardHeight": @@ -3572,7 +3573,7 @@ function populateAssignments(iscompleted = false) {
${item.context_name} - ${item.plannable.title} + ${item.plannable.title} ${convertToDueDate(item.plannable_date)}
${editButtonSvg} @@ -4854,14 +4855,24 @@ Dashboard grades // Map a percentage to a letter grade using the user's configurable GPA // calculator cutoffs (A+ down to F). Returns null when no grade is present. +// Picks the letter with the HIGHEST cutoff the percent meets so the result +// doesn't depend on the key order of the stored bounds object — theme imports +// can reorder keys (e.g. alphabetically, where "A" precedes "A+"), which made +// "+" grades unreachable and displayed e.g. 100% as "A". Cutoffs are coerced +// with Number() so string values carried in by imported themes still match. function percentToLetterGrade(percent) { const bounds = options.gpa_calc_bounds; if (!bounds || typeof percent !== "number") return null; + let best = null; + let bestCutoff = -Infinity; for (const letter of Object.keys(bounds)) { - const cutoff = bounds[letter]?.cutoff; - if (typeof cutoff === "number" && percent >= cutoff) return letter; + const cutoff = Number(bounds[letter]?.cutoff); + if (Number.isFinite(cutoff) && percent >= cutoff && cutoff > bestCutoff) { + best = letter; + bestCutoff = cutoff; + } } - return null; + return best; } function insertGrades() { @@ -5820,31 +5831,37 @@ function applyAestheticChanges() { if (options.customCardStyles === true) { 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;}`; + // Rounds the header image band. The photo is the background of + // .ic-DashboardCard__header_image, and .ic-DashboardCard__header_hero (a + // child that covers the full photo with its colored overlay + 1px + // border) sits on top of it. border-radius only clips the element it's + // on — rounding the parent alone leaves the hero's square overlay + // covering the corners, and rounding the hero alone leaves the photo's + // square corners behind it — so both elements need the same radius. + // Default 0; guard skips the default so stock cards keep square corners. + 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 (options.cardWidth !== undefined && options.cardWidth !== 262) style.textContent += `.ic-DashboardCard {width: ${options.cardWidth}px!important;}`; - // Always emit a fixed card height when custom card styles are on. The old - // `!== 250` guard silently dropped the height rule when cardHeight matched - // the default, which is exactly what happens after importing a theme that - // carries the default cardHeight (250) — leaving cards content-sized. - // Content-sized cards plus the dashboard reflow loop trigger Firefox scroll - // anchoring to yank the viewport back up while scrolling. A fixed height - // (even the default 250px) keeps layout stable. - if (options.cardHeight !== undefined && options.cardHeight !== null && options.cardHeight !== "") { - style.textContent += `.ic-DashboardCard {height: ${options.cardHeight}px!important;}`; - // Canvas sets overflow:hidden on .ic-DashboardCard. With a fixed - // height that clips the appended .canvasrefined-card-assignment area - // (the assignment rows live at the bottom of the card), making the - // .canvasrefined-assignment-link anchors unclickable for users with - // custom card styles enabled. Allow overflow so those rows stay - // visible and interactive when card assignments are shown. - if (options.assignments_due === true) style.textContent += `.ic-DashboardCard {overflow: visible!important;}`; + // Card height sizes the image band via .ic-DashboardCard__header_hero — + // the element Canvas pins at 146px that actually drives the header image + // height (both .ic-DashboardCard and .ic-DashboardCard__header are + // content-sized, so a height on either just clips or adds dead space). + // Everything else — title, actions, and the appended + // .canvasrefined-card-assignment rows — flows below the hero, so the card + // grows with the assignment list and nothing gets clipped. Skipped when + // condensed cards is on, since that mode pins the hero at 60px. + if (options.condensed_cards !== true && options.cardHeight !== undefined && options.cardHeight !== null && options.cardHeight !== "") { + style.textContent += `.ic-DashboardCard__header_hero {height: ${options.cardHeight}px!important;}`; } - // Inner card padding. Applied to the whole .ic-DashboardCard box (the - // element that holds the hero header, title, and action buttons) so the - // colored hero header and its content all get consistent breathing room - // from the card's edges. Guarded by !== 0 (default). + // Inner card padding. Applied to the whole .ic-DashboardCard box so the + // hero header, title, and action buttons all get breathing room from the + // card's edges. Canvas sizes the card with border-box + a fixed width, + // so padding alone squishes the content area (narrower image/rows) + // instead of expanding the card — switch to content-box so the padding + // grows the card outward and the content keeps its full width. + // Guarded by > 0 (default). if (options.cardPadding !== undefined && Number(options.cardPadding) > 0) { - style.textContent += `.ic-DashboardCard {padding: ${options.cardPadding}px!important;}`; + style.textContent += `.ic-DashboardCard {padding: ${options.cardPadding}px!important; box-sizing: content-box!important;}`; } } diff --git a/js/popup.js b/js/popup.js index fa625e2..cfe4ce9 100644 --- a/js/popup.js +++ b/js/popup.js @@ -32,6 +32,7 @@ const syncedSubOptions = [ "customCardStyles", "imageSize", "cardRoundness", + "imageRoundness", "cardSpacing", "cardWidth", "cardHeight", @@ -54,7 +55,7 @@ const localSwitches = []; // Theme export only carries visual settings, never personal productivity data. const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"]; const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"]; -const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding"]; +const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "imageRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding"]; const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "tab_icons"]; const exportSidebar = ["better_sidebar", "sidebar_scale"]; const exportTodo = ["better_todo", "todo_hide_feedback", "todo_full_height", "todo_confetti", "todo_progress_rings", "todo_timeframe", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "hover_preview"]; @@ -135,7 +136,7 @@ const defaultOptions = { "full_width": null, "remlogo": null, "gpa_calc_bounds": { - "A+": { "cutoff": 97, "gpa": 4.3 }, + "A+": { "cutoff": 97, "gpa": 4.0 }, "A": { "cutoff": 93, "gpa": 4 }, "A-": { "cutoff": 90, "gpa": 3.7 }, "B+": { "cutoff": 87, "gpa": 3.3 }, @@ -173,9 +174,10 @@ const defaultOptions = { "card_limit": 25, "imageSize": 100, "cardRoundness": 5, + "imageRoundness": 0, "cardSpacing": 0, "cardWidth": 262, - "cardHeight": 250, + "cardHeight": 146, "cardPadding": 0, "customCardStyles": false, "customBackgroundLink": "", @@ -401,6 +403,14 @@ function setupCardRoundnessInput(initial) { }); } +function setupImageRoundnessInput(initial) { + let el = document.querySelector("#imageRoundness"); + el.value = initial; + el.addEventListener("input", (e) => { + debouncedCardStyleSet("imageRoundness", e.target.value); + }); +} + function setupCardSpacingInput(initial) { let el = document.querySelector("#cardSpacing"); el.value = initial; @@ -1056,6 +1066,10 @@ function setup() { identifier: "cardRoundness", setup: (initial) => setupCardRoundnessInput(initial), }, + { + identifier: "imageRoundness", + setup: (initial) => setupImageRoundnessInput(initial), + }, { identifier: "cardSpacing", setup: (initial) => setupCardSpacingInput(initial), @@ -1534,7 +1548,7 @@ function setup() { "A-": { "cutoff": 90, "gpa": 4.0 }, "B+": { "cutoff": 87, "gpa": 3.0 }, "B": { "cutoff": 83, "gpa": 3.0 }, - "B-": { "cutoff": 89, "gpa": 3.0 }, + "B-": { "cutoff": 80, "gpa": 3.0 }, "C+": { "cutoff": 77, "gpa": 2.0 }, "C": { "cutoff": 73, "gpa": 2.0 }, "C-": { "cutoff": 70, "gpa": 2.0 }, @@ -1545,8 +1559,9 @@ function setup() { }); }); - // Note: the card-style number inputs (imageSize, cardRoundness, cardSpacing, - // cardWidth, cardHeight, cardPadding) are wired via their setup*Input + // Note: the card-style number inputs (imageSize, cardRoundness, + // imageRoundness, cardSpacing, cardWidth, cardHeight, cardPadding) are wired + // via their setup*Input // handlers in menu.special above, which debounce the storage writes. The // duplicate listeners that used to live here referenced nonexistent // #*Value spans (threw on every input) and double-wrote to storage, which @@ -1619,11 +1634,9 @@ async function getExport(storage, options) { case "custom_cards": let arr = []; Object.keys(storage["custom_cards"]).forEach(key => { - if (storage["custom_cards"][key].img !== "") arr.push(storage["custom_cards"][key].img); + const img = storage["custom_cards"][key].img; + if (img && img !== "none" && img.trim() !== "") arr.push(img); }); - if (arr.length === 0) { - arr = ["none"]; - } final["custom_cards"] = arr; break; case "card_colors": @@ -1767,6 +1780,7 @@ function saveCurrentTheme() { "sidebar_scale": current["sidebar_scale"], "imageSize": current["imageSize"], "cardRoundness": current["cardRoundness"], + "imageRoundness": current["imageRoundness"], "cardSpacing": current["cardSpacing"], "cardWidth": current["cardWidth"], "cardHeight": current["cardHeight"], @@ -1867,7 +1881,7 @@ function displaySavedThemes() { let title = makeElement("p", btn, { "className": "theme-button-title", "textContent": `Theme ${index + 1}`}); let date = makeElement("p", btn, { "className": "theme-button-creator", "textContent": `${getRelativeDate(created).time} ago` }); let remove = makeElement("div", btn, { "className": "theme-button-remove", "textContent": "x" }); - btn.style.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.44), rgba(0, 0, 0, 0.44)), url(${local["saved_themes"][key]["custom_cards"][0]})`; + btn.style.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.44), rgba(0, 0, 0, 0.44)), url(${local["saved_themes"][key]["custom_cards"]?.[0] || ""})`; btn.addEventListener("click", () => { importTheme(local["saved_themes"][key]); }); @@ -1906,16 +1920,23 @@ function importTheme(theme) { case "card_colors": sendFromPopup("setcolors", theme["card_colors"]); break; - case "custom_cards": + case "custom_cards": { + // "none" is a legacy placeholder meaning "this theme has + // no custom card images" — treat it as such instead of + // writing the literal string into every card (which hides + // the card image on the dashboard). Fall back to "" so + // cards keep their default Canvas images. + const themeImgs = (theme["custom_cards"] || []).filter(u => u && u !== "none" && u.trim() !== ""); if (theme["custom_cards"].length > 0) { let pos = 0; Object.keys(sync["custom_cards"]).forEach(key => { - sync["custom_cards"][key].img = theme["custom_cards"][pos]; - pos = (pos === theme["custom_cards"].length - 1) ? 0 : pos + 1; + sync["custom_cards"][key].img = themeImgs.length ? themeImgs[pos] : ""; + pos = (pos === themeImgs.length - 1) ? 0 : pos + 1; }); + final["custom_cards"] = sync["custom_cards"]; } - final["custom_cards"] = sync["custom_cards"]; break; + } default: final[key] = theme[key]; break;