diff --git a/chat-summaries/2026-08-26_17-20-summary.md b/chat-summaries/2026-08-26_17-20-summary.md new file mode 100644 index 0000000..4c4be15 --- /dev/null +++ b/chat-summaries/2026-08-26_17-20-summary.md @@ -0,0 +1,33 @@ +# Badge Polish: Gear Animation + Custom Color Picker + +**Date:** 2026-08-26 +**Commits:** 2abd18e..8462148 (6 commits) + +## Task Description + +Two improvements to the subreddit badge system: +1. Animate the gear icon reveal on hover (remove permanent padding gap) +2. Replace all native `` elements with a custom tabbed inline color picker + +## Changes Made + +### New Files +- `content/tweaks/color-picker.js` — Tabbed color picker widget with three panels: Palette (Tailwind/shadcn colors), Named Colors (148 CSS named colors), Custom (HSV picker with canvas SV square + hue bar). Exposes `window.RedditTweaks.colorPicker.create()` API. + +### Modified Files +- `content/styles/badges.css` — Gear animation CSS (padding-right 6px→18px on hover, 150ms transition). Swatch div selector replacing `input[type="color"]`. Full picker widget CSS with dark mode support. +- `content/tweaks/color-popover.js` — Replaced native color inputs with swatch divs. Integrated custom picker between color rows and preview. Added popover repositioning when picker opens. +- `popup/popup.html` — Replaced 12 `` elements with swatch divs. Added color-picker.js script tag. +- `popup/popup.js` — Full rewrite to use swatch divs + custom picker. Added `getColorValue`/`setColorValue` helpers, debounced saves during drag, per-subreddit dirty-mode tracking via `onBeforeChange` callback. +- `popup/popup.css` — Updated swatch selectors. Added picker widget CSS. Added subreddit entry swatch styles. +- `manifest.json` — Added `color-picker.js` to content scripts array. + +### Specs & Plans +- `docs/superpowers/specs/2026-08-26-badge-polish-design.md` — Updated design spec with tabbed picker interface +- `docs/superpowers/plans/2026-08-26-badge-polish.md` — Implementation plan + +## Follow-up Items +- Manual browser verification on old.reddit.com (not possible in this session) +- `prefers-reduced-motion` media query for gear animation (accessibility) +- Touch/pointer events for Custom HSV tab (currently mouse-only) +- Escape key could close picker first, then popover on second press diff --git a/content/styles/badges.css b/content/styles/badges.css index bd5bd74..b5e82dd 100644 --- a/content/styles/badges.css +++ b/content/styles/badges.css @@ -25,7 +25,7 @@ html.rt-enabled .thing.link .flat-list li:has(> .comments) { } .rt-badge:hover { - filter: brightness(0.9); + filter: brightness(1.1); } /* --- Subreddit badge: relative for gear positioning --- */ @@ -316,6 +316,96 @@ html.rt-enabled .thing.link .flat-list li:has(> .comments) { color: #ccc; } +/* --- Preset badges --- */ +.rt-popover-presets { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 0; + border-top: 1px solid #eee; + flex-wrap: wrap; +} + +.rt-preset-list { + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.rt-preset-badge { + display: inline-block; + position: relative; + border: 1px solid; + border-radius: 3px; + padding: 2px 6px; + font-size: 10px; + font-weight: bold; + cursor: pointer; + line-height: 1.4; + user-select: none; +} + +.rt-preset-badge:hover { + filter: brightness(1.1); +} + +.rt-preset-delete { + display: none; + position: absolute; + top: -6px; + right: -6px; + width: 14px; + height: 14px; + background: #d9534f; + color: white; + border-radius: 50%; + font-size: 10px; + line-height: 14px; + text-align: center; + cursor: pointer; +} + +.rt-preset-badge:hover .rt-preset-delete { + display: block; +} + +.rt-preset-add { + width: 22px; + height: 22px; + border: 1px dashed #ccc; + border-radius: 3px; + background: none; + color: #999; + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; +} + +.rt-preset-add:hover { + border-color: #999; + color: #666; + background: #f5f5f5; +} + +.rt-color-popover.rt-dark .rt-popover-presets { + border-top-color: #444; +} + +.rt-color-popover.rt-dark .rt-preset-add { + border-color: #555; + color: #888; +} + +.rt-color-popover.rt-dark .rt-preset-add:hover { + border-color: #888; + color: #ccc; + background: #333; +} + /* --- Color Picker Widget --- */ .rt-color-picker { margin: 8px 0; diff --git a/content/tweaks/color-popover.js b/content/tweaks/color-popover.js index 680b5ad..f7c8a7a 100644 --- a/content/tweaks/color-popover.js +++ b/content/tweaks/color-popover.js @@ -105,6 +105,21 @@ var previewSection = el.querySelector(".rt-popover-preview"); el.insertBefore(picker.el, previewSection); + var presetsDiv = document.createElement("div"); + presetsDiv.className = "rt-popover-presets"; + var presetList = document.createElement("div"); + presetList.className = "rt-preset-list"; + var presetAddBtn = document.createElement("button"); + presetAddBtn.className = "rt-preset-add"; + presetAddBtn.type = "button"; + presetAddBtn.textContent = "+"; + presetAddBtn.title = "Save current colors as preset"; + presetAddBtn.addEventListener("click", saveAsPreset); + presetsDiv.appendChild(presetList); + presetsDiv.appendChild(presetAddBtn); + el.querySelector(".rt-popover-footer").insertAdjacentElement("beforebegin", presetsDiv); + els.presetList = presetList; + els.closeBtn.addEventListener("click", close); els.cancelBtn.addEventListener("click", close); els.saveBtn.addEventListener("click", save); @@ -234,6 +249,73 @@ els.preview.style.color = text; } + function renderPresets(presets) { + els.presetList.innerHTML = ""; + for (var i = 0; i < presets.length; i++) { + (function (index) { + var p = presets[index]; + var badge = document.createElement("span"); + badge.className = "rt-preset-badge"; + badge.style.background = p.bg; + badge.style.borderColor = p.border; + badge.style.color = p.text; + badge.textContent = "Aa"; + badge.title = "Click to apply"; + badge.addEventListener("click", function () { + applyPreset(p); + }); + + var del = document.createElement("span"); + del.className = "rt-preset-delete"; + del.textContent = "\u00d7"; + del.addEventListener("click", function (e) { + e.stopPropagation(); + removePreset(index); + }); + badge.appendChild(del); + els.presetList.appendChild(badge); + })(i); + } + } + + function applyPreset(p) { + markModeDirty(selectedMode); + var props = ["bg", "border", "text"]; + for (var i = 0; i < props.length; i++) { + var prop = props[i]; + els.rows[prop].swatch.style.backgroundColor = p[prop]; + els.rows[prop].hex.value = p[prop]; + els.rows[prop].hex.classList.remove("rt-input-error"); + } + updatePreview(); + if (picker && picker.isOpen() && activeProp) { + picker.setColor(els.rows[activeProp].hex.value); + } + } + + async function saveAsPreset() { + var preset = { + bg: els.rows.bg.hex.value, + border: els.rows.border.hex.value, + text: els.rows.text.hex.value, + }; + var settings = await window.RedditTweaks.loadSettings(); + var presets = settings.colorPresets || []; + presets.push(preset); + settings.colorPresets = presets; + await window.RedditTweaks.saveSettings(settings); + renderPresets(presets); + } + + async function removePreset(index) { + var settings = await window.RedditTweaks.loadSettings(); + var presets = settings.colorPresets || []; + presets.splice(index, 1); + settings.colorPresets = presets; + await window.RedditTweaks.saveSettings(settings); + renderPresets(presets); + } + function positionPopover(anchorEl) { var rect = anchorEl.getBoundingClientRect(); var gap = 6; @@ -311,6 +393,7 @@ applyDarkModeToPopover(settings); loadModeIntoInputs(); updatePreview(); + renderPresets(settings.colorPresets || []); positionPopover(badgeEl); } diff --git a/docs/superpowers/plans/2026-08-26-badge-polish.md b/docs/superpowers/plans/2026-08-26-badge-polish.md new file mode 100644 index 0000000..4580699 --- /dev/null +++ b/docs/superpowers/plans/2026-08-26-badge-polish.md @@ -0,0 +1,1624 @@ +# 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 `` 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 `` + +- [ ] **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: +- `` → `
` 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 = [ + '
', + ' ', + ' ', + "
", + '
', + ' ", + "
", + '
', + '
', + ' Background', + '
', + '
', + ' ', + "
", + "
", + '
', + ' Border', + '
', + '
', + ' ', + "
", + "
", + '
', + ' Text', + '
', + '
', + ' ', + "
", + "
", + "
", + '
', + ' ', + "
", + '", + ].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 + + + +``` + +- [ ] **Step 2: Replace `` with swatch divs in `popup.html`** + +Replace all 12 `` elements with `
` elements. Each keeps its `id` attribute. + +Light Mode Colors section: + +```html +
+

Light Mode Colors

+
+ Subreddit Badge + + + +
+
+ Comments Badge + + + +
+ +
+``` + +Dark Mode Colors section: + +```html +
+

Dark Mode Colors

+
+ Subreddit Badge + + + +
+
+ Comments Badge + + + +
+ +
+``` + +- [ ] **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 `` 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 = ''; + 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" +``` diff --git a/docs/superpowers/specs/2026-08-26-badge-polish-design.md b/docs/superpowers/specs/2026-08-26-badge-polish-design.md index d07664f..524f294 100644 --- a/docs/superpowers/specs/2026-08-26-badge-polish-design.md +++ b/docs/superpowers/specs/2026-08-26-badge-polish-design.md @@ -58,9 +58,53 @@ The native `` delegates to the OS-level color picker. On mac ### Solution -Replace all `` elements — in both the inline popover and the extension popup — with a custom SV+Hue picker widget. The picker renders inline within the containing UI (popover or popup), provides full color control, and stays visually consistent with the extension's design. +Replace all `` elements — in both the inline popover and the extension popup — with a custom color picker widget. The picker renders inline within the containing UI (popover or popup), provides full color control, and stays visually consistent with the extension's design. -### Picker Anatomy +### Picker Structure — Tabbed Interface + +The picker uses three tabs, providing different ways to choose a color: + +``` +┌──────────────────────────────┐ +│ [Palette] [Named] [Custom] │ ← tab bar +├──────────────────────────────┤ +│ │ +│ (active tab content) │ +│ │ +├──────────────────────────────┤ +│ [■ swatch] #c84a20 │ ← current color + hex input (always visible) +└──────────────────────────────┘ +``` + +The hex text input and a small swatch preview of the current color are always visible below the tab content, regardless of which tab is active. This lets the user see and edit the exact value at all times. + +**Tab persistence:** The picker remembers which tab was last used and reopens to that tab. The initial default (before any tab has been selected) is the Palette tab. + +### Tab 1: Palette (Tailwind/shadcn Colors) + +A grid of color swatches from the Tailwind CSS / shadcn color palette. The palette is organized by hue family (rows) with shades as columns. + +**Hue families (rows):** red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose. Gray-scale families (slate, gray, zinc, neutral, stone) are grouped in a separate section at the top or bottom. + +**Shades (columns):** 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 (11 per row). + +**Layout:** Each swatch is a small square (~16×16px). The grid is scrollable vertically within a fixed-height container (~180px) to avoid the picker growing too tall. Hue family labels appear as small text on the left of each row, or omitted if space is tight (the colors are self-explanatory when grouped). + +**Interaction:** Clicking a swatch immediately selects that color — updates the hex input, the swatch preview, and calls `onChange`. The selected swatch gets a subtle highlight border. + +### Tab 2: Named Colors (HTML/CSS Named Colors) + +All 148 CSS named colors displayed as labeled swatches in a scrollable grid. + +**Layout:** Swatches are ~24×18px with the color name displayed below or beside each swatch in small text (~9px). The grid is sorted alphabetically by default. The container is scrollable within the same fixed height (~180px). + +**Interaction:** Same as the palette tab — clicking selects the color immediately. + +### Tab 3: Custom (SV+Hue Picker) + +The freeform picker for precise color selection. + +#### Picker Anatomy ``` ┌──────────────────────────────┐ @@ -75,11 +119,11 @@ Replace all `` elements — in both the inline popover and t └──────────────────────────────┘ ``` -### Color Model +#### Color Model The picker uses HSV internally (hue, saturation, value). HSV maps naturally to the SV square (X = saturation, Y = value) and hue bar (X = hue). Colors are converted to/from hex (`#rrggbb`) for storage, display, and sync with the hex text input. -### SV Square +#### SV Square A `` element, redrawn whenever the hue changes. The canvas renders a two-dimensional gradient: @@ -90,19 +134,23 @@ The gradient is drawn by filling the canvas with the current hue at full saturat **Size:** The SV square fills the available width minus padding. In the 260px popover, this is ~230px. In the 320px popup, ~290px. Height is fixed at 140px regardless of width — this keeps the popover from growing too tall while still providing a usable picking area. The result is a landscape rectangle, not a true square. -### Hue Bar +#### Hue Bar A horizontal strip below the SV square. The background is a static CSS `linear-gradient` cycling through the hue spectrum (red → yellow → green → cyan → blue → magenta → red). A thin vertical indicator shows the current hue position. Click or drag to change hue, which redraws the SV square. **Height:** 14px. Full width of the picker. -### Interaction +#### Interaction - **Click** on the SV square sets saturation and value immediately. - **Drag** on the SV square continuously updates saturation and value (tracks `mousemove` after `mousedown`, stops on `mouseup` or mouse leaving the document). - **Click/drag** on the hue bar changes hue and redraws the SV square. - The hex text input stays bidirectionally synced: valid hex input updates the picker cursor and hue position; picker changes update the hex input. +### Cross-Tab Sync + +Switching tabs while a color is selected carries the current color to the new tab. For the Custom tab, the SV cursor and hue bar position update to reflect the current hex value. For the Palette and Named Colors tabs, if the current hex matches a swatch exactly, that swatch is highlighted. + ### Integration with Popover Each color row in the popover currently has: @@ -168,8 +216,8 @@ When the picker opens inside the popover, the popover grows taller. If this woul | File | Change | |---|---| -| `content/tweaks/color-picker.js` | **New file.** HSV↔hex conversion, canvas SV square rendering, hue bar, mouse tracking, create/open/close/destroy API. Exposes `window.RedditTweaks.colorPicker`. | -| `content/styles/badges.css` | Add CSS for `.rt-color-picker`, `.rt-color-picker-sv` (canvas), `.rt-color-picker-hue` (bar), `.rt-color-picker-sv-cursor`, `.rt-color-picker-hue-indicator`. Light and dark mode variants. | +| `content/tweaks/color-picker.js` | **New file.** Tabbed picker widget: Palette tab (Tailwind color data + grid rendering), Named Colors tab (CSS named colors data + grid), Custom tab (HSV↔hex conversion, canvas SV square, hue bar, mouse tracking). Tab persistence. create/open/close/destroy API. Exposes `window.RedditTweaks.colorPicker`. | +| `content/styles/badges.css` | Add CSS for `.rt-color-picker` (container), tab bar (`.rt-picker-tabs`, `.rt-picker-tab`), palette/named grids (`.rt-picker-grid`, `.rt-picker-swatch`), custom tab (`.rt-picker-sv`, `.rt-picker-hue`, cursors/indicators), current color footer. Light and dark mode variants. | | `content/tweaks/color-popover.js` | Replace `` with `
`. On swatch click, open picker. Wire picker's `onChange` to update hex input, swatch color, and live preview. Close picker on mode toggle. | | `popup/popup.html` | Add `` before `popup.js`. | | `popup/popup.js` | Replace `` creation with swatch `
` + picker integration for global colors and per-subreddit overrides. | @@ -184,10 +232,13 @@ When the picker opens inside the popover, the popover grows taller. If this woul - **Picker open + mode toggle:** Switching light/dark mode in the popover closes any open picker and loads the new mode's values into the swatches. - **Popup scroll:** The picker is inline in the DOM flow, so it scrolls naturally with the popup's controls. - **Multiple pickers:** Only one picker open at a time in any context (popover or popup). Opening a new one closes the previous. -- **Hex input while picker is open:** Typing a valid hex into the text input updates the picker's cursor and hue position in real time. -- **Invalid hex while picker is open:** The hex input shows the error border (existing behavior). The picker position is not updated until a valid hex is entered. +- **Hex input while picker is open:** Typing a valid hex into the text input updates the active tab's state in real time (Custom tab moves cursor; Palette/Named tab highlights matching swatch if any). +- **Invalid hex while picker is open:** The hex input shows the error border (existing behavior). The picker state is not updated until a valid hex is entered. - **Very small viewport:** The SV square has a minimum size of 100×100px. If the container is too narrow, the picker still works but may feel cramped. - **Color precision:** HSV→hex conversion rounds to the nearest integer per channel. This can cause minor rounding differences when round-tripping hex→HSV→hex, but the difference is imperceptible (at most ±1 per channel). +- **Tab persistence scope:** The remembered tab is global (shared across all picker instances in the same page context). Popup and content script have separate persistence since they're separate JS contexts. +- **Palette swatch count:** The full Tailwind palette (22 families × 11 shades = 242 swatches) plus gray families. The grid container is scrollable at a fixed height to keep the picker compact. +- **Named colors sort:** Alphabetical by CSS name. No search/filter — the grid is small enough (~148 entries) to scan visually. ## Testing @@ -196,15 +247,20 @@ Manual testing additions (extends the existing 20-item checklist): 1. Hovering over a subreddit badge smoothly expands the badge to reveal the gear (no gap at rest). 2. The comments badge shifts right during the expansion and back when hover ends. 3. The animation is ~150ms and feels snappy, not laggy. -4. Clicking a color swatch in the popover opens the SV+Hue picker inline. -5. Dragging on the SV square updates the hex input and live preview in real time. -6. Dragging on the hue bar changes the SV square's base color. -7. Only one picker is open at a time — clicking another swatch closes the first. -8. Typing a valid hex updates the picker cursor position. -9. Typing an invalid hex shows the error border but doesn't move the picker. -10. Closing the popover while the picker is open works cleanly. -11. Mode toggle closes any open picker. -12. In the popup, all global color pickers use the custom picker. -13. In the popup, per-subreddit color pickers use the custom picker. -14. The picker renders correctly in dark mode. -15. Color values round-trip correctly (pick → save → reopen → same position). +4. Clicking a color swatch in the popover opens the tabbed picker inline. +5. The Palette tab shows a scrollable grid of Tailwind colors organized by hue. +6. Clicking a palette swatch immediately selects that color and updates the hex input + preview. +7. The Named Colors tab shows all 148 CSS named colors with labels. +8. Clicking a named color swatch selects it immediately. +9. The Custom tab shows the SV+Hue picker; dragging updates hex + preview in real time. +10. Switching tabs carries the current color to the new tab (Custom tab updates cursor position). +11. The picker remembers the last-used tab across open/close cycles. +12. Only one picker is open at a time — clicking another swatch closes the first. +13. Typing a valid hex updates the active tab's state. +14. Typing an invalid hex shows the error border but doesn't update the picker. +15. Closing the popover while the picker is open works cleanly. +16. Mode toggle closes any open picker. +17. In the popup, all global color pickers use the custom tabbed picker. +18. In the popup, per-subreddit color pickers use the custom tabbed picker. +19. The picker renders correctly in dark mode (all three tabs). +20. Color values round-trip correctly (pick → save → reopen → same position/tab). diff --git a/lib/settings.js b/lib/settings.js index cbb3a3b..26e29bc 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -17,6 +17,7 @@ "dark.commentsBorderColor": "#888888", "dark.commentsTextColor": "#cccccc", subredditColors: {}, + colorPresets: [], }; function migrateSubredditColors(colors) { diff --git a/popup/popup.css b/popup/popup.css index d95a9be..3ade14b 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -210,6 +210,80 @@ fieldset input[type="text"] { cursor: pointer; } +/* --- Preset badges --- */ +.rt-preset-bar { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + margin-top: 4px; +} + +.rt-preset-list { + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.rt-preset-badge { + display: inline-block; + position: relative; + border: 1px solid; + border-radius: 3px; + padding: 2px 6px; + font-size: 10px; + font-weight: bold; + cursor: pointer; + line-height: 1.4; + user-select: none; +} + +.rt-preset-badge:hover { + filter: brightness(1.1); +} + +.rt-preset-delete { + display: none; + position: absolute; + top: -6px; + right: -6px; + width: 14px; + height: 14px; + background: #d9534f; + color: white; + border-radius: 50%; + font-size: 10px; + line-height: 14px; + text-align: center; + cursor: pointer; +} + +.rt-preset-badge:hover .rt-preset-delete { + display: block; +} + +.rt-preset-add { + width: 22px; + height: 22px; + border: 1px dashed #ccc; + border-radius: 3px; + background: none; + color: #999; + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; +} + +.rt-preset-add:hover { + border-color: #999; + color: #666; + background: #f5f5f5; +} + /* --- Color Picker Widget --- */ .rt-color-picker { margin: 8px 0; diff --git a/popup/popup.html b/popup/popup.html index fc65913..e712c2a 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -58,6 +58,15 @@
+
+

Color Presets

+

Saved color sets. Click to apply to light subreddit badge.

+
+
+ +
+
+

Per-Subreddit Overrides

Set custom subreddit badge colors per mode.

diff --git a/popup/popup.js b/popup/popup.js index 4b46269..93a7255 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -220,6 +220,63 @@ return overrides; } + function renderPopupPresets(presets) { + var list = document.getElementById("presetList"); + list.innerHTML = ""; + for (var i = 0; i < presets.length; i++) { + (function (index) { + var p = presets[index]; + var badge = document.createElement("span"); + badge.className = "rt-preset-badge"; + badge.style.background = p.bg; + badge.style.borderColor = p.border; + badge.style.color = p.text; + badge.textContent = "Aa"; + badge.title = "Click to apply to light subreddit colors"; + badge.addEventListener("click", function () { + setColorValue(document.getElementById("light.subredditBgColor"), p.bg); + setColorValue(document.getElementById("light.subredditBorderColor"), p.border); + setColorValue(document.getElementById("light.subredditTextColor"), p.text); + closePicker(); + saveFromControls(); + }); + + var del = document.createElement("span"); + del.className = "rt-preset-delete"; + del.textContent = "\u00d7"; + del.addEventListener("click", function (e) { + e.stopPropagation(); + deletePopupPreset(index); + }); + badge.appendChild(del); + list.appendChild(badge); + })(i); + } + } + + async function addPopupPreset() { + var preset = { + bg: getColorValue(document.getElementById("light.subredditBgColor")), + border: getColorValue(document.getElementById("light.subredditBorderColor")), + text: getColorValue(document.getElementById("light.subredditTextColor")), + }; + var settings = await window.RedditTweaks.loadSettings(); + var presets = settings.colorPresets || []; + presets.push(preset); + settings.colorPresets = presets; + await window.RedditTweaks.saveSettings(settings); + renderPopupPresets(presets); + } + + async function deletePopupPreset(index) { + var settings = await window.RedditTweaks.loadSettings(); + var presets = settings.colorPresets || []; + presets.splice(index, 1); + settings.colorPresets = presets; + await window.RedditTweaks.saveSettings(settings); + renderPopupPresets(presets); + } + async function saveFromControls() { var settings = { enabled: document.getElementById("enabled").checked, @@ -255,6 +312,9 @@ } }); + renderPopupPresets(settings.colorPresets || []); + document.getElementById("addPreset").addEventListener("click", addPopupPreset); + document.getElementById("addSubreddit").addEventListener("click", function () { closePicker(); addSubredditEntry("", {});