# 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 bare `div.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 returns `subredditColors` in v2 format: `{ subredditName: { light: {bg, border, text}, dark: {bg, border, text} } }` - `applyColorsToContainer()` reads from `overrides[prefix]` instead of flat `overrides` - `updateColors()` reads subreddit name from `data-rt-subreddit` attribute on the badge container - `window.RedditTweaks.isDarkMode(settings)` — exported for popover use - `window.RedditTweaks.getColorPrefix(settings)` — exported for popover use - [ ] **Step 1: Update `lib/settings.js` — add migration in `loadSettings()`** Replace the entire `loadSettings` function (lines 22-27) with: ```js 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-aware `applyColorsToContainer()`** Replace lines 27-34 of `applyColorsToContainer()` (the override reading and CSS property setting): ```js 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-subreddit` attribute in `processPost()`** After line 60 (`subredditName = subLink.textContent.trim().replace(/^r\//, "");`), add: ```js container.setAttribute("data-rt-subreddit", subredditName); ``` - [ ] **Step 4: Update `content/tweaks/badges.js` — `updateColors()` reads from data attribute** Replace lines 142-145 of `updateColors()`: ```js const subredditName = container.getAttribute("data-rt-subreddit"); ``` - [ ] **Step 5: Export `isDarkMode` and `getColorPrefix` on the `badgeTweak` namespace** Replace lines 157-161: ```js 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** ```bash 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` (inside `processPost()`) - Modify: `content/styles/badges.css` **Interfaces:** - Consumes: `processPost()` from Task 1 (with `data-rt-subreddit` attribute already added) - Produces: - Each subreddit badge `` contains a `⚙` child - CSS rules for `.rt-badge-gear` visibility/positioning, `.rt-badge--subreddit` relative positioning, badge hover `filter: brightness(0.9)` - [ ] **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: ```js 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.css` with updated version** Replace the entire file content: ```css /* --- 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** ```bash 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` (from `lib/settings.js`) - `window.RedditTweaks.saveSettings(settings)` → `Promise` (from `lib/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 popover - `window.RedditTweaks.colorPopover.close()` — closes the popover without saving - The popover element is injected into `document.body` once - [ ] **Step 1: Create `content/tweaks/color-popover.js`** ```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 = [ '
', ' ', ' ', "
", '
', ' ", "
", '
', '
', ' Background', '
', ' ', ' ', "
", "
", '
', ' Border', '
', ' ', ' ', "
", "
", '
', ' Text', '
', ' ', ' ', "
", "
", "
", '
', ' ', "
", '", ].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` — add `color-popover.js` to content scripts** Replace the `js` array in `content_scripts` (lines 20-24): ```json "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** ```bash 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 v2 `subredditColors` schema (from Task 1) - `window.RedditTweaks.saveSettings(settings)` → `Promise` - 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): ```html

Per-Subreddit Overrides

Set custom subreddit badge colors per mode.

``` - [ ] **Step 2: Add styles for the updated subreddit entries in `popup/popup.css`** Append to the end of `popup/popup.css`: ```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: ```js 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 = ''; 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** ```bash 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`).