- Add global color presets: save/apply/delete sets of {bg, border, text}
colors as reusable presets, shown as mini badges in their own colors.
- Popover: preset badges under preview with + to save, click to apply,
× on hover to delete.
- Popup: dedicated Color Presets section with same functionality,
applying to light subreddit badge colors.
- Storage: colorPresets array added to settings defaults.
- Fix badge hover: change brightness(0.9) to brightness(1.1) so hover
brightens (activates) instead of dimming.
Co-authored-by: Cursor <cursoragent@cursor.com>
1625 lines
53 KiB
Markdown
1625 lines
53 KiB
Markdown
# 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"
|
|
```
|