31 KiB
Inline Subreddit Color Popover Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add an inline color-override popover to subreddit badges so users can set per-subreddit colors directly on the page without opening the extension popup.
Architecture: Extends the existing vanilla JS + CSS content script. A new color-popover.js content script handles all popover logic. The subredditColors storage schema is upgraded to be mode-aware (light/dark). Existing files are modified minimally: settings.js gains migration logic, badges.js gains data attributes and gear icon injection, and the popup UI is updated for the new schema.
Tech Stack: Vanilla JavaScript, CSS, Firefox WebExtension API (Manifest V2)
Global Constraints
- Target: old Reddit only (
*://old.reddit.com/*and*://www.reddit.com/*) - Manifest V2 (not V3)
- No ES module imports in content scripts — all JS files listed in manifest
content_scripts.js, shared global scope - All injected DOM elements use the
rt-class prefix - All CSS custom properties use the
--rt-prefix - Storage:
browser.storage.local(not sync) - Post selector:
div.thing.link(never barediv.thing) - No automated tests — manual testing via
about:debugging
File Structure
| File | Responsibility |
|---|---|
lib/settings.js |
Modify. Add v1→v2 migration for subredditColors schema. |
content/tweaks/badges.js |
Modify. Add data-rt-subreddit attribute, gear icon injection, mode-aware override reading, export isDarkMode/getColorPrefix for popover use. |
content/styles/badges.css |
Modify. Add gear icon styles, popover styles (light + dark), replace hover opacity with filter. |
content/tweaks/color-popover.js |
Create. Popover DOM creation, positioning, open/close, save/reset, color input syncing, mode toggle. |
popup/popup.js |
Modify. Update per-subreddit overrides for mode-aware schema. |
popup/popup.html |
Modify. Update per-subreddit override UI with mode toggle. |
popup/popup.css |
Modify. Add styles for mode toggle in subreddit entries. |
manifest.json |
Modify. Add color-popover.js to content scripts array. |
Task 1: Mode-Aware Schema Migration and Override Reading
Files:
- Modify:
lib/settings.js:22-27 - Modify:
content/tweaks/badges.js:18-35 - Modify:
content/tweaks/badges.js:138-148
Interfaces:
-
Consumes: existing
window.RedditTweaks.loadSettings(),window.RedditTweaks.saveSettings() -
Produces:
loadSettings()now returnssubredditColorsin v2 format:{ subredditName: { light: {bg, border, text}, dark: {bg, border, text} } }applyColorsToContainer()reads fromoverrides[prefix]instead of flatoverridesupdateColors()reads subreddit name fromdata-rt-subredditattribute on the badge containerwindow.RedditTweaks.isDarkMode(settings)— exported for popover usewindow.RedditTweaks.getColorPrefix(settings)— exported for popover use
-
Step 1: Update
lib/settings.js— add migration inloadSettings()
Replace the entire loadSettings function (lines 22-27) with:
function migrateSubredditColors(colors) {
let migrated = false;
const result = {};
for (const name of Object.keys(colors)) {
const entry = colors[name];
if (entry && entry.bg !== undefined && !entry.light && !entry.dark) {
result[name] = { light: { bg: entry.bg, border: entry.border, text: entry.text }, dark: { bg: entry.bg, border: entry.border, text: entry.text } };
migrated = true;
} else {
result[name] = entry;
}
}
return { result: result, migrated: migrated };
}
async function loadSettings() {
const stored = await browser.storage.local.get(null);
const merged = Object.assign({}, DEFAULTS, stored);
const migration = migrateSubredditColors(merged.subredditColors || {});
merged.subredditColors = migration.result;
if (migration.migrated) {
await browser.storage.local.set({ subredditColors: merged.subredditColors });
}
return merged;
}
- Step 2: Update
content/tweaks/badges.js— mode-awareapplyColorsToContainer()
Replace lines 27-34 of applyColorsToContainer() (the override reading and CSS property setting):
const entry = subredditName && settings.subredditColors[subredditName];
const overrides = entry && entry[prefix];
container.style.setProperty("--rt-sub-bg", overrides?.bg || settings[prefix + ".subredditBgColor"]);
container.style.setProperty("--rt-sub-border", overrides?.border || settings[prefix + ".subredditBorderColor"]);
container.style.setProperty("--rt-sub-text", overrides?.text || settings[prefix + ".subredditTextColor"]);
container.style.setProperty("--rt-com-bg", settings[prefix + ".commentsBgColor"]);
container.style.setProperty("--rt-com-border", settings[prefix + ".commentsBorderColor"]);
container.style.setProperty("--rt-com-text", settings[prefix + ".commentsTextColor"]);
- Step 3: Update
content/tweaks/badges.js—data-rt-subredditattribute inprocessPost()
After line 60 (subredditName = subLink.textContent.trim().replace(/^r\//, "");), add:
container.setAttribute("data-rt-subreddit", subredditName);
- Step 4: Update
content/tweaks/badges.js—updateColors()reads from data attribute
Replace lines 142-145 of updateColors():
const subredditName = container.getAttribute("data-rt-subreddit");
- Step 5: Export
isDarkModeandgetColorPrefixon thebadgeTweaknamespace
Replace lines 157-161:
window.RedditTweaks = window.RedditTweaks || {};
window.RedditTweaks.badgeTweak = {
apply: apply,
updateColors: updateColors,
isDarkMode: isDarkMode,
getColorPrefix: getColorPrefix,
};
- Step 6: Verify JS is valid
Run: node --check lib/settings.js && node --check content/tweaks/badges.js
Expected: no output (exit 0)
- Step 7: Commit
git add lib/settings.js content/tweaks/badges.js
git commit -m "feat: mode-aware subreddit color overrides with v1→v2 migration"
Task 2: Gear Icon and Badge CSS Changes
Files:
- Modify:
content/tweaks/badges.js:54-67(insideprocessPost()) - Modify:
content/styles/badges.css
Interfaces:
-
Consumes:
processPost()from Task 1 (withdata-rt-subredditattribute already added) -
Produces:
- Each subreddit badge
<a>contains a<span class="rt-badge-gear">⚙</span>child - CSS rules for
.rt-badge-gearvisibility/positioning,.rt-badge--subredditrelative positioning, badge hoverfilter: brightness(0.9)
- Each subreddit badge
-
Step 1: Add gear icon injection in
processPost()
After the subreddit badge is created and appended (after line 61 container.appendChild(badge);), add the gear span:
const gear = document.createElement("span");
gear.className = "rt-badge-gear";
gear.textContent = "\u2699";
gear.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
if (window.RedditTweaks.colorPopover) {
window.RedditTweaks.colorPopover.open(badge, subredditName, currentSettings);
}
});
badge.appendChild(gear);
- Step 2: Replace
badges.csswith updated version
Replace the entire file content:
/* --- FOUC prevention: hide original elements immediately --- */
html.rt-enabled .thing.link .tagline .subreddit,
html.rt-enabled .thing.link .flat-list li:has(> .comments) {
display: none !important;
}
/* --- Badge container --- */
.reddit-tweaks-badges {
margin-bottom: 4px;
line-height: 1.6;
}
/* --- Shared badge styles --- */
.rt-badge {
display: inline-block;
border: 1px solid;
border-radius: 3px;
padding: 2px 6px;
font-size: 11px;
font-weight: bold;
text-decoration: none;
vertical-align: middle;
margin-right: 6px;
cursor: pointer;
}
.rt-badge:hover {
filter: brightness(0.9);
}
/* --- Subreddit badge: relative for gear positioning --- */
.rt-badge--subreddit {
position: relative;
padding-right: 18px;
background: var(--rt-sub-bg, #fde8e0);
border-color: var(--rt-sub-border, #c84a20);
color: var(--rt-sub-text, #c84a20);
}
.rt-badge--comments {
background: var(--rt-com-bg, #e0e0e0);
border-color: var(--rt-com-border, #4a4a4a);
color: var(--rt-com-text, #4a4a4a);
}
/* --- Dark mode --- */
.rt-dark .rt-badge--subreddit {
background: var(--rt-sub-bg, #3d2518);
border-color: var(--rt-sub-border, #ff6b3d);
color: var(--rt-sub-text, #ff6b3d);
}
.rt-dark .rt-badge--comments {
background: var(--rt-com-bg, #3a3a3a);
border-color: var(--rt-com-border, #888888);
color: var(--rt-com-text, #cccccc);
}
/* --- Gear icon --- */
.rt-badge-gear {
position: absolute;
right: 3px;
top: 50%;
transform: translateY(-50%);
font-size: 10px;
line-height: 1;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
cursor: pointer;
}
.rt-badge--subreddit:hover .rt-badge-gear {
opacity: 0.6;
pointer-events: auto;
}
.rt-badge-gear:hover {
opacity: 1 !important;
}
/* --- Color popover --- */
.rt-color-popover {
position: absolute;
z-index: 10000;
background: #fff;
border: 1px solid #ccc;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 12px;
width: 260px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 13px;
color: #1a1a1a;
display: none;
}
.rt-color-popover.rt-popover-visible {
display: block;
}
/* Popover arrow */
.rt-color-popover::before {
content: "";
position: absolute;
top: -6px;
left: 16px;
width: 10px;
height: 10px;
background: #fff;
border-left: 1px solid #ccc;
border-top: 1px solid #ccc;
transform: rotate(45deg);
}
.rt-popover-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #eee;
}
.rt-popover-title {
font-weight: 600;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
}
.rt-popover-close {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
color: #999;
padding: 0 2px;
line-height: 1;
}
.rt-popover-close:hover {
color: #333;
}
.rt-popover-mode {
margin-bottom: 8px;
}
.rt-popover-mode select {
width: 100%;
padding: 3px 6px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 12px;
font-family: inherit;
}
.rt-popover-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.rt-popover-row-label {
font-size: 12px;
flex-shrink: 0;
margin-right: 8px;
}
.rt-popover-row-inputs {
display: flex;
align-items: center;
gap: 4px;
}
.rt-popover-row input[type="color"] {
width: 28px;
height: 20px;
border: 1px solid #ccc;
border-radius: 3px;
padding: 1px;
cursor: pointer;
}
.rt-popover-row input[type="text"] {
width: 70px;
padding: 2px 4px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 12px;
font-family: monospace;
}
.rt-popover-row input[type="text"].rt-input-error {
border-color: #d9534f;
}
.rt-popover-preview {
margin: 8px 0;
padding: 6px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
text-align: center;
}
.rt-popover-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
gap: 6px;
}
.rt-popover-footer-right {
display: flex;
gap: 6px;
}
.rt-btn-save {
background: #4a90d9;
color: #fff;
border: 1px solid #3a7bc8;
border-radius: 3px;
padding: 4px 12px;
font-size: 12px;
cursor: pointer;
}
.rt-btn-save:hover {
background: #3a7bc8;
}
.rt-btn-cancel {
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
padding: 4px 10px;
font-size: 12px;
cursor: pointer;
}
.rt-btn-cancel:hover {
background: #e0e0e0;
}
.rt-btn-reset {
background: none;
border: 1px solid #ccc;
border-radius: 3px;
padding: 4px 10px;
font-size: 12px;
cursor: pointer;
color: #666;
}
.rt-btn-reset:hover {
background: #f5f5f5;
color: #333;
}
/* --- Popover dark mode --- */
.rt-color-popover.rt-dark {
background: #2a2a2a;
border-color: #555;
color: #e0e0e0;
}
.rt-color-popover.rt-dark::before {
background: #2a2a2a;
border-left-color: #555;
border-top-color: #555;
}
.rt-color-popover.rt-dark .rt-popover-header {
border-bottom-color: #444;
}
.rt-color-popover.rt-dark .rt-popover-close {
color: #888;
}
.rt-color-popover.rt-dark .rt-popover-close:hover {
color: #ccc;
}
.rt-color-popover.rt-dark .rt-popover-mode select {
background: #333;
border-color: #555;
color: #e0e0e0;
}
.rt-color-popover.rt-dark .rt-popover-row input[type="color"] {
border-color: #555;
}
.rt-color-popover.rt-dark .rt-popover-row input[type="text"] {
background: #333;
border-color: #555;
color: #e0e0e0;
}
.rt-color-popover.rt-dark .rt-popover-preview {
border-top-color: #444;
border-bottom-color: #444;
}
.rt-color-popover.rt-dark .rt-btn-save {
background: #4a90d9;
border-color: #3a7bc8;
}
.rt-color-popover.rt-dark .rt-btn-cancel {
background: #444;
border-color: #555;
color: #ccc;
}
.rt-color-popover.rt-dark .rt-btn-reset {
border-color: #555;
color: #999;
}
.rt-color-popover.rt-dark .rt-btn-reset:hover {
background: #333;
color: #ccc;
}
- Step 3: Verify files are valid
Run: node --check content/tweaks/badges.js
Expected: no output (exit 0)
- Step 4: Commit
git add content/tweaks/badges.js content/styles/badges.css
git commit -m "feat: gear icon on subreddit badges and popover CSS"
Task 3: Color Popover Logic
Files:
- Create:
content/tweaks/color-popover.js - Modify:
manifest.json:20-24
Interfaces:
-
Consumes:
window.RedditTweaks.loadSettings()→Promise<object>(fromlib/settings.js)window.RedditTweaks.saveSettings(settings)→Promise<void>(fromlib/settings.js)window.RedditTweaks.badgeTweak.isDarkMode(settings)→boolean(from Task 1)window.RedditTweaks.badgeTweak.getColorPrefix(settings)→"light"|"dark"(from Task 1)- Gear icon click handler calls
window.RedditTweaks.colorPopover.open(badge, name, settings)(from Task 2)
-
Produces:
window.RedditTweaks.colorPopover.open(badgeElement, subredditName, settings)— opens/repositions the popoverwindow.RedditTweaks.colorPopover.close()— closes the popover without saving- The popover element is injected into
document.bodyonce
-
Step 1: Create
content/tweaks/color-popover.js
(function () {
"use strict";
var popoverEl = null;
var currentSubreddit = null;
var currentAnchor = null;
var editState = { light: null, dark: null };
var selectedMode = "light";
var els = {};
function getHelpers() {
return {
isDarkMode: window.RedditTweaks.badgeTweak.isDarkMode,
getColorPrefix: window.RedditTweaks.badgeTweak.getColorPrefix,
};
}
function hexValid(val) {
return /^#[0-9a-f]{6}$/i.test(val);
}
function colorToHex(val) {
if (!val) return "#ffffff";
return val;
}
function createPopover() {
var el = document.createElement("div");
el.className = "rt-color-popover";
el.innerHTML = [
'<div class="rt-popover-header">',
' <span class="rt-popover-title"></span>',
' <button class="rt-popover-close" type="button">\u00d7</button>',
"</div>",
'<div class="rt-popover-mode">',
' <select class="rt-popover-mode-select">',
' <option value="light">Light</option>',
' <option value="dark">Dark</option>',
" </select>",
"</div>",
'<div class="rt-popover-body">',
' <div class="rt-popover-row" data-prop="bg">',
' <span class="rt-popover-row-label">Background</span>',
' <div class="rt-popover-row-inputs">',
' <input type="color" class="rt-color-swatch">',
' <input type="text" class="rt-color-hex" maxlength="7">',
" </div>",
" </div>",
' <div class="rt-popover-row" data-prop="border">',
' <span class="rt-popover-row-label">Border</span>',
' <div class="rt-popover-row-inputs">',
' <input type="color" class="rt-color-swatch">',
' <input type="text" class="rt-color-hex" maxlength="7">',
" </div>",
" </div>",
' <div class="rt-popover-row" data-prop="text">',
' <span class="rt-popover-row-label">Text</span>',
' <div class="rt-popover-row-inputs">',
' <input type="color" class="rt-color-swatch">',
' <input type="text" class="rt-color-hex" maxlength="7">',
" </div>",
" </div>",
"</div>",
'<div class="rt-popover-preview">',
' <span class="rt-badge rt-badge--subreddit rt-popover-preview-badge" style="padding-right:6px"></span>',
"</div>",
'<div class="rt-popover-footer">',
' <button class="rt-btn-reset" type="button">Reset Default</button>',
' <div class="rt-popover-footer-right">',
' <button class="rt-btn-cancel" type="button">Cancel</button>',
' <button class="rt-btn-save" type="button">Save</button>',
" </div>",
"</div>",
].join("\n");
document.body.appendChild(el);
popoverEl = el;
els.title = el.querySelector(".rt-popover-title");
els.closeBtn = el.querySelector(".rt-popover-close");
els.modeSelect = el.querySelector(".rt-popover-mode-select");
els.preview = el.querySelector(".rt-popover-preview-badge");
els.saveBtn = el.querySelector(".rt-btn-save");
els.cancelBtn = el.querySelector(".rt-btn-cancel");
els.resetBtn = el.querySelector(".rt-btn-reset");
els.rows = {};
var rows = el.querySelectorAll(".rt-popover-row");
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var prop = row.getAttribute("data-prop");
els.rows[prop] = {
swatch: row.querySelector(".rt-color-swatch"),
hex: row.querySelector(".rt-color-hex"),
};
}
els.closeBtn.addEventListener("click", close);
els.cancelBtn.addEventListener("click", close);
els.saveBtn.addEventListener("click", save);
els.resetBtn.addEventListener("click", resetToDefault);
els.modeSelect.addEventListener("change", function () {
storeCurrentEdits();
selectedMode = els.modeSelect.value;
loadModeIntoInputs();
updatePreview();
});
var props = ["bg", "border", "text"];
for (var p = 0; p < props.length; p++) {
(function (prop) {
var row = els.rows[prop];
row.swatch.addEventListener("input", function () {
row.hex.value = row.swatch.value;
row.hex.classList.remove("rt-input-error");
updatePreview();
});
row.hex.addEventListener("input", function () {
if (hexValid(row.hex.value)) {
row.swatch.value = row.hex.value;
row.hex.classList.remove("rt-input-error");
updatePreview();
} else {
row.hex.classList.add("rt-input-error");
}
});
})(props[p]);
}
document.addEventListener("mousedown", onDocumentMouseDown, true);
document.addEventListener("keydown", onDocumentKeyDown, true);
}
function onDocumentMouseDown(e) {
if (!popoverEl || !popoverEl.classList.contains("rt-popover-visible")) return;
if (popoverEl.contains(e.target)) return;
var active = document.activeElement;
if (active && active.type === "color" && popoverEl.contains(active)) return;
close();
}
function onDocumentKeyDown(e) {
if (e.key === "Escape" && popoverEl && popoverEl.classList.contains("rt-popover-visible")) {
close();
}
}
function getDefaultsForMode(settings, mode) {
return {
bg: settings[mode + ".subredditBgColor"],
border: settings[mode + ".subredditBorderColor"],
text: settings[mode + ".subredditTextColor"],
};
}
function getColorsForMode(settings, subredditName, mode) {
var entry = settings.subredditColors[subredditName];
if (entry && entry[mode]) {
return { bg: entry[mode].bg, border: entry[mode].border, text: entry[mode].text };
}
return getDefaultsForMode(settings, mode);
}
function storeCurrentEdits() {
editState[selectedMode] = {
bg: els.rows.bg.swatch.value,
border: els.rows.border.swatch.value,
text: els.rows.text.swatch.value,
};
}
function loadModeIntoInputs() {
var colors = editState[selectedMode];
var props = ["bg", "border", "text"];
for (var i = 0; i < props.length; i++) {
var prop = props[i];
var val = colorToHex(colors[prop]);
els.rows[prop].swatch.value = val;
els.rows[prop].hex.value = val;
els.rows[prop].hex.classList.remove("rt-input-error");
}
}
function updatePreview() {
var bg = els.rows.bg.swatch.value;
var border = els.rows.border.swatch.value;
var text = els.rows.text.swatch.value;
els.preview.style.background = bg;
els.preview.style.borderColor = border;
els.preview.style.color = text;
}
function positionPopover(anchorEl) {
var rect = anchorEl.getBoundingClientRect();
var gap = 6;
popoverEl.style.visibility = "hidden";
popoverEl.classList.add("rt-popover-visible");
var popRect = popoverEl.getBoundingClientRect();
var top = rect.bottom + window.scrollY + gap;
var left = rect.left + window.scrollX;
if (rect.bottom + gap + popRect.height > window.innerHeight) {
top = rect.top + window.scrollY - popRect.height - gap;
}
var maxLeft = document.documentElement.scrollWidth - popRect.width - 8;
if (left > maxLeft) left = maxLeft;
if (left < 8) left = 8;
popoverEl.style.top = top + "px";
popoverEl.style.left = left + "px";
popoverEl.style.visibility = "";
}
function applyDarkModeToPopover(settings) {
var helpers = getHelpers();
if (helpers.isDarkMode(settings)) {
popoverEl.classList.add("rt-dark");
} else {
popoverEl.classList.remove("rt-dark");
}
}
function open(badgeEl, subredditName, settings) {
if (!popoverEl) createPopover();
if (currentSubreddit === subredditName && popoverEl.classList.contains("rt-popover-visible")) {
close();
return;
}
currentSubreddit = subredditName;
currentAnchor = badgeEl;
var helpers = getHelpers();
selectedMode = helpers.getColorPrefix(settings);
editState.light = getColorsForMode(settings, subredditName, "light");
editState.dark = getColorsForMode(settings, subredditName, "dark");
els.title.textContent = "r/" + subredditName;
els.modeSelect.value = selectedMode;
els.preview.textContent = "r/" + subredditName;
applyDarkModeToPopover(settings);
loadModeIntoInputs();
updatePreview();
positionPopover(badgeEl);
}
function close() {
if (!popoverEl) return;
popoverEl.classList.remove("rt-popover-visible");
currentSubreddit = null;
currentAnchor = null;
editState = { light: null, dark: null };
}
async function save() {
if (!currentSubreddit) return;
storeCurrentEdits();
var settings = await window.RedditTweaks.loadSettings();
var entry = settings.subredditColors[currentSubreddit] || {};
if (editState.light) entry.light = editState.light;
if (editState.dark) entry.dark = editState.dark;
settings.subredditColors[currentSubreddit] = entry;
await window.RedditTweaks.saveSettings(settings);
close();
}
async function resetToDefault() {
if (!currentSubreddit) return;
var settings = await window.RedditTweaks.loadSettings();
delete settings.subredditColors[currentSubreddit];
await window.RedditTweaks.saveSettings(settings);
editState.light = getDefaultsForMode(settings, "light");
editState.dark = getDefaultsForMode(settings, "dark");
loadModeIntoInputs();
updatePreview();
}
window.RedditTweaks = window.RedditTweaks || {};
window.RedditTweaks.colorPopover = {
open: open,
close: close,
};
})();
- Step 2: Update
manifest.json— addcolor-popover.jsto content scripts
Replace the js array in content_scripts (lines 20-24):
"js": [
"lib/settings.js",
"content/tweaks/badges.js",
"content/tweaks/color-popover.js",
"content/main.js"
],
color-popover.js loads after badges.js (so badgeTweak namespace exists) and before main.js (so the popover namespace is available when badges are processed).
- Step 3: Verify JS is valid
Run: node --check content/tweaks/color-popover.js
Expected: no output (exit 0)
- Step 4: Verify manifest JSON is valid
Run: python3 -c "import json; json.load(open('manifest.json')); print('valid')"
Expected: valid
- Step 5: Commit
git add content/tweaks/color-popover.js manifest.json
git commit -m "feat: inline color popover with mode toggle, live preview, and save/reset"
Task 4: Popup UI Updates for Mode-Aware Overrides
Files:
- Modify:
popup/popup.html:60-65 - Modify:
popup/popup.js:36-105 - Modify:
popup/popup.css(add new styles)
Interfaces:
-
Consumes:
window.RedditTweaks.loadSettings()→ settings with v2subredditColorsschema (from Task 1)window.RedditTweaks.saveSettings(settings)→Promise<void>
-
Produces: updated popup UI (no API consumed by other tasks)
-
Step 1: Update
popup/popup.html— per-subreddit section
Replace lines 60-65 (the per-subreddit overrides section):
<div class="section">
<h2>Per-Subreddit Overrides</h2>
<p class="section-hint">Set custom subreddit badge colors per mode.</p>
<div id="subredditList"></div>
<button id="addSubreddit" class="btn-secondary">+ Add Subreddit</button>
</div>
- Step 2: Add styles for the updated subreddit entries in
popup/popup.css
Append to the end of popup/popup.css:
.section-hint {
font-size: 11px;
color: #888;
margin-bottom: 8px;
}
.subreddit-entry-header {
display: flex;
gap: 4px;
align-items: center;
margin-bottom: 4px;
}
.subreddit-entry-header input[type="text"] {
flex: 1;
min-width: 0;
padding: 2px 6px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 12px;
font-family: monospace;
}
.subreddit-entry-mode {
display: flex;
gap: 4px;
align-items: center;
margin-bottom: 4px;
}
.subreddit-entry-mode select {
width: auto;
padding: 2px 6px;
font-size: 11px;
margin-top: 0;
}
.subreddit-entry-colors {
display: flex;
gap: 4px;
align-items: center;
}
- Step 3: Replace per-subreddit override functions in
popup/popup.js
Replace renderSubredditOverrides (lines 36-43), addSubredditEntry (lines 45-88), and collectSubredditOverrides (lines 90-105) with:
function renderSubredditOverrides(overrides) {
var list = document.getElementById("subredditList");
list.innerHTML = "";
Object.entries(overrides).forEach(function (pair) {
addSubredditEntry(pair[0], pair[1]);
});
}
function addSubredditEntry(name, modeColors) {
var list = document.getElementById("subredditList");
var entry = document.createElement("div");
entry.className = "subreddit-entry";
var header = document.createElement("div");
header.className = "subreddit-entry-header";
var nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.placeholder = "subreddit";
nameInput.value = name || "";
var removeBtn = document.createElement("button");
removeBtn.className = "btn-remove";
removeBtn.textContent = "\u00d7";
removeBtn.addEventListener("click", function () {
entry.remove();
saveFromControls();
});
header.appendChild(nameInput);
header.appendChild(removeBtn);
var modeRow = document.createElement("div");
modeRow.className = "subreddit-entry-mode";
var modeSelect = document.createElement("select");
modeSelect.innerHTML = '<option value="light">Light</option><option value="dark">Dark</option>';
modeSelect.value = "light";
modeRow.appendChild(modeSelect);
var colorsDiv = document.createElement("div");
colorsDiv.className = "subreddit-entry-colors";
var lightColors = modeColors && modeColors.light ? modeColors.light : {};
var darkColors = modeColors && modeColors.dark ? modeColors.dark : {};
var bgInput = document.createElement("input");
bgInput.type = "color";
bgInput.value = colorToHex(lightColors.bg);
bgInput.title = "Background";
var borderInput = document.createElement("input");
borderInput.type = "color";
borderInput.value = colorToHex(lightColors.border);
borderInput.title = "Border";
var textInput = document.createElement("input");
textInput.type = "color";
textInput.value = colorToHex(lightColors.text);
textInput.title = "Text";
colorsDiv.appendChild(bgInput);
colorsDiv.appendChild(borderInput);
colorsDiv.appendChild(textInput);
entry._colorState = {
light: { bg: bgInput.value, border: borderInput.value, text: textInput.value },
dark: { bg: colorToHex(darkColors.bg), border: colorToHex(darkColors.border), text: colorToHex(darkColors.text) },
};
modeSelect.addEventListener("change", function () {
var prev = modeSelect.value === "dark" ? "light" : "dark";
entry._colorState[prev] = { bg: bgInput.value, border: borderInput.value, text: textInput.value };
var next = entry._colorState[modeSelect.value];
bgInput.value = next.bg;
borderInput.value = next.border;
textInput.value = next.text;
});
[nameInput, bgInput, borderInput, textInput].forEach(function (input) {
input.addEventListener("change", saveFromControls);
});
entry.appendChild(header);
entry.appendChild(modeRow);
entry.appendChild(colorsDiv);
entry._nameInput = nameInput;
entry._bgInput = bgInput;
entry._borderInput = borderInput;
entry._textInput = textInput;
entry._modeSelect = modeSelect;
list.appendChild(entry);
}
function collectSubredditOverrides() {
var overrides = {};
var entries = document.querySelectorAll(".subreddit-entry");
entries.forEach(function (entry) {
var name = entry._nameInput.value.trim().replace(/^r\//, "");
if (!name) return;
var currentMode = entry._modeSelect.value;
entry._colorState[currentMode] = {
bg: entry._bgInput.value,
border: entry._borderInput.value,
text: entry._textInput.value,
};
overrides[name] = {
light: entry._colorState.light,
dark: entry._colorState.dark,
};
});
return overrides;
}
- Step 4: Verify JS is valid
Run: node --check popup/popup.js
Expected: no output (exit 0)
- Step 5: Commit
git add popup/popup.html popup/popup.js popup/popup.css
git commit -m "feat: popup per-subreddit overrides with mode-aware light/dark toggle"
Post-Implementation
After all tasks are complete, load the extension as a temporary add-on in Firefox via about:debugging → "This Firefox" → "Load Temporary Add-on" → select manifest.json. Then run through the test checklist from the spec (items 1–20 in the Testing section of docs/superpowers/specs/2026-08-26-inline-color-popover-design.md).