Files
reddit-tweaks-ffe/docs/superpowers/plans/2026-08-26-reddit-badge-tweaks.md
cottongin e37d76be09 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 <cursoragent@cursor.com>
2026-08-26 14:55:52 -04:00

29 KiB
Raw Permalink Blame History

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 <body>, 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<object> with merged defaults + stored values
    • window.RedditTweaks.saveSettings(settings) — returns Promise<void>, writes to browser.storage.local
    • window.RedditTweaks.resetSettings() — returns Promise<void>, clears storage
  • Step 1: Create manifest.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
(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:

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:

python3 -c "import json; json.load(open('manifest.json')); print('valid')"

Expected: valid

  • Step 5: Commit
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

/* --- 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
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
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<object> (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

(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
(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
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
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<object> (from lib/settings.js, Task 1)
    • window.RedditTweaks.saveSettings(settings) → Promise<void> (from lib/settings.js, Task 1)
    • window.RedditTweaks.resetSettings() → Promise<void> (from lib/settings.js, Task 1)
  • Produces: complete settings popup (no API consumed by other tasks)

  • Step 1: Create popup/popup.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="popup-container">
    <h1>Reddit Tweaks</h1>

    <div class="section">
      <label class="toggle-row">
        <span>Enabled</span>
        <input type="checkbox" id="enabled">
      </label>
    </div>

    <div class="section">
      <label for="darkMode">Dark Mode</label>
      <select id="darkMode">
        <option value="auto">Auto (detect RES)</option>
        <option value="light">Force Light</option>
        <option value="dark">Force Dark</option>
      </select>
    </div>

    <div class="section">
      <h2>Light Mode Colors</h2>
      <fieldset>
        <legend>Subreddit Badge</legend>
        <label>Background <input type="color" id="light.subredditBgColor"></label>
        <label>Border <input type="color" id="light.subredditBorderColor"></label>
        <label>Text <input type="color" id="light.subredditTextColor"></label>
      </fieldset>
      <fieldset>
        <legend>Comments Badge</legend>
        <label>Background <input type="color" id="light.commentsBgColor"></label>
        <label>Border <input type="color" id="light.commentsBorderColor"></label>
        <label>Text <input type="color" id="light.commentsTextColor"></label>
      </fieldset>
      <button id="resetLight" class="btn-secondary">Reset Light Defaults</button>
    </div>

    <div class="section">
      <h2>Dark Mode Colors</h2>
      <fieldset>
        <legend>Subreddit Badge</legend>
        <label>Background <input type="color" id="dark.subredditBgColor"></label>
        <label>Border <input type="color" id="dark.subredditBorderColor"></label>
        <label>Text <input type="color" id="dark.subredditTextColor"></label>
      </fieldset>
      <fieldset>
        <legend>Comments Badge</legend>
        <label>Background <input type="color" id="dark.commentsBgColor"></label>
        <label>Border <input type="color" id="dark.commentsBorderColor"></label>
        <label>Text <input type="color" id="dark.commentsTextColor"></label>
      </fieldset>
      <button id="resetDark" class="btn-secondary">Reset Dark Defaults</button>
    </div>

    <div class="section">
      <h2>Per-Subreddit Overrides</h2>
      <div id="subredditList"></div>
      <button id="addSubreddit" class="btn-secondary">+ Add Subreddit</button>
    </div>

    <div class="section">
      <button id="resetAll" class="btn-danger">Reset All Settings</button>
    </div>
  </div>
  <script src="../lib/settings.js"></script>
  <script src="popup.js"></script>
</body>
</html>

Note: transparent is not a valid <input type="color"> 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
* {
  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
(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
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
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).