Files
reddit-tweaks-ffe/docs/superpowers/plans/2026-08-26-inline-color-popover.md

31 KiB
Raw Blame History

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:

  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):

    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:

      container.setAttribute("data-rt-subreddit", subredditName);
  • Step 4: Update content/tweaks/badges.js — updateColors() reads from data attribute

Replace lines 142-145 of updateColors():

      const subredditName = container.getAttribute("data-rt-subreddit");
  • Step 5: Export isDarkMode and getColorPrefix on the badgeTweak namespace

Replace lines 157-161:

  window.RedditTweaks = window.RedditTweaks || {};
  window.RedditTweaks.badgeTweak = {
    apply: apply,
    updateColors: updateColors,
    isDarkMode: isDarkMode,
    getColorPrefix: getColorPrefix,
  };
  • Step 6: Verify JS is valid

Run: node --check lib/settings.js && node --check content/tweaks/badges.js

Expected: no output (exit 0)

  • Step 7: Commit
git add lib/settings.js content/tweaks/badges.js
git commit -m "feat: mode-aware subreddit color overrides with v1→v2 migration"

Task 2: Gear Icon and Badge CSS Changes

Files:

  • Modify: content/tweaks/badges.js:54-67 (inside processPost())
  • Modify: content/styles/badges.css

Interfaces:

  • Consumes: processPost() from Task 1 (with data-rt-subreddit attribute already added)

  • Produces:

    • Each subreddit badge <a> contains a <span class="rt-badge-gear">⚙</span> 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:

      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:

/* --- FOUC prevention: hide original elements immediately --- */
html.rt-enabled .thing.link .tagline .subreddit,
html.rt-enabled .thing.link .flat-list li:has(> .comments) {
  display: none !important;
}

/* --- Badge container --- */
.reddit-tweaks-badges {
  margin-bottom: 4px;
  line-height: 1.6;
}

/* --- Shared badge styles --- */
.rt-badge {
  display: inline-block;
  border: 1px solid;
  border-radius: 3px;
  padding: 2px 6px;
  font-size: 11px;
  font-weight: bold;
  text-decoration: none;
  vertical-align: middle;
  margin-right: 6px;
  cursor: pointer;
}

.rt-badge:hover {
  filter: brightness(0.9);
}

