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 `<input type="color">` 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 `<input type="color">` 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.
Replace all `<input type="color">` 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.
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.
**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.
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.
A `<canvas>` 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.
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.
- **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.
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.
The swatch `<div>` 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 `<div>` 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 `<input type="color">` in the popup (global colors + per-subreddit overrides) is replaced with a clickable swatch `<div>`. Clicking opens the picker inline below the control, within the popup's scrollable area.
The popup loads the picker module via a `<script>` tag in `popup.html` (the content script version is loaded via the manifest for the page context).
### Picker API
The picker module exposes `window.RedditTweaks.colorPicker`:
-`create(containerEl)` — creates a picker instance and appends it to the container. Returns a picker object.
- Picker object:
-`open(anchorEl, currentHex, onChange)` — shows the picker below `anchorEl`, sets initial color from hex, calls `onChange(hex)` on every change.
-`close()` — hides the picker.
-`setColor(hex)` — programmatically update the picker's color (for external sync).
-`isOpen()` — returns whether the picker is currently visible.
-`destroy()` — removes DOM elements and event listeners.
- Hue bar border: `1px solid #ccc`, `border-radius: 3px`
- SV cursor: 10px circle, `2px solid white`, `box-shadow: 0 0 2px rgba(0,0,0,0.5)` for visibility against any background
- Hue indicator: 2px wide vertical line, white with dark shadow
**Dark mode (`.rt-dark` context):**
- SV square border: `1px solid #555`
- Hue bar border: `1px solid #555`
- Cursor and indicator styles unchanged (they're designed to be visible on any background)
**Spacing:** 6px margin between SV square and hue bar. 8px margin above the picker (separating it from the swatch row). 8px margin below (before the next row or preview section).
### Popover Repositioning
When the picker opens inside the popover, the popover grows taller. If this would push the popover below the viewport bottom, the popover's existing positioning logic handles the flip (it already accounts for viewport overflow). The popover doesn't reposition dynamically while open — the picker height is fixed, so the size change happens once on open.
| `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 `<input type="color" class="rt-color-swatch">` with `<div class="rt-color-swatch">`. 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 `<script src="../content/tweaks/color-picker.js"></script>` before `popup.js`. |
| `popup/popup.js` | Replace `<input type="color">` creation with swatch `<div>` + picker integration for global colors and per-subreddit overrides. |
| `popup/popup.css` | Add picker styles for the popup context (same classes, scoped by parent). |
- **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.