Files
reddit-tweaks-ffe/docs/superpowers/plans/2026-08-26-badge-polish.md

1625 lines
53 KiB
Markdown
Raw Permalink Normal View History

# Badge Polish: Gear Animation + Custom Color Picker — 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:** Animate the gear icon reveal on badge hover and replace all native `<input type="color">` elements with a tabbed inline color picker (Palette, Named Colors, Custom HSV).
**Architecture:** The gear animation is a CSS-only change (2 lines). The custom color picker is a new vanilla JS module (`color-picker.js`) that builds a tabbed widget with three panels: a Tailwind/shadcn palette grid, CSS named colors grid, and an HSV picker with canvas-rendered SV square + hue bar. The module exposes a `create()` factory that returns a picker instance with `open/close/setColor/isOpen/destroy` methods and an `.el` property for DOM insertion. The picker is integrated into the inline popover (`color-popover.js`) and the extension popup (`popup.js`).
**Tech Stack:** Vanilla JS, HTML5 Canvas, CSS, Firefox WebExtension APIs (manifest v2).
## Global Constraints
- No external dependencies — vanilla JS only, no build step
- Must work in both Firefox content script and popup contexts
- Colors stored as lowercase `#rrggbb` hex strings
- All CSS classes prefixed with `rt-` to avoid conflicts with Reddit's styles
- Dark mode support via `.rt-dark` class on ancestor elements
- Spec: `docs/superpowers/specs/2026-08-26-badge-polish-design.md`
---
### Task 1: Gear Icon Animation
**Files:**
- Modify: `content/styles/badges.css:32-38`
**Interfaces:**
- Consumes: nothing
- Produces: nothing (CSS-only, no API changes)
- [ ] **Step 1: Update `.rt-badge--subreddit` and add hover rule**
In `content/styles/badges.css`, replace the `.rt-badge--subreddit` block:
```css
.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);
}
```
with:
```css
.rt-badge--subreddit {
position: relative;
padding-right: 6px;
transition: padding-right 0.15s;
background: var(--rt-sub-bg, #fde8e0);
border-color: var(--rt-sub-border, #c84a20);
color: var(--rt-sub-text, #c84a20);
}
.rt-badge--subreddit:hover {
padding-right: 18px;
}
```
- [ ] **Step 2: Verify manually**
Load `old.reddit.com` with the extension enabled. Verify:
1. Subreddit badges have no gap at rest (padding matches left side).
2. Hovering over a subreddit badge smoothly expands it to reveal the gear icon (~150ms).
3. The adjacent comments badge shifts right during expansion.
4. On mouse-out, the badge shrinks and the gear fades out.
- [ ] **Step 3: Commit**
```bash
git add content/styles/badges.css
git commit -m "feat: animate gear icon reveal on badge hover"
```
---
### Task 2: Color Picker Widget + CSS
**Files:**
- Create: `content/tweaks/color-picker.js`
- Modify: `content/styles/badges.css` (append picker CSS at end)
- Modify: `popup/popup.css` (append picker CSS at end)
- Modify: `manifest.json:20-25`
**Interfaces:**
- Consumes: nothing
- Produces: `window.RedditTweaks.colorPicker` with:
- `create()` → returns `{ el: HTMLElement, open(hex, onChange), close(), setColor(hex), isOpen(), destroy() }`
- [ ] **Step 1: Create `content/tweaks/color-picker.js`**
```javascript
(function () {
"use strict";
var SHADES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
var PALETTE = [
["slate","#f8fafc","#f1f5f9","#e2e8f0","#cbd5e1","#94a3b8","#64748b","#475569","#334155","#1e293b","#0f172a","#020617"],
["gray","#f9fafb","#f3f4f6","#e5e7eb","#d1d5db","#9ca3af","#6b7280","#4b5563","#374151","#1f2937","#111827","#030712"],
["zinc","#fafafa","#f4f4f5","#e4e4e7","#d4d4d8","#a1a1aa","#71717a","#52525b","#3f3f46","#27272a","#18181b","#09090b"],
["neutral","#fafafa","#f5f5f5","#e5e5e5","#d4d4d4","#a3a3a3","#737373","#525252","#404040","#262626","#171717","#0a0a0a"],
["stone","#fafaf9","#f5f5f4","#e7e5e4","#d6d3d1","#a8a29e","#78716c","#57534e","#44403c","#292524","#1c1917","#0c0a09"],
["red","#fef2f2","#fee2e2","#fecaca","#fca5a5","#f87171","#ef4444","#dc2626","#b91c1c","#991b1b","#7f1d1d","#450a0a"],
["orange","#fff7ed","#ffedd5","#fed7aa","#fdba74","#fb923c","#f97316","#ea580c","#c2410c","#9a3412","#7c2d12","#431407"],
["amber","#fffbeb","#fef3c7","#fde68a","#fcd34d","#fbbf24","#f59e0b","#d97706","#b45309","#92400e","#78350f","#451a03"],
["yellow","#fefce8","#fef9c3","#fef08a","#fde047","#facc15","#eab308","#ca8a04","#a16207","#854d0e","#713f12","#422006"],
["lime","#f7fee7","#ecfccb","#d9f99d","#bef264","#a3e635","#84cc16","#65a30d","#4d7c0f","#3f6212","#365314","#1a2e05"],
["green","#f0fdf4","#dcfce7","#bbf7d0","#86efac","#4ade80","#22c55e","#16a34a","#15803d","#166534","#14532d","#052e16"],
["emerald","#ecfdf5","#d1fae5","#a7f3d0","#6ee7b7","#34d399","#10b981","#059669","#047857","#065f46","#064e3b","#022c22"],
["teal","#f0fdfa","#ccfbf1","#99f6e4","#5eead4","#2dd4bf","#14b8a6","#0d9488","#0f766e","#115e59","#134e4a","#042f2e"],
["cyan","#ecfeff","#cffafe","#a5f3fc","#67e8f9","#22d3ee","#06b6d4","#0891b2","#0e7490","#155e75","#164e63","#083344"],
["sky","#f0f9ff","#e0f2fe","#bae6fd","#7dd3fc","#38bdf8","#0ea5e9","#0284c7","#0369a1","#075985","#0c4a6e","#082f49"],
["blue","#eff6ff","#dbeafe","#bfdbfe","#93c5fd","#60a5fa","#3b82f6","#2563eb","#1d4ed8","#1e40af","#1e3a8a","#172554"],
["indigo","#eef2ff","#e0e7ff","#c7d2fe","#a5b4fc","#818cf8","#6366f1","#4f46e5","#4338ca","#3730a3","#312e81","#1e1b4b"],
["violet","#f5f3ff","#ede9fe","#ddd6fe","#c4b5fd","#a78bfa","#8b5cf6","#7c3aed","#6d28d9","#5b21b6","#4c1d95","#2e1065"],
["purple","#faf5ff","#f3e8ff","#e9d5ff","#d8b4fe","#c084fc","#a855f7","#9333ea","#7e22ce","#6b21a8","#581c87","#3b0764"],
["fuchsia","#fdf4ff","#fae8ff","#f5d0fe","#f0abfc","#e879f9","#d946ef","#c026d3","#a21caf","#86198f","#701a75","#4a044e"],
["pink","#fdf2f8","#fce7f3","#fbcfe8","#f9a8d4","#f472b6","#ec4899","#db2777","#be185d","#9d174d","#831843","#500724"],
["rose","#fff1f2","#ffe4e6","#fecdd3","#fda4af","#fb7185","#f43f5e","#e11d48","#be123c","#9f1239","#881337","#4c0519"],
];
var NAMED_COLORS = [
["aliceblue","#f0f8ff"],["antiquewhite","#faebd7"],["aqua","#00ffff"],
["aquamarine","#7fffd4"],["azure","#f0ffff"],["beige","#f5f5dc"],
["bisque","#ffe4c4"],["black","#000000"],["blanchedalmond","#ffebcd"],
["blue","#0000ff"],["blueviolet","#8a2be2"],["brown","#a52a2a"],
["burlywood","#deb887"],["cadetblue","#5f9ea0"],["chartreuse","#7fff00"],
["chocolate","#d2691e"],["coral","#ff7f50"],["cornflowerblue","#6495ed"],
["cornsilk","#fff8dc"],["crimson","#dc143c"],["cyan","#00ffff"],
["darkblue","#00008b"],["darkcyan","#008b8b"],["darkgoldenrod","#b8860b"],
["darkgray","#a9a9a9"],["darkgreen","#006400"],["darkkhaki","#bdb76b"],
["darkmagenta","#8b008b"],["darkolivegreen","#556b2f"],["darkorange","#ff8c00"],
["darkorchid","#9932cc"],["darkred","#8b0000"],["darksalmon","#e9967a"],
["darkseagreen","#8fbc8f"],["darkslateblue","#483d8b"],["darkslategray","#2f4f4f"],
["darkturquoise","#00ced1"],["darkviolet","#9400d3"],["deeppink","#ff1493"],
["deepskyblue","#00bfff"],["dimgray","#696969"],["dodgerblue","#1e90ff"],
["firebrick","#b22222"],["floralwhite","#fffaf0"],["forestgreen","#228b22"],
["fuchsia","#ff00ff"],["gainsboro","#dcdcdc"],["ghostwhite","#f8f8ff"],
["gold","#ffd700"],["goldenrod","#daa520"],["gray","#808080"],
["green","#008000"],["greenyellow","#adff2f"],["honeydew","#f0fff0"],
["hotpink","#ff69b4"],["indianred","#cd5c5c"],["indigo","#4b0082"],
["ivory","#fffff0"],["khaki","#f0e68c"],["lavender","#e6e6fa"],
["lavenderblush","#fff0f5"],["lawngreen","#7cfc00"],["lemonchiffon","#fffacd"],
["lightblue","#add8e6"],["lightcoral","#f08080"],["lightcyan","#e0ffff"],
["lightgoldenrodyellow","#fafad2"],["lightgray","#d3d3d3"],["lightgreen","#90ee90"],
["lightpink","#ffb6c1"],["lightsalmon","#ffa07a"],["lightseagreen","#20b2aa"],
["lightskyblue","#87cefa"],["lightslategray","#778899"],["lightsteelblue","#b0c4de"],
["lightyellow","#ffffe0"],["lime","#00ff00"],["limegreen","#32cd32"],
["linen","#faf0e6"],["magenta","#ff00ff"],["maroon","#800000"],
["mediumaquamarine","#66cdaa"],["mediumblue","#0000cd"],["mediumorchid","#ba55d3"],
["mediumpurple","#9370db"],["mediumseagreen","#3cb371"],["mediumslateblue","#7b68ee"],
["mediumspringgreen","#00fa9a"],["mediumturquoise","#48d1cc"],["mediumvioletred","#c71585"],
["midnightblue","#191970"],["mintcream","#f5fffa"],["mistyrose","#ffe4e1"],
["moccasin","#ffe4b5"],["navajowhite","#ffdead"],["navy","#000080"],
["oldlace","#fdf5e6"],["olive","#808000"],["olivedrab","#6b8e23"],
["orange","#ffa500"],["orangered","#ff4500"],["orchid","#da70d6"],
["palegoldenrod","#eee8aa"],["palegreen","#98fb98"],["paleturquoise","#afeeee"],
["palevioletred","#db7093"],["papayawhip","#ffefd5"],["peachpuff","#ffdab9"],
["peru","#cd853f"],["pink","#ffc0cb"],["plum","#dda0dd"],
["powderblue","#b0e0e6"],["purple","#800080"],["rebeccapurple","#663399"],
["red","#ff0000"],["rosybrown","#bc8f8f"],["royalblue","#4169e1"],
["saddlebrown","#8b4513"],["salmon","#fa8072"],["sandybrown","#f4a460"],
["seagreen","#2e8b57"],["seashell","#fff5ee"],["sienna","#a0522d"],
["silver","#c0c0c0"],["skyblue","#87ceeb"],["slateblue","#6a5acd"],
["slategray","#708090"],["snow","#fffafa"],["springgreen","#00ff7f"],
["steelblue","#4682b4"],["tan","#d2b48c"],["teal","#008080"],
["thistle","#d8bfd8"],["tomato","#ff6347"],["turquoise","#40e0d0"],
["violet","#ee82ee"],["wheat","#f5deb3"],["white","#ffffff"],
["whitesmoke","#f5f5f5"],["yellow","#ffff00"],["yellowgreen","#9acd32"],
];
function hsvToRgb(h, s, v) {
var c = v * s;
var x = c * (1 - Math.abs((h / 60) % 2 - 1));
var m = v - c;
var r, g, b;
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
}
function rgbToHex(r, g, b) {
return "#" + [r, g, b].map(function (c) {
return c.toString(16).padStart(2, "0");
}).join("");
}
function hexToRgb(hex) {
return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
}
function rgbToHsv(r, g, b) {
r /= 255; g /= 255; b /= 255;
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
var d = max - min;
var h, s, v;
v = max;
s = max === 0 ? 0 : d / max;
if (d === 0) {
h = 0;
} else if (max === r) {
h = 60 * (((g - b) / d) % 6);
} else if (max === g) {
h = 60 * ((b - r) / d + 2);
} else {
h = 60 * ((r - g) / d + 4);
}
if (h < 0) h += 360;
return [h, s, v];
}
function hsvToHex(h, s, v) {
var rgb = hsvToRgb(h, s, v);
return rgbToHex(rgb[0], rgb[1], rgb[2]);
}
function hexToHsv(hex) {
var rgb = hexToRgb(hex);
return rgbToHsv(rgb[0], rgb[1], rgb[2]);
}
var lastTab = "palette";
function buildPalettePanel(onSelect) {
var panel = document.createElement("div");
panel.className = "rt-picker-panel";
var grid = document.createElement("div");
grid.className = "rt-picker-grid rt-picker-grid--palette";
PALETTE.forEach(function (family) {
var row = document.createElement("div");
row.className = "rt-picker-grid-row";
for (var i = 1; i < family.length; i++) {
var swatch = document.createElement("div");
swatch.className = "rt-picker-swatch";
swatch.style.backgroundColor = family[i];
swatch.setAttribute("data-hex", family[i].toLowerCase());
swatch.title = family[0] + "-" + SHADES[i - 1];
swatch.addEventListener("click", (function (hex) {
return function () { onSelect(hex); };
})(family[i].toLowerCase()));
row.appendChild(swatch);
}
grid.appendChild(row);
});
panel.appendChild(grid);
return panel;
}
function buildNamedPanel(onSelect) {
var panel = document.createElement("div");
panel.className = "rt-picker-panel";
var grid = document.createElement("div");
grid.className = "rt-picker-grid rt-picker-grid--named";
NAMED_COLORS.forEach(function (entry) {
var wrap = document.createElement("div");
wrap.className = "rt-picker-named-item";
var swatch = document.createElement("div");
swatch.className = "rt-picker-swatch";
swatch.style.backgroundColor = entry[1];
swatch.setAttribute("data-hex", entry[1].toLowerCase());
var label = document.createElement("span");
label.className = "rt-picker-named-label";
label.textContent = entry[0];
wrap.appendChild(swatch);
wrap.appendChild(label);
wrap.addEventListener("click", (function (hex) {
return function () { onSelect(hex); };
})(entry[1].toLowerCase()));
grid.appendChild(wrap);
});
panel.appendChild(grid);
return panel;
}
function create() {
var state = { h: 0, s: 1, v: 1, hex: "#ff0000", onChange: null, visible: false };
var el = document.createElement("div");
el.className = "rt-color-picker";
el.style.display = "none";
var tabBar = document.createElement("div");
tabBar.className = "rt-picker-tabs";
var tabDefs = [
{ id: "palette", label: "Palette" },
{ id: "named", label: "Named" },
{ id: "custom", label: "Custom" },
];
var tabBtns = {};
tabDefs.forEach(function (t) {
var btn = document.createElement("button");
btn.className = "rt-picker-tab";
btn.textContent = t.label;
btn.type = "button";
btn.addEventListener("click", function () { switchTab(t.id); });
tabBar.appendChild(btn);
tabBtns[t.id] = btn;
});
el.appendChild(tabBar);
var palettePanel = buildPalettePanel(function (hex) { setColorInternal(hex); });
el.appendChild(palettePanel);
var namedPanel = buildNamedPanel(function (hex) { setColorInternal(hex); });
el.appendChild(namedPanel);
var customPanel = document.createElement("div");
customPanel.className = "rt-picker-panel rt-picker-panel--custom";
var svWrap = document.createElement("div");
svWrap.className = "rt-picker-sv-wrap";
var svCanvas = document.createElement("canvas");
svCanvas.className = "rt-picker-sv";
svCanvas.height = 140;
var svCursor = document.createElement("div");
svCursor.className = "rt-picker-sv-cursor";
svWrap.appendChild(svCanvas);
svWrap.appendChild(svCursor);
customPanel.appendChild(svWrap);
var hueBar = document.createElement("div");
hueBar.className = "rt-picker-hue";
var hueIndicator = document.createElement("div");
hueIndicator.className = "rt-picker-hue-indicator";
hueBar.appendChild(hueIndicator);
customPanel.appendChild(hueBar);
el.appendChild(customPanel);
var panels = { palette: palettePanel, named: namedPanel, custom: customPanel };
function switchTab(id) {
lastTab = id;
tabDefs.forEach(function (t) {
panels[t.id].style.display = t.id === id ? "" : "none";
tabBtns[t.id].classList.toggle("rt-picker-tab--active", t.id === id);
});
if (id === "custom") {
resizeSV();
drawSV();
updateSVCursor();
updateHueIndicator();
}
if (id === "palette") highlightSwatch(palettePanel);
if (id === "named") highlightSwatch(namedPanel);
}
function resizeSV() {
var w = svWrap.clientWidth;
if (w < 100) w = 100;
svCanvas.width = w;
svCanvas.height = 140;
}
function drawSV() {
var ctx = svCanvas.getContext("2d");
var w = svCanvas.width;
var h = svCanvas.height;
ctx.fillStyle = "hsl(" + state.h + ", 100%, 50%)";
ctx.fillRect(0, 0, w, h);
var wg = ctx.createLinearGradient(0, 0, w, 0);
wg.addColorStop(0, "rgba(255,255,255,1)");
wg.addColorStop(1, "rgba(255,255,255,0)");
ctx.fillStyle = wg;
ctx.fillRect(0, 0, w, h);
var bg = ctx.createLinearGradient(0, 0, 0, h);
bg.addColorStop(0, "rgba(0,0,0,0)");
bg.addColorStop(1, "rgba(0,0,0,1)");
ctx.fillStyle = bg;
ctx.fillRect(0, 0, w, h);
}
function updateSVCursor() {
svCursor.style.left = (state.s * svCanvas.width) + "px";
svCursor.style.top = ((1 - state.v) * svCanvas.height) + "px";
}
function updateHueIndicator() {
hueIndicator.style.left = ((state.h / 360) * hueBar.clientWidth) + "px";
}
function highlightSwatch(panel) {
var prev = panel.querySelector(".rt-picker-swatch--selected");
if (prev) prev.classList.remove("rt-picker-swatch--selected");
var match = panel.querySelector("[data-hex=\"" + state.hex + "\"]");
if (match) match.classList.add("rt-picker-swatch--selected");
}
function setColorInternal(hex) {
state.hex = hex.toLowerCase();
var hsv = hexToHsv(state.hex);
state.h = hsv[0]; state.s = hsv[1]; state.v = hsv[2];
if (lastTab === "custom") { drawSV(); updateSVCursor(); updateHueIndicator(); }
if (lastTab === "palette") highlightSwatch(palettePanel);
if (lastTab === "named") highlightSwatch(namedPanel);
if (state.onChange) state.onChange(state.hex);
}
function updateFromHSV() {
state.hex = hsvToHex(state.h, state.s, state.v);
if (state.onChange) state.onChange(state.hex);
}
function onSVMouse(e) {
var rect = svCanvas.getBoundingClientRect();
state.s = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
state.v = Math.max(0, Math.min(1, 1 - (e.clientY - rect.top) / rect.height));
updateSVCursor();
updateFromHSV();
}
svWrap.addEventListener("mousedown", function (e) {
e.preventDefault();
onSVMouse(e);
function onMove(ev) { onSVMouse(ev); }
function onUp() { document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); }
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
});
function onHueMouse(e) {
var rect = hueBar.getBoundingClientRect();
state.h = Math.max(0, Math.min(360, ((e.clientX - rect.left) / rect.width) * 360));
updateHueIndicator();
drawSV();
updateFromHSV();
}
hueBar.addEventListener("mousedown", function (e) {
e.preventDefault();
onHueMouse(e);
function onMove(ev) { onHueMouse(ev); }
function onUp() { document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); }
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
});
function open(hex, onChange) {
state.onChange = onChange;
setColor(hex || "#ff0000");
el.style.display = "";
state.visible = true;
switchTab(lastTab);
}
function close() {
el.style.display = "none";
state.visible = false;
state.onChange = null;
}
function setColor(hex) {
if (!hex || !/^#[0-9a-f]{6}$/i.test(hex)) hex = "#ff0000";
state.hex = hex.toLowerCase();
var hsv = hexToHsv(state.hex);
state.h = hsv[0]; state.s = hsv[1]; state.v = hsv[2];
if (state.visible) {
if (lastTab === "custom") { drawSV(); updateSVCursor(); updateHueIndicator(); }
if (lastTab === "palette") highlightSwatch(palettePanel);
if (lastTab === "named") highlightSwatch(namedPanel);
}
}
function isOpen() { return state.visible; }
function destroy() { if (el.parentNode) el.parentNode.removeChild(el); }
return { el: el, open: open, close: close, setColor: setColor, isOpen: isOpen, destroy: destroy };
}
window.RedditTweaks = window.RedditTweaks || {};
window.RedditTweaks.colorPicker = { create: create };
})();
```
- [ ] **Step 2: Append picker CSS to `content/styles/badges.css`**
Add at the end of the file:
```css
/* --- Color Picker Widget --- */
.rt-color-picker {
margin: 8px 0;
font-size: 12px;
}
.rt-picker-tabs {
display: flex;
border-bottom: 1px solid #ccc;
margin-bottom: 8px;
}
.rt-picker-tab {
flex: 1;
padding: 4px 8px;
background: none;
border: none;
border-bottom: 2px solid transparent;
font-size: 11px;
font-family: inherit;
cursor: pointer;
color: #666;
}
.rt-picker-tab:hover {
color: #333;
}
.rt-picker-tab--active {
color: #1a1a1a;
border-bottom-color: #4a90d9;
font-weight: 600;
}
.rt-picker-panel {
max-height: 180px;
overflow-y: auto;
}
.rt-picker-grid--palette {
display: flex;
flex-direction: column;
gap: 2px;
}
.rt-picker-grid-row {
display: flex;
gap: 2px;
}
.rt-picker-swatch {
width: 16px;
height: 16px;
border-radius: 2px;
border: 1px solid rgba(0, 0, 0, 0.1);
cursor: pointer;
flex-shrink: 0;
}
.rt-picker-swatch:hover {
border-color: rgba(0, 0, 0, 0.4);
transform: scale(1.2);
z-index: 1;
position: relative;
}
.rt-picker-swatch--selected {
border: 2px solid #1a1a1a;
box-shadow: 0 0 0 1px white;
}
.rt-picker-grid--named {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 2px 0;
}
.rt-picker-named-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
width: 40px;
}
.rt-picker-named-item .rt-picker-swatch {
width: 24px;
height: 18px;
}
.rt-picker-named-label {
font-size: 8px;
color: #888;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
line-height: 1.2;
}
.rt-picker-panel--custom {
max-height: none;
overflow: visible;
}
.rt-picker-sv-wrap {
position: relative;
margin-bottom: 6px;
}
.rt-picker-sv {
display: block;
width: 100%;
border: 1px solid #ccc;
border-radius: 3px;
cursor: crosshair;
}
.rt-picker-sv-cursor {
position: absolute;
width: 10px;
height: 10px;
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
pointer-events: none;
transform: translate(-50%, -50%);
}
.rt-picker-hue {
position: relative;
height: 14px;
border: 1px solid #ccc;
border-radius: 3px;
background: linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000);
cursor: crosshair;
}
.rt-picker-hue-indicator {
position: absolute;
top: -1px;
width: 2px;
height: calc(100% + 2px);
background: white;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
pointer-events: none;
transform: translateX(-50%);
}
/* Picker dark mode */
.rt-dark .rt-picker-tabs,
.rt-color-popover.rt-dark .rt-picker-tabs {
border-bottom-color: #555;
}
.rt-dark .rt-picker-tab,
.rt-color-popover.rt-dark .rt-picker-tab {
color: #999;
}
.rt-dark .rt-picker-tab:hover,
.rt-color-popover.rt-dark .rt-picker-tab:hover {
color: #ccc;
}
.rt-dark .rt-picker-tab--active,
.rt-color-popover.rt-dark .rt-picker-tab--active {
color: #e0e0e0;
border-bottom-color: #4a90d9;
}
.rt-dark .rt-picker-swatch,
.rt-color-popover.rt-dark .rt-picker-swatch {
border-color: rgba(255, 255, 255, 0.1);
}
.rt-dark .rt-picker-swatch:hover,
.rt-color-popover.rt-dark .rt-picker-swatch:hover {
border-color: rgba(255, 255, 255, 0.4);
}
.rt-dark .rt-picker-swatch--selected,
.rt-color-popover.rt-dark .rt-picker-swatch--selected {
border-color: #e0e0e0;
box-shadow: 0 0 0 1px #333;
}
.rt-dark .rt-picker-named-label,
.rt-color-popover.rt-dark .rt-picker-named-label {
color: #777;
}
.rt-dark .rt-picker-sv,
.rt-color-popover.rt-dark .rt-picker-sv {
border-color: #555;
}
.rt-dark .rt-picker-hue,
.rt-color-popover.rt-dark .rt-picker-hue {
border-color: #555;
}
```
- [ ] **Step 3: Append picker CSS to `popup/popup.css`**
Add the same picker CSS block from Step 2 at the end of `popup/popup.css`. The dark mode rules won't apply in the popup (no `.rt-dark` ancestor) but are harmless.
- [ ] **Step 4: Update `manifest.json`**
Add `content/tweaks/color-picker.js` to the content scripts JS array, **before** `color-popover.js` (the popover depends on the picker):
```json
"js": [
"lib/settings.js",
"content/tweaks/badges.js",
"content/tweaks/color-picker.js",
"content/tweaks/color-popover.js",
"content/main.js"
]
```
- [ ] **Step 5: Verify manually**
Load `old.reddit.com`, open the browser console, and verify:
1. `window.RedditTweaks.colorPicker` exists and has a `create` method.
2. `var p = window.RedditTweaks.colorPicker.create();` returns an object with `el`, `open`, `close`, `setColor`, `isOpen`, `destroy`.
3. `document.body.appendChild(p.el); p.open("#ff0000", function(h){ console.log(h); });` shows the picker with three tabs.
4. Clicking palette swatches logs hex values.
5. Clicking named color swatches logs hex values.
6. Dragging on the Custom tab's SV square/hue bar logs hex values.
7. Clean up: `p.destroy();`
- [ ] **Step 6: Commit**
```bash
git add content/tweaks/color-picker.js content/styles/badges.css popup/popup.css manifest.json
git commit -m "feat: add tabbed color picker widget (palette, named colors, custom HSV)"
```
---
### Task 3: Popover Integration
**Files:**
- Modify: `content/tweaks/color-popover.js`
- Modify: `content/styles/badges.css:180-187` (update swatch selector)
**Interfaces:**
- Consumes: `window.RedditTweaks.colorPicker.create()` → `{ el, open, close, setColor, isOpen, destroy }`
- Produces: Updated popover using custom picker instead of native `<input type="color">`
- [ ] **Step 1: Update swatch CSS selector in `badges.css`**
Replace the selector for the native color input:
```css
.rt-popover-row input[type="color"] {
width: 28px;
height: 20px;
border: 1px solid #ccc;
border-radius: 3px;
padding: 1px;
cursor: pointer;
}
```
with:
```css
.rt-popover-row .rt-color-swatch {
width: 28px;
height: 20px;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
display: inline-block;
vertical-align: middle;
}
```
Also replace the dark mode variant:
```css
.rt-color-popover.rt-dark .rt-popover-row input[type="color"] {
border-color: #555;
}
```
with:
```css
.rt-color-popover.rt-dark .rt-popover-row .rt-color-swatch {
border-color: #555;
}
```
- [ ] **Step 2: Rewrite `color-popover.js`**
Replace the entire file with the updated version. Key changes from the previous version:
- `<input type="color">` → `<div class="rt-color-swatch">` in the HTML template
- Add `var picker = null;` and `var activeProp = null;` at module scope
- Create picker lazily in `createPopover()` and insert between body and preview
- Swatch `input` event → `click` handler that opens/toggles picker
- Hex `input` handler syncs picker via `picker.setColor()` when open
- `loadModeIntoInputs()` sets `swatch.style.backgroundColor` instead of `.value`
- `updatePreview()` and `storeCurrentEdits()` read from `hex.value` instead of `swatch.value`
- Mode toggle closes picker
- `close()` closes picker and resets `activeProp`
- `onDocumentMouseDown` excludes clicks inside `picker.el`
- Remove the `active.type === "color"` check (no longer relevant)
Full replacement content for `content/tweaks/color-popover.js`:
```javascript
(function () {
"use strict";
var popoverEl = null;
var currentSubreddit = null;
var currentAnchor = null;
var editState = { light: null, dark: null };
var selectedMode = "light";
var dirtyModes = { light: false, dark: false };
var hadOverride = { light: false, dark: false };
var picker = null;
var activeProp = null;
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">',
' <div class="rt-color-swatch"></div>',
' <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">',
' <div class="rt-color-swatch"></div>',
' <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">',
' <div class="rt-color-swatch"></div>',
' <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"),
};
}
picker = window.RedditTweaks.colorPicker.create();
var previewSection = el.querySelector(".rt-popover-preview");
el.insertBefore(picker.el, previewSection);
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 () {
markModeDirty(selectedMode);
storeCurrentEdits();
selectedMode = els.modeSelect.value;
closePicker();
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("click", function () {
onSwatchClick(prop);
});
row.hex.addEventListener("input", function () {
markModeDirty(selectedMode);
if (hexValid(row.hex.value)) {
row.swatch.style.backgroundColor = row.hex.value;
row.hex.classList.remove("rt-input-error");
if (picker.isOpen() && activeProp === prop) {
picker.setColor(row.hex.value);
}
updatePreview();
} else {
row.hex.classList.add("rt-input-error");
}
});
})(props[p]);
}
document.addEventListener("mousedown", onDocumentMouseDown, true);
document.addEventListener("keydown", onDocumentKeyDown, true);
}
function onSwatchClick(prop) {
if (picker.isOpen() && activeProp === prop) {
closePicker();
return;
}
closePicker();
activeProp = prop;
var row = els.rows[prop];
picker.open(row.hex.value, function (hex) {
markModeDirty(selectedMode);
row.swatch.style.backgroundColor = hex;
row.hex.value = hex;
row.hex.classList.remove("rt-input-error");
updatePreview();
});
}
function closePicker() {
if (picker && picker.isOpen()) picker.close();
activeProp = null;
}
function onDocumentMouseDown(e) {
if (!popoverEl || !popoverEl.classList.contains("rt-popover-visible")) return;
if (popoverEl.contains(e.target)) 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 markModeDirty(mode) {
dirtyModes[mode] = true;
}
function resetTracking() {
dirtyModes = { light: false, dark: false };
hadOverride = { light: false, dark: false };
}
function storeCurrentEdits() {
editState[selectedMode] = {
bg: els.rows.bg.hex.value,
border: els.rows.border.hex.value,
text: els.rows.text.hex.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.style.backgroundColor = val;
els.rows[prop].hex.value = val;
els.rows[prop].hex.classList.remove("rt-input-error");
}
}
function updatePreview() {
var bg = els.rows.bg.hex.value;
var border = els.rows.border.hex.value;
var text = els.rows.text.hex.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;
resetTracking();
var existingEntry = settings.subredditColors[subredditName];
hadOverride.light = !!(existingEntry && existingEntry.light);
hadOverride.dark = !!(existingEntry && existingEntry.dark);
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;
closePicker();
applyDarkModeToPopover(settings);
loadModeIntoInputs();
updatePreview();
positionPopover(badgeEl);
}
function close() {
if (!popoverEl) return;
closePicker();
popoverEl.classList.remove("rt-popover-visible");
currentSubreddit = null;
currentAnchor = null;
editState = { light: null, dark: null };
resetTracking();
}
async function save() {
if (!currentSubreddit) return;
storeCurrentEdits();
var settings = await window.RedditTweaks.loadSettings();
var entry = settings.subredditColors[currentSubreddit] || {};
if (dirtyModes.light || hadOverride.light) {
entry.light = editState.light;
}
if (dirtyModes.dark || hadOverride.dark) {
entry.dark = editState.dark;
}
if (!entry.light && !entry.dark) {
delete settings.subredditColors[currentSubreddit];
} else {
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);
resetTracking();
editState.light = getDefaultsForMode(settings, "light");
editState.dark = getDefaultsForMode(settings, "dark");
closePicker();
loadModeIntoInputs();
updatePreview();
}
function refreshTheme(settings) {
if (!popoverEl || !popoverEl.classList.contains("rt-popover-visible")) return;
applyDarkModeToPopover(settings);
}
window.RedditTweaks = window.RedditTweaks || {};
window.RedditTweaks.colorPopover = {
open: open,
close: close,
refreshTheme: refreshTheme,
};
})();
```
- [ ] **Step 3: Verify manually**
Load `old.reddit.com`. Open the inline color popover by hovering a subreddit badge and clicking the gear. Verify:
1. Color rows show swatch divs (colored rectangles) instead of native color pickers.
2. Clicking a swatch opens the tabbed picker below the rows.
3. Palette tab: clicking a Tailwind swatch updates the row's swatch + hex input + preview badge.
4. Named tab: clicking a named color works the same.
5. Custom tab: dragging the SV square and hue bar updates live.
6. Clicking the same swatch toggles the picker closed.
7. Clicking a different swatch switches the picker to that property.
8. Typing a valid hex updates the picker's state.
9. Mode toggle closes the picker and loads the other mode's colors.
10. Cancel/Escape/click-outside dismisses the popover cleanly.
11. Save persists the colors.
12. Dark mode styling works (if RES night mode is available).
- [ ] **Step 4: Commit**
```bash
git add content/tweaks/color-popover.js content/styles/badges.css
git commit -m "feat: integrate tabbed color picker into inline popover"
```
---
### Task 4: Popup Integration
**Files:**
- Modify: `popup/popup.html:72-73`
- Modify: `popup/popup.js`
- Modify: `popup/popup.css:85-92`
**Interfaces:**
- Consumes: `window.RedditTweaks.colorPicker.create()` → `{ el, open, close, setColor, isOpen, destroy }`
- Produces: Updated popup with custom picker for all global and per-subreddit color controls
- [ ] **Step 1: Add script tag to `popup.html`**
Add the color-picker script before popup.js:
```html
<script src="../lib/settings.js"></script>
<script src="../content/tweaks/color-picker.js"></script>
<script src="popup.js"></script>
```
- [ ] **Step 2: Replace `<input type="color">` with swatch divs in `popup.html`**
Replace all 12 `<input type="color" ...>` elements with `<div>` elements. Each keeps its `id` attribute.
Light Mode Colors section:
```html
<div class="section">
<h2>Light Mode Colors</h2>
<fieldset>
<legend>Subreddit Badge</legend>
<label>Background <div class="rt-color-swatch" id="light.subredditBgColor"></div></label>
<label>Border <div class="rt-color-swatch" id="light.subredditBorderColor"></div></label>
<label>Text <div class="rt-color-swatch" id="light.subredditTextColor"></div></label>
</fieldset>
<fieldset>
<legend>Comments Badge</legend>
<label>Background <div class="rt-color-swatch" id="light.commentsBgColor"></div></label>
<label>Border <div class="rt-color-swatch" id="light.commentsBorderColor"></div></label>
<label>Text <div class="rt-color-swatch" id="light.commentsTextColor"></div></label>
</fieldset>
<button id="resetLight" class="btn-secondary">Reset Light Defaults</button>
</div>
```
Dark Mode Colors section:
```html
<div class="section">
<h2>Dark Mode Colors</h2>
<fieldset>
<legend>Subreddit Badge</legend>
<label>Background <div class="rt-color-swatch" id="dark.subredditBgColor"></div></label>
<label>Border <div class="rt-color-swatch" id="dark.subredditBorderColor"></div></label>
<label>Text <div class="rt-color-swatch" id="dark.subredditTextColor"></div></label>
</fieldset>
<fieldset>
<legend>Comments Badge</legend>
<label>Background <div class="rt-color-swatch" id="dark.commentsBgColor"></div></label>
<label>Border <div class="rt-color-swatch" id="dark.commentsBorderColor"></div></label>
<label>Text <div class="rt-color-swatch" id="dark.commentsTextColor"></div></label>
</fieldset>
<button id="resetDark" class="btn-secondary">Reset Dark Defaults</button>
</div>
```
- [ ] **Step 3: Update swatch CSS selector in `popup.css`**
Replace:
```css
fieldset input[type="color"] {
width: 40px;
height: 24px;
border: 1px solid #ccc;
border-radius: 3px;
padding: 1px;
cursor: pointer;
}
```
with:
```css
fieldset .rt-color-swatch {
width: 40px;
height: 24px;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
display: inline-block;
vertical-align: middle;
}
```
- [ ] **Step 4: Rewrite `popup.js`**
Replace the entire file. Key changes from the previous version:
- Add `getColorValue(el)` / `setColorValue(el, val)` helpers for reading/writing swatch div values
- Create a shared picker instance
- Wire swatch click handlers for global colors in `init()`
- Replace `el.value` reads/writes with helper calls for color elements
- Replace `<input type="color">` creation in `addSubredditEntry()` with swatch divs
- Replace `el.addEventListener("input", saveFromControls)` for color elements with picker-based flow
Full replacement content for `popup/popup.js`:
```javascript
(function () {
"use strict";
var picker = null;
var activeSwatchEl = null;
var COLOR_KEYS = [
"light.subredditBgColor",
"light.subredditBorderColor",
"light.subredditTextColor",
"light.commentsBgColor",
"light.commentsBorderColor",
"light.commentsTextColor",
"dark.subredditBgColor",
"dark.subredditBorderColor",
"dark.subredditTextColor",
"dark.commentsBgColor",
"dark.commentsBorderColor",
"dark.commentsTextColor",
];
function colorToHex(val) {
if (!val) return "#ffffff";
return val;
}
function getColorValue(el) {
return el.dataset.value || "#ffffff";
}
function setColorValue(el, val) {
el.dataset.value = val;
el.style.backgroundColor = val;
}
function openPickerForSwatch(swatchEl) {
if (!picker) {
picker = window.RedditTweaks.colorPicker.create();
}
if (picker.isOpen() && activeSwatchEl === swatchEl) {
closePicker();
return;
}
closePicker();
activeSwatchEl = swatchEl;
var parent = swatchEl.closest("label") || swatchEl.parentNode;
parent.parentNode.insertBefore(picker.el, parent.nextSibling);
picker.open(getColorValue(swatchEl), function (hex) {
setColorValue(swatchEl, hex);
saveFromControls();
});
}
function closePicker() {
if (picker && picker.isOpen()) picker.close();
activeSwatchEl = null;
}
function populateControls(settings) {
document.getElementById("enabled").checked = settings.enabled;
document.getElementById("darkMode").value = settings.darkMode;
COLOR_KEYS.forEach(function (key) {
var el = document.getElementById(key);
if (el) setColorValue(el, colorToHex(settings[key]));
});
renderSubredditOverrides(settings.subredditColors || {});
}
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 () {
closePicker();
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 bgSwatch = document.createElement("div");
bgSwatch.className = "rt-color-swatch";
setColorValue(bgSwatch, colorToHex(lightColors.bg));
bgSwatch.title = "Background";
var borderSwatch = document.createElement("div");
borderSwatch.className = "rt-color-swatch";
setColorValue(borderSwatch, colorToHex(lightColors.border));
borderSwatch.title = "Border";
var textSwatch = document.createElement("div");
textSwatch.className = "rt-color-swatch";
setColorValue(textSwatch, colorToHex(lightColors.text));
textSwatch.title = "Text";
colorsDiv.appendChild(bgSwatch);
colorsDiv.appendChild(borderSwatch);
colorsDiv.appendChild(textSwatch);
entry._colorState = {
light: { bg: getColorValue(bgSwatch), border: getColorValue(borderSwatch), text: getColorValue(textSwatch) },
dark: { bg: colorToHex(darkColors.bg), border: colorToHex(darkColors.border), text: colorToHex(darkColors.text) },
};
entry._hadOverride = {
light: !!(modeColors && modeColors.light),
dark: !!(modeColors && modeColors.dark),
};
entry._dirtyModes = { light: false, dark: false };
modeSelect.addEventListener("change", function () {
closePicker();
var prev = modeSelect.value === "dark" ? "light" : "dark";
entry._dirtyModes[prev] = true;
entry._colorState[prev] = { bg: getColorValue(bgSwatch), border: getColorValue(borderSwatch), text: getColorValue(textSwatch) };
var next = entry._colorState[modeSelect.value];
setColorValue(bgSwatch, next.bg);
setColorValue(borderSwatch, next.border);
setColorValue(textSwatch, next.text);
});
[bgSwatch, borderSwatch, textSwatch].forEach(function (swatch) {
swatch.addEventListener("click", function () {
openPickerForSwatch(swatch);
});
});
nameInput.addEventListener("change", function () {
saveFromControls();
});
entry.appendChild(header);
entry.appendChild(modeRow);
entry.appendChild(colorsDiv);
entry._nameInput = nameInput;
entry._bgSwatch = bgSwatch;
entry._borderSwatch = borderSwatch;
entry._textSwatch = textSwatch;
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: getColorValue(entry._bgSwatch),
border: getColorValue(entry._borderSwatch),
text: getColorValue(entry._textSwatch),
};
var entryOverrides = {};
if (entry._hadOverride.light || entry._dirtyModes.light) {
entryOverrides.light = entry._colorState.light;
}
if (entry._hadOverride.dark || entry._dirtyModes.dark) {
entryOverrides.dark = entry._colorState.dark;
}
if (entryOverrides.light || entryOverrides.dark) {
overrides[name] = entryOverrides;
}
});
return overrides;
}
async function saveFromControls() {
var settings = {
enabled: document.getElementById("enabled").checked,
darkMode: document.getElementById("darkMode").value,
subredditColors: collectSubredditOverrides(),
};
COLOR_KEYS.forEach(function (key) {
var el = document.getElementById(key);
if (el) settings[key] = getColorValue(el);
});
try {
await window.RedditTweaks.saveSettings(settings);
} catch (err) {
console.error("Reddit Tweaks: failed to save settings", err);
}
}
async function init() {
var settings = await window.RedditTweaks.loadSettings();
populateControls(settings);
document.getElementById("enabled").addEventListener("change", saveFromControls);
document.getElementById("darkMode").addEventListener("change", saveFromControls);
COLOR_KEYS.forEach(function (key) {
var el = document.getElementById(key);
if (el) {
el.addEventListener("click", function () {
openPickerForSwatch(el);
});
}
});
document.getElementById("addSubreddit").addEventListener("click", function () {
closePicker();
addSubredditEntry("", {});
});
document.getElementById("resetLight").addEventListener("click", async function () {
closePicker();
var settings = await window.RedditTweaks.loadSettings();
var 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 () {
closePicker();
var settings = await window.RedditTweaks.loadSettings();
var 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?")) {
closePicker();
await window.RedditTweaks.resetSettings();
var settings = await window.RedditTweaks.loadSettings();
populateControls(settings);
}
});
}
init();
})();
```
- [ ] **Step 5: Add swatch styles for subreddit entries to `popup.css`**
The subreddit entry swatches need sizing. Add after the `.subreddit-entry-colors` block:
```css
.subreddit-entry-colors .rt-color-swatch {
width: 40px;
height: 24px;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
}
```
- [ ] **Step 6: Verify manually**
Open the extension popup (click the toolbar icon). Verify:
1. All global color controls show swatch divs (colored rectangles) instead of native color pickers.
2. Clicking a global swatch opens the tabbed picker below it.
3. All three tabs work: Palette, Named Colors, Custom.
4. Selecting a color updates the swatch and saves settings.
5. Per-subreddit entries also use swatch divs.
6. Clicking a per-subreddit swatch opens the picker.
7. Mode toggle in per-subreddit entries closes any open picker.
8. Adding a new subreddit entry closes any open picker.
9. Reset buttons work and close any open picker.
10. Only one picker is open at a time across all swatches.
- [ ] **Step 7: Commit**
```bash
git add popup/popup.html popup/popup.js popup/popup.css
git commit -m "feat: integrate tabbed color picker into extension popup"
```