Files
reddit-tweaks-ffe/docs/superpowers/plans/2026-08-26-badge-polish.md
cottongin 48db2fc70e feat: add color presets + fix badge hover effect
- 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>
2026-08-26 18:00:54 -04:00

53 KiB

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:

.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:

.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
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

(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:

/* --- 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):

"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
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:

.rt-popover-row input[type="color"] {
  width: 28px;
  height: 20px;
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 1px;
  cursor: pointer;
}

with:

.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:

.rt-color-popover.rt-dark .rt-popover-row input[type="color"] {
  border-color: #555;
}

with:

.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:

(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
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:

  <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:

    <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:

    <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:

fieldset input[type="color"] {
  width: 40px;
  height: 24px;
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 1px;
  cursor: pointer;
}

with:

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:

(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:

.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
git add popup/popup.html popup/popup.js popup/popup.css
git commit -m "feat: integrate tabbed color picker into extension popup"