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>
This commit is contained in:
987
docs/superpowers/plans/2026-08-26-reddit-badge-tweaks.md
Normal file
987
docs/superpowers/plans/2026-08-26-reddit-badge-tweaks.md
Normal file
@@ -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 `<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).
|
||||
Reference in New Issue
Block a user