Files
reddit-tweaks-ffe/docs/superpowers/specs/2026-08-26-badge-polish-design.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

16 KiB
Raw Permalink Blame History

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

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

Solution

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.

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

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  [<input type="color">] [<input type="text" hex>]

This changes to:

Label  [<div swatch ■>] [<input type="text" hex>]

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.

Styling

Light mode:

  • Picker background: matches parent (transparent, inherits popover/popup background)
  • SV square border: 1px solid #ccc, border-radius: 3px
  • 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.

File Changes

File Change
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).
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).