From e37d76be095aa9d938c776feaf78cad2fd3804ce Mon Sep 17 00:00:00 2001 From: cottongin Date: Wed, 26 Aug 2026 14:55:52 -0400 Subject: [PATCH] Add implementation plan for Reddit badge tweaks extension Four tasks: extension skeleton, badge CSS, DOM manipulation, settings popup. Each task has exact file paths, complete code, and verification steps. Co-authored-by: Cursor --- .../plans/2026-08-26-reddit-badge-tweaks.md | 987 ++++++++++++++++++ 1 file changed, 987 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-26-reddit-badge-tweaks.md diff --git a/docs/superpowers/plans/2026-08-26-reddit-badge-tweaks.md b/docs/superpowers/plans/2026-08-26-reddit-badge-tweaks.md new file mode 100644 index 0000000..806e048 --- /dev/null +++ b/docs/superpowers/plans/2026-08-26-reddit-badge-tweaks.md @@ -0,0 +1,987 @@ +# Firefox Reddit Tweaks 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:** Build a Firefox extension that relocates subreddit name and comment count on old Reddit into styled badge elements above each post title. + +**Architecture:** Pure CSS + vanilla JS content script (Manifest V2). No frameworks, no build step. Content scripts share global scope via the manifest's `content_scripts.js` array. CSS is injected at parse time to prevent FOUC. Settings stored in `browser.storage.local`. + +**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) +- Permissions: `storage`, `activeTab` +- Post selector: `div.thing.link` (never bare `div.thing`) +- Zero-comment edge case: when original text is `"comment"`, badge displays `"0 comments"` +- Master toggle change triggers `location.reload()`, not teardown +- Color settings changes apply live via `storage.onChanged` without reload +- Dark mode detection: `.res-nightmode` on ``, observed reactively via MutationObserver + +--- + +## File Structure + +| File | Responsibility | +|---|---| +| `manifest.json` | Extension manifest — permissions, content scripts (CSS + JS), browser action | +| `lib/settings.js` | Defaults, load/save helpers for `browser.storage.local`. Shared by content scripts and popup | +| `content/styles/badges.css` | Badge appearance (light + dark), CSS custom properties with fallbacks, hiding rules for original elements (FOUC prevention) | +| `content/tweaks/badges.js` | Badge tweak module — DOM manipulation, MutationObservers (content + dark mode), storage change listener | +| `content/main.js` | Entry point — loads settings, checks `enabled`, calls badge tweak | +| `popup/popup.html` | Settings popup HTML structure | +| `popup/popup.css` | Settings popup styling | +| `popup/popup.js` | Settings popup logic — binds controls to storage, handles per-subreddit overrides | +| `icons/icon-48.png` | Toolbar icon (48×48) | +| `icons/icon-96.png` | Toolbar icon (96×96) | + +--- + +### Task 1: Extension Skeleton (Manifest + Settings + Icons) + +**Files:** +- Create: `manifest.json` +- Create: `lib/settings.js` +- Create: `icons/icon-48.png` +- Create: `icons/icon-96.png` + +**Interfaces:** +- Consumes: nothing (first task) +- Produces: + - `window.RedditTweaks.DEFAULTS` — object with all default setting values (see settings table in spec) + - `window.RedditTweaks.loadSettings()` — returns `Promise` with merged defaults + stored values + - `window.RedditTweaks.saveSettings(settings)` — returns `Promise`, writes to `browser.storage.local` + - `window.RedditTweaks.resetSettings()` — returns `Promise`, clears storage + +- [ ] **Step 1: Create `manifest.json`** + +```json +{ + "manifest_version": 2, + "name": "Reddit Tweaks", + "version": "1.0.0", + "description": "Reskins old Reddit post listings with badge-style subreddit and comment indicators.", + "permissions": ["storage", "activeTab"], + "icons": { + "48": "icons/icon-48.png", + "96": "icons/icon-96.png" + }, + "browser_action": { + "default_icon": "icons/icon-48.png", + "default_popup": "popup/popup.html", + "default_title": "Reddit Tweaks Settings" + }, + "content_scripts": [ + { + "matches": ["*://old.reddit.com/*", "*://www.reddit.com/*"], + "css": ["content/styles/badges.css"], + "js": [ + "lib/settings.js", + "content/tweaks/badges.js", + "content/main.js" + ], + "run_at": "document_idle" + } + ] +} +``` + +- [ ] **Step 2: Create `lib/settings.js`** + +```js +(function () { + "use strict"; + + const DEFAULTS = { + enabled: true, + darkMode: "auto", + "light.subredditBgColor": "transparent", + "light.subredditBorderColor": "#c84a20", + "light.subredditTextColor": "#c84a20", + "light.commentsBgColor": "#e0e0e0", + "light.commentsBorderColor": "#4a4a4a", + "light.commentsTextColor": "#4a4a4a", + "dark.subredditBgColor": "transparent", + "dark.subredditBorderColor": "#ff6b3d", + "dark.subredditTextColor": "#ff6b3d", + "dark.commentsBgColor": "#3a3a3a", + "dark.commentsBorderColor": "#888888", + "dark.commentsTextColor": "#cccccc", + subredditColors: {}, + }; + + async function loadSettings() { + const stored = await browser.storage.local.get(null); + return Object.assign({}, DEFAULTS, stored); + } + + async function saveSettings(settings) { + const toStore = {}; + for (const key of Object.keys(settings)) { + if (key in DEFAULTS) { + toStore[key] = settings[key]; + } + } + await browser.storage.local.set(toStore); + } + + async function resetSettings() { + await browser.storage.local.clear(); + } + + window.RedditTweaks = window.RedditTweaks || {}; + window.RedditTweaks.DEFAULTS = DEFAULTS; + window.RedditTweaks.loadSettings = loadSettings; + window.RedditTweaks.saveSettings = saveSettings; + window.RedditTweaks.resetSettings = resetSettings; +})(); +``` + +- [ ] **Step 3: Create placeholder icons** + +Generate simple 48×48 and 96×96 PNG icons. These can be single-color placeholder squares (e.g., orange `#c84a20` background with a white "RT" text or just a solid color). Use any available method — canvas script, ImageMagick, or a minimal valid PNG. + +Place at `icons/icon-48.png` and `icons/icon-96.png`. + +- [ ] **Step 4: Verify extension loads in Firefox** + +The extension needs the content script files to exist (even if empty) or Firefox will error. Create empty placeholder files: + +```bash +mkdir -p content/styles content/tweaks +touch content/styles/badges.css content/tweaks/badges.js content/main.js +``` + +Then verify the manifest is valid JSON: + +```bash +python3 -c "import json; json.load(open('manifest.json')); print('valid')" +``` + +Expected: `valid` + +- [ ] **Step 5: Commit** + +```bash +git add manifest.json lib/settings.js icons/ content/styles/badges.css content/tweaks/badges.js content/main.js +git commit -m "feat: extension skeleton with manifest, settings module, and placeholder icons" +``` + +--- + +### Task 2: Badge Styling (CSS) + +**Files:** +- Create: `content/styles/badges.css` (replace the empty placeholder) + +**Interfaces:** +- Consumes: nothing (pure CSS, no JS dependencies) +- Produces: + - CSS rules for `.reddit-tweaks-badges`, `.rt-badge`, `.rt-badge--subreddit`, `.rt-badge--comments` + - CSS rules hiding original elements: `.thing.link .tagline .subreddit` and `.thing.link .flat-list .comments` set to `display: none` + - Dark mode variant via `.rt-dark` class on `.reddit-tweaks-badges` + - CSS custom properties: `--rt-sub-bg`, `--rt-sub-border`, `--rt-sub-text`, `--rt-com-bg`, `--rt-com-border`, `--rt-com-text` + +- [ ] **Step 1: Write `content/styles/badges.css`** + +```css +/* --- FOUC prevention: hide original elements immediately --- */ +.thing.link .tagline .subreddit, +.thing.link .flat-list .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 { + opacity: 0.8; +} + +/* --- Light mode (default) --- */ +.rt-badge--subreddit { + background: var(--rt-sub-bg, transparent); + 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, transparent); + 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); +} +``` + +- [ ] **Step 2: Verify CSS is valid** + +```bash +python3 -c " +css = open('content/styles/badges.css').read() +print(f'CSS file: {len(css)} chars') +assert '.thing.link' in css, 'missing post selector' +assert '.rt-badge' in css, 'missing badge class' +assert '.rt-dark' in css, 'missing dark mode' +assert '--rt-sub-bg' in css, 'missing CSS custom properties' +assert 'display: none' in css, 'missing FOUC hide rules' +print('all checks passed') +" +``` + +Expected: `all checks passed` + +- [ ] **Step 3: Commit** + +```bash +git add content/styles/badges.css +git commit -m "feat: badge CSS with light/dark mode, custom properties, and FOUC prevention" +``` + +--- + +### Task 3: Badge DOM Manipulation + +**Files:** +- Create: `content/tweaks/badges.js` (replace the empty placeholder) +- Create: `content/main.js` (replace the empty placeholder) + +**Interfaces:** +- Consumes: + - `window.RedditTweaks.loadSettings()` → `Promise` (from `lib/settings.js`, Task 1) + - `window.RedditTweaks.DEFAULTS` → object (from `lib/settings.js`, Task 1) + - CSS classes from Task 2: `.reddit-tweaks-badges`, `.rt-badge`, `.rt-badge--subreddit`, `.rt-badge--comments`, `.rt-dark` + - CSS custom properties from Task 2: `--rt-sub-bg`, `--rt-sub-border`, `--rt-sub-text`, `--rt-com-bg`, `--rt-com-border`, `--rt-com-text` +- Produces: + - `window.RedditTweaks.badgeTweak.apply(settings)` — processes all unprocessed `div.thing.link` posts, starts MutationObservers + - `window.RedditTweaks.badgeTweak.updateColors(settings)` — re-applies CSS custom properties to all badge containers + +- [ ] **Step 1: Write `content/tweaks/badges.js`** + +```js +(function () { + "use strict"; + + let contentObserver = null; + let darkModeObserver = null; + let currentSettings = null; + + function isDarkMode(settings) { + if (settings.darkMode === "light") return false; + if (settings.darkMode === "dark") return true; + return document.body.classList.contains("res-nightmode"); + } + + function getColorPrefix(settings) { + return isDarkMode(settings) ? "dark" : "light"; + } + + function applyColorsToContainer(container, settings, subredditName) { + const prefix = getColorPrefix(settings); + + if (isDarkMode(settings)) { + container.classList.add("rt-dark"); + } else { + container.classList.remove("rt-dark"); + } + + const overrides = subredditName && settings.subredditColors[subredditName]; + + 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"]); + } + + function normalizeCommentText(text) { + const trimmed = text.trim(); + if (trimmed === "comment") return "0 comments"; + return trimmed; + } + + function processPost(post, settings) { + if (post.hasAttribute("data-rt-processed")) return; + + const entry = post.querySelector(".entry"); + if (!entry) return; + + const container = document.createElement("div"); + container.className = "reddit-tweaks-badges"; + + let subredditName = null; + + const subLink = post.querySelector(".tagline .subreddit"); + if (subLink) { + const badge = document.createElement("a"); + badge.className = "rt-badge rt-badge--subreddit"; + badge.href = subLink.href; + badge.textContent = subLink.textContent.trim(); + subredditName = subLink.textContent.trim().replace(/^r\//, ""); + container.appendChild(badge); + } + + const commentsLink = post.querySelector(".flat-list .comments"); + if (commentsLink) { + const badge = document.createElement("a"); + badge.className = "rt-badge rt-badge--comments"; + badge.href = commentsLink.href; + badge.textContent = normalizeCommentText(commentsLink.textContent); + container.appendChild(badge); + } + + if (container.children.length === 0) return; + + applyColorsToContainer(container, settings, subredditName); + entry.insertBefore(container, entry.firstChild); + post.setAttribute("data-rt-processed", "true"); + } + + function processPosts(settings) { + const posts = document.querySelectorAll("div.thing.link:not([data-rt-processed])"); + posts.forEach(function (post) { + processPost(post, settings); + }); + } + + function getObserverTarget() { + return ( + document.getElementById("siteTable") || + document.querySelector('.content[role="main"]') || + document.body + ); + } + + function startContentObserver(settings) { + if (contentObserver) contentObserver.disconnect(); + + const target = getObserverTarget(); + contentObserver = new MutationObserver(function (mutations) { + let hasNewPosts = false; + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.matches && node.matches("div.thing.link")) { + hasNewPosts = true; + } else if (node.querySelector && node.querySelector("div.thing.link")) { + hasNewPosts = true; + } + } + } + } + if (hasNewPosts) processPosts(currentSettings); + }); + + contentObserver.observe(target, { childList: true, subtree: true }); + } + + function startDarkModeObserver() { + if (darkModeObserver) darkModeObserver.disconnect(); + + darkModeObserver = new MutationObserver(function () { + if (currentSettings && currentSettings.darkMode === "auto") { + updateColors(currentSettings); + } + }); + + darkModeObserver.observe(document.body, { + attributes: true, + attributeFilter: ["class"], + }); + } + + function updateColors(settings) { + currentSettings = settings; + const containers = document.querySelectorAll(".reddit-tweaks-badges"); + containers.forEach(function (container) { + const subBadge = container.querySelector(".rt-badge--subreddit"); + const subredditName = subBadge + ? subBadge.textContent.trim().replace(/^r\//, "") + : null; + applyColorsToContainer(container, settings, subredditName); + }); + } + + function apply(settings) { + currentSettings = settings; + processPosts(settings); + startContentObserver(settings); + startDarkModeObserver(); + } + + window.RedditTweaks = window.RedditTweaks || {}; + window.RedditTweaks.badgeTweak = { + apply: apply, + updateColors: updateColors, + }; +})(); +``` + +- [ ] **Step 2: Write `content/main.js`** + +```js +(function () { + "use strict"; + + async function init() { + const settings = await window.RedditTweaks.loadSettings(); + if (!settings.enabled) return; + + window.RedditTweaks.badgeTweak.apply(settings); + + browser.storage.onChanged.addListener(function (changes) { + if (changes.enabled) { + location.reload(); + return; + } + + window.RedditTweaks.loadSettings().then(function (updated) { + window.RedditTweaks.badgeTweak.updateColors(updated); + }); + }); + } + + init(); +})(); +``` + +- [ ] **Step 3: Verify JS is valid** + +```bash +node --check lib/settings.js && echo "settings.js: valid" +node --check content/tweaks/badges.js && echo "badges.js: valid" +node --check content/main.js && echo "main.js: valid" +``` + +Expected: all three report `valid` + +- [ ] **Step 4: Commit** + +```bash +git add content/tweaks/badges.js content/main.js +git commit -m "feat: badge DOM manipulation with MutationObservers and dark mode detection" +``` + +--- + +### Task 4: Settings Popup UI + +**Files:** +- Create: `popup/popup.html` +- Create: `popup/popup.css` +- Create: `popup/popup.js` + +**Interfaces:** +- Consumes: + - `window.RedditTweaks.DEFAULTS` → object (from `lib/settings.js`, Task 1) + - `window.RedditTweaks.loadSettings()` → `Promise` (from `lib/settings.js`, Task 1) + - `window.RedditTweaks.saveSettings(settings)` → `Promise` (from `lib/settings.js`, Task 1) + - `window.RedditTweaks.resetSettings()` → `Promise` (from `lib/settings.js`, Task 1) +- Produces: complete settings popup (no API consumed by other tasks) + +- [ ] **Step 1: Create `popup/popup.html`** + +```html + + + + + + + + + + + + +``` + +Note: `transparent` is not a valid `` value. The popup.js code must handle this: when loading settings, map `"transparent"` to `#ffffff` (or a sensible stand-in) for the color picker; when saving, if the default for that field is `"transparent"`, provide a "Use transparent" checkbox next to the background color pickers. Alternatively, use a text input instead of a color picker for background fields. Choose the approach that keeps the UI simplest — a text input with a color preview swatch is recommended for fields that may be `"transparent"`. + +- [ ] **Step 2: Create `popup/popup.css`** + +```css +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + width: 320px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 13px; + color: #1a1a1a; + background: #ffffff; +} + +.popup-container { + padding: 16px; +} + +h1 { + font-size: 16px; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #e0e0e0; +} + +h2 { + font-size: 13px; + font-weight: 600; + margin-bottom: 8px; +} + +.section { + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid #f0f0f0; +} + +.section:last-child { + border-bottom: none; + margin-bottom: 0; +} + +.toggle-row { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; +} + +label { + display: block; + margin-bottom: 4px; +} + +select { + width: 100%; + padding: 4px 8px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 13px; + margin-top: 4px; +} + +fieldset { + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 8px 12px; + margin-bottom: 8px; +} + +legend { + font-weight: 600; + font-size: 12px; + padding: 0 4px; +} + +fieldset label { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; + font-size: 12px; +} + +fieldset input[type="color"] { + width: 40px; + height: 24px; + border: 1px solid #ccc; + border-radius: 3px; + padding: 1px; + cursor: pointer; +} + +fieldset input[type="text"] { + width: 80px; + padding: 2px 6px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + font-family: monospace; +} + +.btn-secondary { + background: #f0f0f0; + border: 1px solid #ccc; + border-radius: 3px; + padding: 4px 10px; + font-size: 12px; + cursor: pointer; + margin-top: 4px; +} + +.btn-secondary:hover { + background: #e0e0e0; +} + +.btn-danger { + background: #d9534f; + color: white; + border: 1px solid #c9302c; + border-radius: 3px; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + width: 100%; +} + +.btn-danger:hover { + background: #c9302c; +} + +.subreddit-entry { + display: flex; + gap: 4px; + align-items: center; + margin-bottom: 6px; + padding: 6px; + background: #fafafa; + border: 1px solid #e0e0e0; + border-radius: 4px; +} + +.subreddit-entry input[type="text"] { + flex: 1; + min-width: 0; +} + +.subreddit-entry .btn-remove { + background: none; + border: none; + color: #999; + cursor: pointer; + font-size: 16px; + padding: 0 4px; +} + +.subreddit-entry .btn-remove:hover { + color: #d9534f; +} +``` + +- [ ] **Step 3: Create `popup/popup.js`** + +```js +(function () { + "use strict"; + + const COLOR_KEYS = [ + "light.subredditBorderColor", + "light.subredditTextColor", + "light.commentsBgColor", + "light.commentsBorderColor", + "light.commentsTextColor", + "dark.subredditBorderColor", + "dark.subredditTextColor", + "dark.commentsBgColor", + "dark.commentsBorderColor", + "dark.commentsTextColor", + ]; + + const BG_KEYS = [ + "light.subredditBgColor", + "dark.subredditBgColor", + ]; + + function colorToHex(val) { + if (!val || val === "transparent") return "#ffffff"; + return val; + } + + function populateControls(settings) { + document.getElementById("enabled").checked = settings.enabled; + document.getElementById("darkMode").value = settings.darkMode; + + COLOR_KEYS.forEach(function (key) { + const el = document.getElementById(key); + if (el) el.value = colorToHex(settings[key]); + }); + + BG_KEYS.forEach(function (key) { + const el = document.getElementById(key); + if (el) { + el.value = settings[key] === "transparent" ? "" : settings[key]; + el.placeholder = "transparent"; + } + }); + + renderSubredditOverrides(settings.subredditColors || {}); + } + + function renderSubredditOverrides(overrides) { + const list = document.getElementById("subredditList"); + list.innerHTML = ""; + + Object.entries(overrides).forEach(function ([name, colors]) { + addSubredditEntry(name, colors); + }); + } + + function addSubredditEntry(name, colors) { + const list = document.getElementById("subredditList"); + const entry = document.createElement("div"); + entry.className = "subreddit-entry"; + + const nameInput = document.createElement("input"); + nameInput.type = "text"; + nameInput.placeholder = "subreddit"; + nameInput.value = name || ""; + + const bgInput = document.createElement("input"); + bgInput.type = "color"; + bgInput.value = colorToHex(colors?.bg); + bgInput.title = "Background"; + + const borderInput = document.createElement("input"); + borderInput.type = "color"; + borderInput.value = colorToHex(colors?.border); + borderInput.title = "Border"; + + const textInput = document.createElement("input"); + textInput.type = "color"; + textInput.value = colorToHex(colors?.text); + textInput.title = "Text"; + + const removeBtn = document.createElement("button"); + removeBtn.className = "btn-remove"; + removeBtn.textContent = "×"; + removeBtn.addEventListener("click", function () { + entry.remove(); + saveFromControls(); + }); + + [nameInput, bgInput, borderInput, textInput].forEach(function (input) { + input.addEventListener("change", saveFromControls); + }); + + entry.appendChild(nameInput); + entry.appendChild(bgInput); + entry.appendChild(borderInput); + entry.appendChild(textInput); + entry.appendChild(removeBtn); + list.appendChild(entry); + } + + function collectSubredditOverrides() { + const overrides = {}; + const entries = document.querySelectorAll(".subreddit-entry"); + entries.forEach(function (entry) { + const inputs = entry.querySelectorAll("input"); + const name = inputs[0].value.trim().replace(/^r\//, ""); + if (name) { + overrides[name] = { + bg: inputs[1].value, + border: inputs[2].value, + text: inputs[3].value, + }; + } + }); + return overrides; + } + + function saveFromControls() { + const settings = { + enabled: document.getElementById("enabled").checked, + darkMode: document.getElementById("darkMode").value, + subredditColors: collectSubredditOverrides(), + }; + + COLOR_KEYS.forEach(function (key) { + const el = document.getElementById(key); + if (el) settings[key] = el.value; + }); + + BG_KEYS.forEach(function (key) { + const el = document.getElementById(key); + if (el) { + settings[key] = el.value.trim() || "transparent"; + } + }); + + window.RedditTweaks.saveSettings(settings); + } + + async function init() { + const settings = await window.RedditTweaks.loadSettings(); + populateControls(settings); + + document.getElementById("enabled").addEventListener("change", saveFromControls); + document.getElementById("darkMode").addEventListener("change", saveFromControls); + + COLOR_KEYS.forEach(function (key) { + const el = document.getElementById(key); + if (el) el.addEventListener("input", saveFromControls); + }); + + BG_KEYS.forEach(function (key) { + const el = document.getElementById(key); + if (el) el.addEventListener("input", saveFromControls); + }); + + document.getElementById("addSubreddit").addEventListener("click", function () { + addSubredditEntry("", {}); + }); + + document.getElementById("resetLight").addEventListener("click", async function () { + const settings = await window.RedditTweaks.loadSettings(); + const DEFAULTS = window.RedditTweaks.DEFAULTS; + Object.keys(DEFAULTS).forEach(function (key) { + if (key.startsWith("light.")) settings[key] = DEFAULTS[key]; + }); + await window.RedditTweaks.saveSettings(settings); + populateControls(settings); + }); + + document.getElementById("resetDark").addEventListener("click", async function () { + const settings = await window.RedditTweaks.loadSettings(); + const DEFAULTS = window.RedditTweaks.DEFAULTS; + Object.keys(DEFAULTS).forEach(function (key) { + if (key.startsWith("dark.")) settings[key] = DEFAULTS[key]; + }); + await window.RedditTweaks.saveSettings(settings); + populateControls(settings); + }); + + document.getElementById("resetAll").addEventListener("click", async function () { + if (confirm("Reset all settings to defaults?")) { + await window.RedditTweaks.resetSettings(); + const settings = await window.RedditTweaks.loadSettings(); + populateControls(settings); + } + }); + } + + init(); +})(); +``` + +- [ ] **Step 4: Verify HTML references are correct** + +```bash +python3 -c " +import os +html = open('popup/popup.html').read() +assert 'popup.css' in html, 'missing CSS reference' +assert 'popup.js' in html, 'missing JS reference' +assert '../lib/settings.js' in html, 'missing settings.js reference' +print('all references present') + +for f in ['popup/popup.html', 'popup/popup.css', 'popup/popup.js']: + assert os.path.exists(f), f'missing {f}' +print('all popup files exist') +" +``` + +Expected: both checks pass + +- [ ] **Step 5: Commit** + +```bash +git add popup/ +git commit -m "feat: settings popup with color pickers, dark mode, and subreddit overrides" +``` + +--- + +## Post-Implementation + +After all tasks are complete, verify the full extension by loading it 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–15).