From 6bf79351a7bbd4a83f1d57e24387fcebea3611a4 Mon Sep 17 00:00:00 2001 From: cottongin Date: Wed, 26 Aug 2026 15:58:43 -0400 Subject: [PATCH] Add implementation plan for inline subreddit color popover Co-authored-by: Cursor --- .../plans/2026-08-26-inline-color-popover.md | 1107 +++++++++++++++++ 1 file changed, 1107 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-26-inline-color-popover.md diff --git a/docs/superpowers/plans/2026-08-26-inline-color-popover.md b/docs/superpowers/plans/2026-08-26-inline-color-popover.md new file mode 100644 index 0000000..f8ea580 --- /dev/null +++ b/docs/superpowers/plans/2026-08-26-inline-color-popover.md @@ -0,0 +1,1107 @@ +# 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`).