1108 lines
31 KiB
Markdown
1108 lines
31 KiB
Markdown
|
|
# 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:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
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):
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
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:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
container.setAttribute("data-rt-subreddit", subredditName);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Update `content/tweaks/badges.js` — `updateColors()` reads from data attribute**
|
|||
|
|
|
|||
|
|
Replace lines 142-145 of `updateColors()`:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
const subredditName = container.getAttribute("data-rt-subreddit");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Export `isDarkMode` and `getColorPrefix` on the `badgeTweak` namespace**
|
|||
|
|
|
|||
|
|
Replace lines 157-161:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
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**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
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:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
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:
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* --- 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**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
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`**
|
|||
|
|
|
|||
|
|
```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):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
"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**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
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):
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<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`:
|
|||
|
|
|
|||
|
|
```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:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
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**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
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`).
|