/* --- Subreddit badge: relative for gear positioning --- */
.rt-badge--subreddit {
  position: relative;
  padding-right: 18px;
  background: var(--rt-sub-bg, #fde8e0);
  border-color: var(--rt-sub-border, #c84a20);
  color: var(--rt-sub-text, #c84a20);
}

.rt-badge--comments {
  background: var(--rt-com-bg, #e0e0e0);
  border-color: var(--rt-com-border, #4a4a4a);
  color: var(--rt-com-text, #4a4a4a);
}

/* --- Dark mode --- */
.rt-dark .rt-badge--subreddit {
  background: var(--rt-sub-bg, #3d2518);
  border-color: var(--rt-sub-border, #ff6b3d);
  color: var(--rt-sub-text, #ff6b3d);
}

.rt-dark .rt-badge--comments {
  background: var(--rt-com-bg, #3a3a3a);
  border-color: var(--rt-com-border, #888888);
  color: var(--rt-com-text, #cccccc);
}

/* --- Gear icon --- */
.rt-badge-gear {
  position: absolute;
  right: 3px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 10px;
  line-height: 1;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
  cursor: pointer;
}

.rt-badge--subreddit:hover .rt-badge-gear {
  opacity: 0.6;
  pointer-events: auto;
}

.rt-badge-gear:hover {
  opacity: 1 !important;
}

/* --- Color popover --- */
.rt-color-popover {
  position: absolute;
  z-index: 10000;
  background: #fff;
  border: 1px solid #ccc;
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  padding: 12px;
  width: 260px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: 13px;
  color: #1a1a1a;
  display: none;
}

.rt-color-popover.rt-popover-visible {
  display: block;
}

/* Popover arrow */
.rt-color-popover::before {
  content: "";
  position: absolute;
  top: -6px;
  left: 16px;
  width: 10px;
  height: 10px;
  background: #fff;
  border-left: 1px solid #ccc;
  border-top: 1px solid #ccc;
  transform: rotate(45deg);
}

.rt-popover-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
  padding-bottom: 6px;
  border-bottom: 1px solid #eee;
}

.rt-popover-title {
  font-weight: 600;
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 200px;
}

.rt-popover-close {
  background: none;
  border: none;
  font-size: 16px;
  cursor: pointer;
  color: #999;
  padding: 0 2px;
  line-height: 1;
}

.rt-popover-close:hover {
  color: #333;
}

.rt-popover-mode {
  margin-bottom: 8px;
}

.rt-popover-mode select {
  width: 100%;
  padding: 3px 6px;
  border: 1px solid #ccc;
  border-radius: 3px;
  font-size: 12px;
  font-family: inherit;
}

.rt-popover-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;
}

.rt-popover-row-label {
  font-size: 12px;
  flex-shrink: 0;
  margin-right: 8px;
}

.rt-popover-row-inputs {
  display: flex;
  align-items: center;
  gap: 4px;
}

.rt-popover-row input[type="color"] {
  width: 28px;
  height: 20px;
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 1px;
  cursor: pointer;
}

.rt-popover-row input[type="text"] {
  width: 70px;
  padding: 2px 4px;
  border: 1px solid #ccc;
  border-radius: 3px;
  font-size: 12px;
  font-family: monospace;
}

.rt-popover-row input[type="text"].rt-input-error {
  border-color: #d9534f;
}

.rt-popover-preview {
  margin: 8px 0;
  padding: 6px 0;
  border-top: 1px solid #eee;
  border-bottom: 1px solid #eee;
  text-align: center;
}

.rt-popover-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 8px;
  gap: 6px;
}

.rt-popover-footer-right {
  display: flex;
  gap: 6px;
}

.rt-btn-save {
  background: #4a90d9;
  color: #fff;
  border: 1px solid #3a7bc8;
  border-radius: 3px;
  padding: 4px 12px;
  font-size: 12px;
  cursor: pointer;
}

.rt-btn-save:hover {
  background: #3a7bc8;
}

.rt-btn-cancel {
  background: #f0f0f0;
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 4px 10px;
  font-size: 12px;
  cursor: pointer;
}

.rt-btn-cancel:hover {
  background: #e0e0e0;
}

.rt-btn-reset {
  background: none;
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 4px 10px;
  font-size: 12px;
  cursor: pointer;
  color: #666;
}

.rt-btn-reset:hover {
  background: #f5f5f5;
  color: #333;
}

/* --- Popover dark mode --- */
.rt-color-popover.rt-dark {
  background: #2a2a2a;
  border-color: #555;
  color: #e0e0e0;
}

.rt-color-popover.rt-dark::before {
  background: #2a2a2a;
  border-left-color: #555;
  border-top-color: #555;
}

.rt-color-popover.rt-dark .rt-popover-header {
  border-bottom-color: #444;
}

.rt-color-popover.rt-dark .rt-popover-close {
  color: #888;
}

.rt-color-popover.rt-dark .rt-popover-close:hover {
  color: #ccc;
}

.rt-color-popover.rt-dark .rt-popover-mode select {
  background: #333;
  border-color: #555;
  color: #e0e0e0;
}

.rt-color-popover.rt-dark .rt-popover-row input[type="color"] {
  border-color: #555;
}

.rt-color-popover.rt-dark .rt-popover-row input[type="text"] {
  background: #333;
  border-color: #555;
  color: #e0e0e0;
}

.rt-color-popover.rt-dark .rt-popover-preview {
  border-top-color: #444;
  border-bottom-color: #444;
}

.rt-color-popover.rt-dark .rt-btn-save {
  background: #4a90d9;
  border-color: #3a7bc8;
}

.rt-color-popover.rt-dark .rt-btn-cancel {
  background: #444;
  border-color: #555;
  color: #ccc;
}

.rt-color-popover.rt-dark .rt-btn-reset {
  border-color: #555;
  color: #999;
}

.rt-color-popover.rt-dark .rt-btn-reset:hover {
  background: #333;
  color: #ccc;
}
  • Step 3: Verify files are valid

Run: node --check content/tweaks/badges.js

Expected: no output (exit 0)

  • Step 4: Commit
git add content/tweaks/badges.js content/styles/badges.css
git commit -m "feat: gear icon on subreddit badges and popover CSS"

Task 3: Color Popover Logic

Files:

  • Create: content/tweaks/color-popover.js
  • Modify: manifest.json:20-24

Interfaces:

  • Consumes:

    • window.RedditTweaks.loadSettings() → Promise<object> (from lib/settings.js)
    • window.RedditTweaks.saveSettings(settings) → Promise<void> (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

(function () {
  "use strict";

  var popoverEl = null;
  var currentSubreddit = null;
  var currentAnchor = null;
  var editState = { light: null, dark: null };
  var selectedMode = "light";

  var els = {};

  function getHelpers() {
    return {
      isDarkMode: window.RedditTweaks.badgeTweak.isDarkMode,
      getColorPrefix: window.RedditTweaks.badgeTweak.getColorPrefix,
    };
  }

  function hexValid(val) {
    return /^#[0-9a-f]{6}$/i.test(val);
  }

  function colorToHex(val) {
    if (!val) return "#ffffff";
    return val;
  }

  function createPopover() {
    var el = document.createElement("div");
    el.className = "rt-color-popover";

    el.innerHTML = [
      '<div class="rt-popover-header">',
      '  <span class="rt-popover-title"></span>',
      '  <button class="rt-popover-close" type="button">\u00d7</button>',
      "</div>",
      '<div class="rt-popover-mode">',
      '  <select class="rt-popover-mode-select">',
      '    <option value="light">Light</option>',
      '    <option value="dark">Dark</option>',
      "  </select>",
      "</div>",
      '<div class="rt-popover-body">',
      '  <div class="rt-popover-row" data-prop="bg">',
      '    <span class="rt-popover-row-label">Background</span>',
      '    <div class="rt-popover-row-inputs">',
      '      <input type="color" class="rt-color-swatch">',
      '      <input type="text" class="rt-color-hex" maxlength="7">',
      "    </div>",
      "  </div>",
      '  <div class="rt-popover-row" data-prop="border">',
      '    <span class="rt-popover-row-label">Border</span>',
      '    <div class="rt-popover-row-inputs">',
      '      <input type="color" class="rt-color-swatch">',
      '      <input type="text" class="rt-color-hex" maxlength="7">',
      "    </div>",
      "  </div>",
      '  <div class="rt-popover-row" data-prop="text">',
      '    <span class="rt-popover-row-label">Text</span>',
      '    <div class="rt-popover-row-inputs">',
      '      <input type="color" class="rt-color-swatch">',
      '      <input type="text" class="rt-color-hex" maxlength="7">',
      "    </div>",
      "  </div>",
      "</div>",
      '<div class="rt-popover-preview">',
      '  <span class="rt-badge rt-badge--subreddit rt-popover-preview-badge" style="padding-right:6px"></span>',
      "</div>",
      '<div class="rt-popover-footer">',
      '  <button class="rt-btn-reset" type="button">Reset Default</button>',
      '  <div class="rt-popover-footer-right">',
      '    <button class="rt-btn-cancel" type="button">Cancel</button>',
      '    <button class="rt-btn-save" type="button">Save</button>',
      "  </div>",
      "</div>",
    ].join("\n");

    document.body.appendChild(el);
    popoverEl = el;

    els.title = el.querySelector(".rt-popover-title");
    els.closeBtn = el.querySelector(".rt-popover-close");
    els.modeSelect = el.querySelector(".rt-popover-mode-select");
    els.preview = el.querySelector(".rt-popover-preview-badge");
    els.saveBtn = el.querySelector(".rt-btn-save");
    els.cancelBtn = el.querySelector(".rt-btn-cancel");
    els.resetBtn = el.querySelector(".rt-btn-reset");

    els.rows = {};
    var rows = el.querySelectorAll(".rt-popover-row");
    for (var i = 0; i < rows.length; i++) {
      var row = rows[i];
      var prop = row.getAttribute("data-prop");
      els.rows[prop] = {
        swatch: row.querySelector(".rt-color-swatch"),
        hex: row.querySelector(".rt-color-hex"),
      };
    }

    els.closeBtn.addEventListener("click", close);
    els.cancelBtn.addEventListener("click", close);
    els.saveBtn.addEventListener("click", save);
    els.resetBtn.addEventListener("click", resetToDefault);

    els.modeSelect.addEventListener("change", function () {
      storeCurrentEdits();
      selectedMode = els.modeSelect.value;
      loadModeIntoInputs();
      updatePreview();
    });

    var props = ["bg", "border", "text"];
    for (var p = 0; p < props.length; p++) {
      (function (prop) {
        var row = els.rows[prop];
        row.swatch.addEventListener("input", function () {
          row.hex.value = row.swatch.value;
          row.hex.classList.remove("rt-input-error");
          updatePreview();
        });
        row.hex.addEventListener("input", function () {
          if (hexValid(row.hex.value)) {
            row.swatch.value = row.hex.value;
            row.hex.classList.remove("rt-input-error");
            updatePreview();
          } else {
            row.hex.classList.add("rt-input-error");
          }
        });
      })(props[p]);
    }

    document.addEventListener("mousedown", onDocumentMouseDown, true);
    document.addEventListener("keydown", onDocumentKeyDown, true);
  }

  function onDocumentMouseDown(e) {
    if (!popoverEl || !popoverEl.classList.contains("rt-popover-visible")) return;
    if (popoverEl.contains(e.target)) return;

    var active = document.activeElement;
    if (active && active.type === "color" && popoverEl.contains(active)) return;

    close();
  }

  function onDocumentKeyDown(e) {
    if (e.key === "Escape" && popoverEl && popoverEl.classList.contains("rt-popover-visible")) {
      close();
    }
  }

  function getDefaultsForMode(settings, mode) {
    return {
      bg: settings[mode + ".subredditBgColor"],
      border: settings[mode + ".subredditBorderColor"],
      text: settings[mode + ".subredditTextColor"],
    };
  }

  function getColorsForMode(settings, subredditName, mode) {
    var entry = settings.subredditColors[subredditName];
    if (entry && entry[mode]) {
      return { bg: entry[mode].bg, border: entry[mode].border, text: entry[mode].text };
    }
    return getDefaultsForMode(settings, mode);
  }

  function storeCurrentEdits() {
    editState[selectedMode] = {
      bg: els.rows.bg.swatch.value,
      border: els.rows.border.swatch.value,
      text: els.rows.text.swatch.value,
    };
  }

  function loadModeIntoInputs() {
    var colors = editState[selectedMode];
    var props = ["bg", "border", "text"];
    for (var i = 0; i < props.length; i++) {
      var prop = props[i];
      var val = colorToHex(colors[prop]);
      els.rows[prop].swatch.value = val;
      els.rows[prop].hex.value = val;
      els.rows[prop].hex.classList.remove("rt-input-error");
    }
  }

  function updatePreview() {
    var bg = els.rows.bg.swatch.value;
    var border = els.rows.border.swatch.value;
    var text = els.rows.text.swatch.value;
    els.preview.style.background = bg;
    els.preview.style.borderColor = border;
    els.preview.style.color = text;
  }

  function positionPopover(anchorEl) {
    var rect = anchorEl.getBoundingClientRect();
    var gap = 6;

    popoverEl.style.visibility = "hidden";
    popoverEl.classList.add("rt-popover-visible");

    var popRect = popoverEl.getBoundingClientRect();
    var top = rect.bottom + window.scrollY + gap;
    var left = rect.left + window.scrollX;

    if (rect.bottom + gap + popRect.height > window.innerHeight) {
      top = rect.top + window.scrollY - popRect.height - gap;
    }

    var maxLeft = document.documentElement.scrollWidth - popRect.width - 8;
    if (left > maxLeft) left = maxLeft;
    if (left < 8) left = 8;

    popoverEl.style.top = top + "px";
    popoverEl.style.left = left + "px";
    popoverEl.style.visibility = "";
  }

  function applyDarkModeToPopover(settings) {
    var helpers = getHelpers();
    if (helpers.isDarkMode(settings)) {
      popoverEl.classList.add("rt-dark");
    } else {
      popoverEl.classList.remove("rt-dark");
    }
  }

  function open(badgeEl, subredditName, settings) {
    if (!popoverEl) createPopover();

    if (currentSubreddit === subredditName && popoverEl.classList.contains("rt-popover-visible")) {
      close();
      return;
    }

    currentSubreddit = subredditName;
    currentAnchor = badgeEl;

    var helpers = getHelpers();
    selectedMode = helpers.getColorPrefix(settings);

    editState.light = getColorsForMode(settings, subredditName, "light");
    editState.dark = getColorsForMode(settings, subredditName, "dark");

    els.title.textContent = "r/" + subredditName;
    els.modeSelect.value = selectedMode;
    els.preview.textContent = "r/" + subredditName;

    applyDarkModeToPopover(settings);
    loadModeIntoInputs();
    updatePreview();
    positionPopover(badgeEl);
  }

  function close() {
    if (!popoverEl) return;
    popoverEl.classList.remove("rt-popover-visible");
    currentSubreddit = null;
    currentAnchor = null;
    editState = { light: null, dark: null };
  }

  async function save() {
    if (!currentSubreddit) return;

    storeCurrentEdits();

    var settings = await window.RedditTweaks.loadSettings();
    var entry = settings.subredditColors[currentSubreddit] || {};

    if (editState.light) entry.light = editState.light;
    if (editState.dark) entry.dark = editState.dark;

    settings.subredditColors[currentSubreddit] = entry;
    await window.RedditTweaks.saveSettings(settings);
    close();
  }

  async function resetToDefault() {
    if (!currentSubreddit) return;

    var settings = await window.RedditTweaks.loadSettings();
    delete settings.subredditColors[currentSubreddit];
    await window.RedditTweaks.saveSettings(settings);

    editState.light = getDefaultsForMode(settings, "light");
    editState.dark = getDefaultsForMode(settings, "dark");
    loadModeIntoInputs();
    updatePreview();
  }

  window.RedditTweaks = window.RedditTweaks || {};
  window.RedditTweaks.colorPopover = {
    open: open,
    close: close,
  };
})();
  • Step 2: Update manifest.json — add color-popover.js to content scripts

Replace the js array in content_scripts (lines 20-24):

      "js": [
        "lib/settings.js",
        "content/tweaks/badges.js",
        "content/tweaks/color-popover.js",
        "content/main.js"
      ],

color-popover.js loads after badges.js (so badgeTweak namespace exists) and before main.js (so the popover namespace is available when badges are processed).

  • Step 3: Verify JS is valid

Run: node --check content/tweaks/color-popover.js

Expected: no output (exit 0)

  • Step 4: Verify manifest JSON is valid

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

Expected: valid

  • Step 5: Commit
git add content/tweaks/color-popover.js manifest.json
git commit -m "feat: inline color popover with mode toggle, live preview, and save/reset"

Task 4: Popup UI Updates for Mode-Aware Overrides

Files:

  • Modify: popup/popup.html:60-65
  • Modify: popup/popup.js:36-105
  • Modify: popup/popup.css (add new styles)

Interfaces:

  • Consumes:

    • window.RedditTweaks.loadSettings() → settings with v2 subredditColors schema (from Task 1)
    • window.RedditTweaks.saveSettings(settings) → Promise<void>
  • Produces: updated popup UI (no API consumed by other tasks)

  • Step 1: Update popup/popup.html — per-subreddit section

Replace lines 60-65 (the per-subreddit overrides section):

    <div class="section">
      <h2>Per-Subreddit Overrides</h2>
      <p class="section-hint">Set custom subreddit badge colors per mode.</p>
      <div id="subredditList"></div>
      <button id="addSubreddit" class="btn-secondary">+ Add Subreddit</button>
    </div>
  • Step 2: Add styles for the updated subreddit entries in popup/popup.css

Append to the end of popup/popup.css:


.section-hint {
  font-size: 11px;
  color: #888;
  margin-bottom: 8px;
}

.subreddit-entry-header {
  display: flex;
  gap: 4px;
  align-items: center;
  margin-bottom: 4px;
}

.subreddit-entry-header input[type="text"] {
  flex: 1;
  min-width: 0;
  padding: 2px 6px;
  border: 1px solid #ccc;
  border-radius: 3px;
  font-size: 12px;
  font-family: monospace;
}

.subreddit-entry-mode {
  display: flex;
  gap: 4px;
  align-items: center;
  margin-bottom: 4px;
}

.subreddit-entry-mode select {
  width: auto;
  padding: 2px 6px;
  font-size: 11px;
  margin-top: 0;
}

.subreddit-entry-colors {
  display: flex;
  gap: 4px;
  align-items: center;
}
  • Step 3: Replace per-subreddit override functions in popup/popup.js

Replace renderSubredditOverrides (lines 36-43), addSubredditEntry (lines 45-88), and collectSubredditOverrides (lines 90-105) with:

  function renderSubredditOverrides(overrides) {
    var list = document.getElementById("subredditList");
    list.innerHTML = "";

    Object.entries(overrides).forEach(function (pair) {
      addSubredditEntry(pair[0], pair[1]);
    });
  }

  function addSubredditEntry(name, modeColors) {
    var list = document.getElementById("subredditList");
    var entry = document.createElement("div");
    entry.className = "subreddit-entry";

    var header = document.createElement("div");
    header.className = "subreddit-entry-header";

    var nameInput = document.createElement("input");
    nameInput.type = "text";
    nameInput.placeholder = "subreddit";
    nameInput.value = name || "";

    var removeBtn = document.createElement("button");
    removeBtn.className = "btn-remove";
    removeBtn.textContent = "\u00d7";
    removeBtn.addEventListener("click", function () {
      entry.remove();
      saveFromControls();
    });

    header.appendChild(nameInput);
    header.appendChild(removeBtn);

    var modeRow = document.createElement("div");
    modeRow.className = "subreddit-entry-mode";

    var modeSelect = document.createElement("select");
    modeSelect.innerHTML = '<option value="light">Light</option><option value="dark">Dark</option>';
    modeSelect.value = "light";
    modeRow.appendChild(modeSelect);

    var colorsDiv = document.createElement("div");
    colorsDiv.className = "subreddit-entry-colors";

    var lightColors = modeColors && modeColors.light ? modeColors.light : {};
    var darkColors = modeColors && modeColors.dark ? modeColors.dark : {};

    var bgInput = document.createElement("input");
    bgInput.type = "color";
    bgInput.value = colorToHex(lightColors.bg);
    bgInput.title = "Background";

    var borderInput = document.createElement("input");
    borderInput.type = "color";
    borderInput.value = colorToHex(lightColors.border);
    borderInput.title = "Border";

    var textInput = document.createElement("input");
    textInput.type = "color";
    textInput.value = colorToHex(lightColors.text);
    textInput.title = "Text";

    colorsDiv.appendChild(bgInput);
    colorsDiv.appendChild(borderInput);
    colorsDiv.appendChild(textInput);

    entry._colorState = {
      light: { bg: bgInput.value, border: borderInput.value, text: textInput.value },
      dark: { bg: colorToHex(darkColors.bg), border: colorToHex(darkColors.border), text: colorToHex(darkColors.text) },
    };

    modeSelect.addEventListener("change", function () {
      var prev = modeSelect.value === "dark" ? "light" : "dark";
      entry._colorState[prev] = { bg: bgInput.value, border: borderInput.value, text: textInput.value };
      var next = entry._colorState[modeSelect.value];
      bgInput.value = next.bg;
      borderInput.value = next.border;
      textInput.value = next.text;
    });

    [nameInput, bgInput, borderInput, textInput].forEach(function (input) {
      input.addEventListener("change", saveFromControls);
    });

    entry.appendChild(header);
    entry.appendChild(modeRow);
    entry.appendChild(colorsDiv);

    entry._nameInput = nameInput;
    entry._bgInput = bgInput;
    entry._borderInput = borderInput;
    entry._textInput = textInput;
    entry._modeSelect = modeSelect;

    list.appendChild(entry);
  }

  function collectSubredditOverrides() {
    var overrides = {};
    var entries = document.querySelectorAll(".subreddit-entry");
    entries.forEach(function (entry) {
      var name = entry._nameInput.value.trim().replace(/^r\//, "");
      if (!name) return;

      var currentMode = entry._modeSelect.value;
      entry._colorState[currentMode] = {
        bg: entry._bgInput.value,
        border: entry._borderInput.value,
        text: entry._textInput.value,
      };

      overrides[name] = {
        light: entry._colorState.light,
        dark: entry._colorState.dark,
      };
    });
    return overrides;
  }
  • Step 4: Verify JS is valid

Run: node --check popup/popup.js

Expected: no output (exit 0)

  • Step 5: Commit
git add popup/popup.html popup/popup.js popup/popup.css
git commit -m "feat: popup per-subreddit overrides with mode-aware light/dark toggle"

Post-Implementation

After all tasks are complete, load the extension as a temporary add-on in Firefox via about:debugging → "This Firefox" → "Load Temporary Add-on" → select manifest.json. Then run through the test checklist from the spec (items 1–20 in the Testing section of docs/superpowers/specs/2026-08-26-inline-color-popover-design.md).