mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-09-23 05:15:10 +02:00
yet another dark mode fix
This commit is contained in:
parent
6b9653cc15
commit
c63b44a0b3
@ -1239,6 +1239,18 @@ hr {
|
||||
#planner-today-btn:hover {
|
||||
background: var(--bcbackground-2) !important;
|
||||
}
|
||||
/* The "Today" button keeps its filled surface (from the rule above) but
|
||||
gets no outline. Two borders were boxing it in: the themed border the
|
||||
rule above paints on the button itself, and — the sneaky one —
|
||||
Instructure's Button variant draws its own light-gray border
|
||||
(rgb(232,234,236)) on the inner [class$="-baseButton__content"] span,
|
||||
which reads as a bright 1px ring on the dark chip. The icon buttons'
|
||||
content spans carry no border, so only Today needs this. Transparent
|
||||
(instead of border: none) keeps the button's exact dimensions. */
|
||||
#planner-today-btn,
|
||||
#planner-today-btn [class$="-baseButton__content"] {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
/* Instructure's Button variant paints its inner content span white (and a
|
||||
light gray on hover / white + inset shadow on active). That span covers
|
||||
the themed button background painted above, so the "Today" button still
|
||||
@ -1250,6 +1262,98 @@ hr {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
/* Dashboard list-view trays ("Add To Do" / "My Grades" opened from the
|
||||
header buttons): Instructure UI renders Tray panels as body-level
|
||||
portals — body > span > span[...-tray] — with a hardcoded white
|
||||
background. Paint the tray panel with the theme background; the tray
|
||||
contents already pick up the theme text color. */
|
||||
body > span > span[class*="-tray"] {
|
||||
background: var(--bcbackground-0) !important;
|
||||
}
|
||||
/* The Add To Do form ships its own <style> tag hardcoding background
|
||||
#FFFFFF on its root. */
|
||||
.UpdateItemTray-styles__root {
|
||||
background: var(--bcbackground-0) !important;
|
||||
}
|
||||
/* InstUI TextInput / Select facades (Title, Date, Time, Course fields):
|
||||
white surface, dark ink, gray border. Repaint with the theme surface,
|
||||
text, and border colors — the calendar / arrow icons inside use
|
||||
currentColor, so they follow. Attribute-contains matching because
|
||||
emotion appends animation-state classes (e.g. transition--*) after the
|
||||
component class. */
|
||||
[class*="-textInput__facade"] {
|
||||
background: var(--bcbackground-1) !important;
|
||||
border-color: var(--bcborders) !important;
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
/* Field labels (Title / Date / Time / Course / Details) and the date-time
|
||||
summary message keep Canvas's dark ink. */
|
||||
[class$="-formFieldLayout__label"],
|
||||
[class$="-formFieldMessage"] {
|
||||
color: var(--bctext-1) !important;
|
||||
}
|
||||
/* InstUI ContextView popovers — the opportunities popup behind "Show My
|
||||
Grades" and the date-picker calendar behind the Date field: white card
|
||||
with dark text. Theme the card; descendants without their own ink
|
||||
color (weekday headers, month label) inherit from here. */
|
||||
[class*="-contextView__content"] {
|
||||
background: var(--bcbackground-0) !important;
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
/* The opportunities popup's tab labels carry their own dark ink. */
|
||||
[class*="-contextView__content"] [class$="-view-tab"] {
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
/* Flatten the white InstUI View surfaces nested inside those popovers
|
||||
(the calendar body, the opportunities tab strip, the panel content) so
|
||||
the themed card shows through. */
|
||||
[class*="-contextView__content"] [class*="-view--inlineBlock"],
|
||||
[class*="-contextView__content"] [class*="-view--block"],
|
||||
[class*="-contextView__content"] [class*="-view-tabs__container"],
|
||||
[class*="-contextView__content"] [class*="-view-panel__content"],
|
||||
[class*="-contextView__content"] [class*="-calendar__navigation"] {
|
||||
background: transparent !important;
|
||||
}
|
||||
/* Calendar day chips: white squares with dark numbers. Flatten them and
|
||||
recolor; the selected day keeps a filled chip (matched structurally via
|
||||
aria-selected, since the chip's emotion class is a content hash). */
|
||||
[class*="-calendarDay__day"] {
|
||||
background: transparent !important;
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
button[aria-selected="true"] > [class*="-calendarDay__day"] {
|
||||
background: var(--bclinks) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
/* InstUI Select dropdowns (Time / Course) open body-level popover portals
|
||||
with white View wrappers and white option rows around the options list.
|
||||
Theme the list, flatten the wrappers and rows; the wrappers' emotion
|
||||
classes carry no semantic suffix, so they are matched structurally with
|
||||
:has() on the options list they contain (portal pattern: body > span). */
|
||||
[class*="-options__list"] {
|
||||
background: var(--bcbackground-0) !important;
|
||||
color: var(--bctext-0) !important;
|
||||
border-color: var(--bcborders) !important;
|
||||
}
|
||||
[class*="-options__list"] [class$="-optionItem__container"] {
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
[class$="-optionItem"] {
|
||||
background: transparent !important;
|
||||
}
|
||||
[class$="-optionItem"]:hover,
|
||||
[class$="-optionItem"][aria-selected="true"] {
|
||||
background: var(--bcbackground-2) !important;
|
||||
}
|
||||
body > span span:has([class*="-options__list"]) {
|
||||
background: transparent !important;
|
||||
}
|
||||
/* Planner "Submitted" pill in the completed-items row: InstUI renders it
|
||||
as a white chip with gray text. */
|
||||
.BadgeList-styles__item [class*="-pill"] {
|
||||
background: var(--bcbackground-2) !important;
|
||||
color: var(--bctext-1) !important;
|
||||
}
|
||||
/* Flash alert toasts (.flashalert-message, e.g. "Nothing planned today.
|
||||
Selecting next item."): Canvas renders them as white cards with dark
|
||||
text, unthemed in dark mode. Paint them with the theme background and
|
||||
|
||||
200
js/content.js
200
js/content.js
@ -105,10 +105,44 @@ function findContentContainer() {
|
||||
|
||||
let submissionPageButtonObserver = null;
|
||||
let submissionButtonScheduled = false;
|
||||
let assignmentButtonScheduled = false;
|
||||
let profileLogoutButtonObserver = null;
|
||||
let newCanvasButtonObserver = null;
|
||||
let sequenceFooterObserver = null;
|
||||
|
||||
// Current user id, needed to build "Go to Grades" links on assignment pages.
|
||||
// The page's ENV global isn't visible to content scripts (isolated world), so
|
||||
// ask the Canvas API once and cache the result.
|
||||
// undefined = not fetched yet, null = fetch failed, number = ok.
|
||||
let currentUserIdCache;
|
||||
let currentUserIdPromise = null;
|
||||
function ensureCurrentUserId() {
|
||||
if (currentUserIdCache !== undefined) return Promise.resolve(currentUserIdCache);
|
||||
if (!currentUserIdPromise) {
|
||||
currentUserIdPromise = getData(`${domain}/api/v1/users/self`)
|
||||
.then(user => {
|
||||
currentUserIdCache = (user && user.id) || null;
|
||||
})
|
||||
.catch(() => {
|
||||
currentUserIdCache = null;
|
||||
})
|
||||
.then(() => {
|
||||
currentUserIdPromise = null;
|
||||
return currentUserIdCache;
|
||||
});
|
||||
}
|
||||
return currentUserIdPromise;
|
||||
}
|
||||
|
||||
// Assignment pages (/courses/123/assignments/456) link to the current user's
|
||||
// submission ("grades") page for that assignment. The lookahead keeps this
|
||||
// from matching the submission pages themselves (/.../submissions/678).
|
||||
function getAssignmentGradesLink() {
|
||||
const match = window.location.pathname.match(/^\/courses\/(\d+)\/assignments\/(\d+)(?!\/submissions)(?:\/|$)/);
|
||||
if (!match || currentUserIdCache == null) return null;
|
||||
return `${domain}/courses/${match[1]}/assignments/${match[2]}/submissions/${currentUserIdCache}`;
|
||||
}
|
||||
|
||||
function addSubmissionPageButton() {
|
||||
const assignmentLink = getSubmissionAssignmentLink();
|
||||
if (!assignmentLink) return;
|
||||
@ -139,6 +173,31 @@ function addSubmissionPageButton() {
|
||||
}
|
||||
}
|
||||
|
||||
// Assignment pages: /courses/123/assignments/456 — add a "Go to Grades" button
|
||||
// to the right edge of the title row.
|
||||
function addAssignmentPageButton() {
|
||||
const link = getAssignmentGradesLink();
|
||||
if (!link) return;
|
||||
// Place the button inside the assignment header's .title-content block,
|
||||
// pinned to its right edge on the title's line. .title-content is a plain
|
||||
// block wrapping the <h1>, so switch it to a flex row (h1 left, button
|
||||
// right); the h1 still wraps its text when long.
|
||||
const titleContent = document.querySelector(".assignment-title .title-content")
|
||||
|| document.querySelector(".title-content");
|
||||
if (!titleContent || titleContent.querySelector("#canvasrefined-assignment-grades")) return;
|
||||
|
||||
titleContent.style.display = "flex";
|
||||
titleContent.style.alignItems = "center";
|
||||
titleContent.style.gap = "12px";
|
||||
makeElement("a", titleContent, {
|
||||
id: "canvasrefined-assignment-grades",
|
||||
className: "canvasrefined-custom-btn",
|
||||
href: link,
|
||||
textContent: "Go to Grades",
|
||||
style: "display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:auto;padding:6px 12px;text-decoration:none;font-weight:700;font-size:16px;color:inherit!important;white-space:nowrap;",
|
||||
});
|
||||
}
|
||||
|
||||
function addProfileLogoutPageButton() {
|
||||
if (!isProfilePage()) return;
|
||||
const content = document.getElementById("content");
|
||||
@ -208,6 +267,44 @@ function maintainSubmissionPageButton() {
|
||||
});
|
||||
}
|
||||
|
||||
// Same reconciliation pattern as maintainSubmissionPageButton, but for the
|
||||
// "Go to Grades" button on assignment pages. The grades link needs the
|
||||
// current user id, so on the first assignment page visit we kick off the API
|
||||
// fetch and re-run once it resolves.
|
||||
function maintainAssignmentPageButton() {
|
||||
if (assignmentButtonScheduled) return;
|
||||
assignmentButtonScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
assignmentButtonScheduled = false;
|
||||
const isAssignmentPage = /^\/courses\/\d+\/assignments\/\d+(?!\/submissions)(?:\/|$)/.test(window.location.pathname);
|
||||
const existing = document.getElementById("canvasrefined-assignment-grades");
|
||||
if (!isAssignmentPage) {
|
||||
if (existing) {
|
||||
const titleContent = existing.closest(".title-content");
|
||||
existing.remove();
|
||||
// Undo the flex-row layout we applied to the title block.
|
||||
if (titleContent) {
|
||||
titleContent.style.display = "";
|
||||
titleContent.style.alignItems = "";
|
||||
titleContent.style.gap = "";
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (currentUserIdCache === undefined) {
|
||||
ensureCurrentUserId().then(() => maintainAssignmentPageButton());
|
||||
return;
|
||||
}
|
||||
const link = getAssignmentGradesLink();
|
||||
if (!link) return;
|
||||
if (existing) {
|
||||
if (existing.href !== link) existing.href = link;
|
||||
return;
|
||||
}
|
||||
addAssignmentPageButton();
|
||||
});
|
||||
}
|
||||
|
||||
function isAssignmentPage() {
|
||||
return /^\/courses\/\d+\/assignments(?:\/\d+)?(?:\/|$)/.test(current_page);
|
||||
}
|
||||
@ -270,18 +367,25 @@ function watchSequenceFooter() {
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// One persistent, rAF-throttled observer that keeps the button present whenever
|
||||
// we're on a submission page. Unlike the old 10s-disconnecting observer, this
|
||||
// survives Canvas' post-navigation re-renders that remove injected nodes. The
|
||||
// extra delayed checks cover React hydration that wipes the button after our
|
||||
// first add without emitting any later mutation for the observer to catch.
|
||||
// One persistent, rAF-throttled observer that keeps both assignment-page
|
||||
// navigation buttons present: the "Back to Assignment" button on submission
|
||||
// pages and the "Go to Grades" button on assignment pages. Unlike the old
|
||||
// 10s-disconnecting observer, this survives Canvas' post-navigation re-renders
|
||||
// that remove injected nodes. The extra delayed checks cover React hydration
|
||||
// that wipes the button after our first add without emitting any later mutation
|
||||
// for the observer to catch.
|
||||
function maintainAssignmentNavButtons() {
|
||||
maintainSubmissionPageButton();
|
||||
maintainAssignmentPageButton();
|
||||
}
|
||||
|
||||
function watchSubmissionPageButton() {
|
||||
if (submissionPageButtonObserver) return;
|
||||
maintainSubmissionPageButton();
|
||||
submissionPageButtonObserver = new MutationObserver(maintainSubmissionPageButton);
|
||||
maintainAssignmentNavButtons();
|
||||
submissionPageButtonObserver = new MutationObserver(maintainAssignmentNavButtons);
|
||||
submissionPageButtonObserver.observe(document.documentElement, { childList: true, subtree: true });
|
||||
for (const ms of [300, 800, 1600, 3000, 5000]) {
|
||||
setTimeout(maintainSubmissionPageButton, ms);
|
||||
setTimeout(maintainAssignmentNavButtons, ms);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1907,6 +2011,49 @@ function getDashboardCourseOrder() {
|
||||
return order;
|
||||
}
|
||||
|
||||
// Keep the centered % / count text clear of the progress graphics (the rings'
|
||||
// center hole and the rainbow's bowl). The text block is measured after each
|
||||
// render; if it would cross into the strokes its fonts are scaled down, and
|
||||
// when the hole is really tight the count line is dropped before the % is
|
||||
// allowed to shrink below readable size. Font sizes reset to the defaults on
|
||||
// every render so the text grows back when there is room again.
|
||||
// `neededRadius(hw, hh)` returns the distance from the hole's center to the
|
||||
// farthest text corner; the text fits when that is <= availableRadius.
|
||||
function fitProgressOverlayText(overlay, neededRadius, availableRadius) {
|
||||
const textWrap = overlay?.firstElementChild;
|
||||
const pct = overlay?.querySelector('.canvasrefined-progress-percent');
|
||||
const cnt = overlay?.querySelector('.canvasrefined-progress-count');
|
||||
if (!textWrap || !pct || !cnt || textWrap === pct || textWrap === cnt) return;
|
||||
// Undo any shrink applied by a previous render before measuring (these
|
||||
// are the default sizes the overlays are created with).
|
||||
pct.style.fontSize = '20px';
|
||||
cnt.style.fontSize = '12px';
|
||||
cnt.style.display = '';
|
||||
if (!availableRadius || availableRadius <= 0) return;
|
||||
let w = textWrap.offsetWidth;
|
||||
let h = textWrap.offsetHeight;
|
||||
if (!w || !h) return;
|
||||
let needed = neededRadius(w / 2, h / 2);
|
||||
if (needed <= availableRadius) return;
|
||||
let scale = availableRadius / needed;
|
||||
if (20 * scale < 11) {
|
||||
// Too tight for both lines: drop the count and re-fit the % alone.
|
||||
cnt.style.display = 'none';
|
||||
w = textWrap.offsetWidth;
|
||||
h = textWrap.offsetHeight;
|
||||
needed = neededRadius(w / 2, h / 2);
|
||||
if (needed <= availableRadius) return;
|
||||
scale = availableRadius / needed;
|
||||
}
|
||||
pct.style.fontSize = `${Math.max(10, Math.round(20 * scale))}px`;
|
||||
if (cnt.style.display !== 'none') cnt.style.fontSize = `${Math.max(9, Math.round(12 * scale))}px`;
|
||||
// Final safety: if the readable-size floors above still don't fit, drop
|
||||
// the count line so the % is guaranteed to clear the strokes.
|
||||
if (neededRadius(textWrap.offsetWidth / 2, textWrap.offsetHeight / 2) > availableRadius) {
|
||||
cnt.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Mode "rings": concentric rings, one per course, each filled by completion.
|
||||
function renderProgressRingsMode(wrapper, shown, totalAll, completedAll, percent) {
|
||||
const containerWidth = wrapper.clientWidth || 240;
|
||||
@ -1946,16 +2093,21 @@ function renderProgressRingsMode(wrapper, shown, totalAll, completedAll, percent
|
||||
const decrement = stroke + gap;
|
||||
const ringCount = shown.length;
|
||||
const startRadius = outerRadius - stroke / 2;
|
||||
const minCenterRadius = 28;
|
||||
// Keep the center hole big enough for the % / count text so the numbers
|
||||
// never sit on top of the ring strokes (fitProgressOverlayText shrinks the
|
||||
// text as a safety net for unusually wide labels).
|
||||
const minCenterRadius = 44;
|
||||
const requiredSpace = (ringCount - 1) * decrement + stroke / 2 + minCenterRadius;
|
||||
let adjustFactor = 1;
|
||||
if (requiredSpace > startRadius) {
|
||||
adjustFactor = (startRadius - minCenterRadius - stroke / 2) / Math.max(1, (ringCount - 1) * decrement);
|
||||
}
|
||||
|
||||
let innerEdge = startRadius - stroke / 2;
|
||||
shown.forEach((entry, idx) => {
|
||||
const radius = startRadius - idx * Math.max(1, Math.floor(decrement * adjustFactor));
|
||||
if (radius <= 0) return;
|
||||
innerEdge = Math.min(innerEdge, radius - stroke / 2);
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const prog = entry.total === 0 ? 0 : entry.completed / entry.total;
|
||||
const color = courseRingColor(entry.courseId, idx);
|
||||
@ -2032,6 +2184,9 @@ function renderProgressRingsMode(wrapper, shown, totalAll, completedAll, percent
|
||||
};
|
||||
});
|
||||
|
||||
// Shrink the % / count text if it would reach the innermost ring.
|
||||
fitProgressOverlayText(overlay, (hw, hh) => Math.hypot(hw, hh), Math.max(0, innerEdge - 2));
|
||||
|
||||
const maxIdx = shown.length - 1;
|
||||
svg.querySelectorAll('circle').forEach(c => {
|
||||
const idx = parseInt(c.getAttribute('data-idx'));
|
||||
@ -2066,8 +2221,11 @@ function renderProgressRainbow(wrapper, shown, totalAll, completedAll, percent)
|
||||
svg.setAttribute('height', String(svgHeight));
|
||||
svg.setAttribute('viewBox', `0 0 ${size} ${svgHeight}`);
|
||||
|
||||
// shrink spacing if too many classes would overflow the inner radius
|
||||
const minInnerRadius = 14;
|
||||
// shrink spacing if too many classes would overflow the inner radius;
|
||||
// keep the inner bowl big enough for the % / count text so the numbers
|
||||
// never sit on top of the arcs (fitProgressOverlayText shrinks the text
|
||||
// as a safety net for unusually wide labels).
|
||||
const minInnerRadius = 56;
|
||||
const requiredSpace = (ringCount - 1) * decrement;
|
||||
let adjustFactor = 1;
|
||||
if (requiredSpace > outerRadius - minInnerRadius) {
|
||||
@ -2170,13 +2328,31 @@ function renderProgressRainbow(wrapper, shown, totalAll, completedAll, percent)
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'canvasrefined-progress-overlay';
|
||||
overlay.style.cssText = `position:absolute;left:0;top:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;transform:translateY(${nudge}px);`;
|
||||
overlay.innerHTML = `<div class='canvasrefined-progress-percent' style='font-weight:700;font-size:20px;line-height:1;color:var(--bctext-0);'></div><div class='canvasrefined-progress-count' style='font-size:12px;margin-top:3px;color:var(--bctext-0);'></div>`;
|
||||
// Same textWrap structure as rings mode so fitProgressOverlayText
|
||||
// measures the whole percent+count block, not just one line.
|
||||
const textWrap = document.createElement('div');
|
||||
textWrap.style.cssText = 'text-align:center;color:var(--bctext-0);';
|
||||
textWrap.innerHTML = `<div class='canvasrefined-progress-percent' style='font-weight:700;font-size:20px;line-height:1;'></div><div class='canvasrefined-progress-count' style='font-size:12px;margin-top:3px;'></div>`;
|
||||
overlay.appendChild(textWrap);
|
||||
wrapper.appendChild(overlay);
|
||||
} else {
|
||||
overlay.style.transform = `translateY(${nudge}px)`;
|
||||
}
|
||||
overlay.querySelector('.canvasrefined-progress-percent').textContent = `${percent}%`;
|
||||
overlay.querySelector('.canvasrefined-progress-count').textContent = `${completedAll}/${totalAll} done`;
|
||||
|
||||
// Shrink the % / count text if any corner would cross the innermost arc.
|
||||
// The text is centered at (cx, holeCenterY); the bowl is the semicircle
|
||||
// of innerRadius around (cx, baseY), so check the farthest text corner
|
||||
// against the bowl's inner edge.
|
||||
const bowlRadius = Math.max(0, innerRadius - stroke / 2 - 2);
|
||||
fitProgressOverlayText(overlay, (hw, hh) => {
|
||||
const dv = Math.max(
|
||||
Math.abs(baseY - holeCenterY + hh),
|
||||
Math.abs(baseY - holeCenterY - hh)
|
||||
);
|
||||
return Math.hypot(hw, dv);
|
||||
}, bowlRadius);
|
||||
}
|
||||
|
||||
// Mode "lines": one horizontal bar per course, each with its own %.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user