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

988 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`**
```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<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`**
```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<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`**
```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`**
```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).