# Badge Polish: Gear Animation + Custom Color Picker — Design Spec **Date:** 2026-08-26 **Status:** Draft **Parent spec:** `2026-08-26-inline-color-popover-design.md` ## Overview Two improvements to the inline color popover and badge system: 1. **Gear icon animation** — remove the permanent padding gap on subreddit badges. The badge grows on hover to reveal the gear, then shrinks back. 2. **Custom color picker** — replace all native `` elements (which delegate to the macOS system picker) with an inline SV+Hue picker widget built in vanilla JS/canvas. ## Improvement 1: Gear Icon Animation ### Current Behavior `.rt-badge--subreddit` has `padding-right: 18px` at all times, reserving space for the gear icon even when it's hidden. This wastes ~12px on every subreddit badge. ### New Behavior The subreddit badge uses `padding-right: 6px` at rest (matching the left padding). On hover, `padding-right` transitions to `18px` over 150ms, smoothly expanding the badge to reveal the gear icon. The gear's opacity transition (0 → 0.6, 150ms) runs in parallel. When the mouse leaves, both transitions reverse — the gear fades out and the badge shrinks. The adjacent comments badge shifts right during the expansion. This is natural inline-block flow and at 150ms feels intentional rather than jittery. ### CSS Changes ```css .rt-badge--subreddit { position: relative; padding-right: 6px; transition: padding-right 0.15s; /* background, border-color, color unchanged */ } .rt-badge--subreddit:hover { padding-right: 18px; } ``` The gear icon CSS (`.rt-badge-gear`) is unchanged — it remains `position: absolute; right: 3px` and animates opacity on parent hover. ### File Changes | File | Change | |---|---| | `content/styles/badges.css` | Change `.rt-badge--subreddit` `padding-right` from `18px` to `6px`, add `transition: padding-right 0.15s`, add `.rt-badge--subreddit:hover { padding-right: 18px }`. | No JS changes required. --- ## Improvement 2: Custom Color Picker Widget ### Problem The native `` delegates to the OS-level color picker. On macOS, this opens a separate floating panel that is detached from the popover context, has limited precision, and feels foreign to the extension's UI. ### Solution 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 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 ``` ┌──────────────────────────────┐ │ ┌──────────────────────┐ │ │ │ │ │ ← SV square (saturation-value) │ │ [cursor ●] │ │ Click/drag to pick S + V │ │ │ │ Canvas-rendered for current hue │ └──────────────────────┘ │ │ ┌──────────────────────────┐ │ │ │ ← hue slider → │ │ ← Hue bar (horizontal rainbow) │ └──────────────────────────┘ │ Click/drag to change hue └──────────────────────────────┘ ``` #### 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 A `` element, redrawn whenever the hue changes. The canvas renders a two-dimensional gradient: - Horizontal axis: saturation (0% at left → 100% at right) - Vertical axis: value/brightness (100% at top → 0% at bottom) The gradient is drawn by filling the canvas with the current hue at full saturation, then overlaying a white-to-transparent horizontal gradient and a transparent-to-black vertical gradient. A small circular cursor indicates the current position. **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 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 - **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: ``` Label [] [] ``` This changes to: ``` Label [
] [] ``` The swatch `
` is a small colored rectangle (same size as the old color input: 28×20px) showing the current color. Clicking the swatch opens the picker inline, appearing below the clicked row within the popover. The picker slides into view — it's part of the popover DOM, so the popover grows vertically to accommodate it. Only one picker is open at a time within the popover. Clicking a different row's swatch closes the current picker and opens the new one. Clicking the same swatch while its picker is already open toggles the picker closed. When the picker is open, changes are reflected live in: - The swatch `
` for that row - The hex text input for that row - The popover's live preview badge ### Integration with Popup The popup uses the same picker widget. Each `` in the popup (global colors + per-subreddit overrides) is replaced with a clickable swatch `
`. Clicking opens the picker inline below the control, within the popup's scrollable area. The popup loads the picker module via a `` before `popup.js`. | | `popup/popup.js` | Replace `` creation with swatch `
` + picker integration for global colors and per-subreddit overrides. | | `popup/popup.css` | Add picker styles for the popup context (same classes, scoped by parent). | | `manifest.json` | Add `content/tweaks/color-picker.js` to content scripts array (before `color-popover.js`). | --- ## Edge Cases - **Picker open + popover dismiss:** If the picker is open when the popover is dismissed (Cancel, Escape, click-outside), the popover hides and the picker is hidden with it. No orphaned state. - **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 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 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 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).