From a3567005bd2486e2fe117fdbff7904822ec4123e Mon Sep 17 00:00:00 2001 From: cottongin Date: Wed, 26 Aug 2026 17:11:53 -0400 Subject: [PATCH] feat: add tabbed color picker widget (palette, named colors, custom HSV) Co-authored-by: Cursor --- content/styles/badges.css | 204 +++++++++++++++++ content/tweaks/color-picker.js | 385 +++++++++++++++++++++++++++++++++ manifest.json | 1 + popup/popup.css | 204 +++++++++++++++++ 4 files changed, 794 insertions(+) create mode 100644 content/tweaks/color-picker.js diff --git a/content/styles/badges.css b/content/styles/badges.css index da9a253..40abbeb 100644 --- a/content/styles/badges.css +++ b/content/styles/badges.css @@ -333,3 +333,207 @@ html.rt-enabled .thing.link .flat-list li:has(> .comments) { background: #333; color: #ccc; } + +/* --- 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; +} diff --git a/content/tweaks/color-picker.js b/content/tweaks/color-picker.js new file mode 100644 index 0000000..609a325 --- /dev/null +++ b/content/tweaks/color-picker.js @@ -0,0 +1,385 @@ +(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 }; +})(); diff --git a/manifest.json b/manifest.json index d6a072c..4ef1e29 100644 --- a/manifest.json +++ b/manifest.json @@ -20,6 +20,7 @@ "js": [ "lib/settings.js", "content/tweaks/badges.js", + "content/tweaks/color-picker.js", "content/tweaks/color-popover.js", "content/main.js" ], diff --git a/popup/popup.css b/popup/popup.css index f74f4c7..52b30fd 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -200,3 +200,207 @@ fieldset input[type="text"] { gap: 4px; align-items: center; } + +/* --- 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; +}