Files
reddit-tweaks-ffe/docs/superpowers/specs/2026-08-26-inline-color-popover-design.md
cottongin b8f7a96715 Harden spec after adversarial review
Address six gaps found during review:
- Fix subreddit name extraction bug (gear icon text in textContent)
- Prevent gear icon layout shift via absolute positioning
- Handle native color picker dialog in click-outside detection
- Make per-subreddit overrides mode-aware (light/dark)
- Replace badge hover opacity with filter: brightness
- Split popover logic into separate file

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 15:54:08 -04:00

17 KiB
Raw Permalink Blame History

Inline Subreddit Color Popover — Design Spec

Date: 2026-08-26 Status: Draft Parent spec: 2026-08-26-reddit-badge-tweaks-design.md

Overview

Add an inline color-override popover to subreddit badges, letting users set per-subreddit colors directly on the page without opening the extension popup. A gear icon appears on hover over a subreddit badge; clicking it opens a floating popover with color pickers, a live preview, and Save/Cancel/Reset controls. The popover writes to the same subredditColors storage used by the popup, keeping both UIs in sync.

Approach

Single shared popover. One <div class="rt-color-popover"> element is injected into document.body once when the badge tweak initializes. Opening the popover for any subreddit re-positions and re-populates this single element. This avoids creating hidden popover elements per post and ensures only one popover is open at a time.

Gear Icon — Trigger

Each subreddit badge (<a class="rt-badge rt-badge--subreddit">) gets a <span class="rt-badge-gear"> appended as a child, containing a gear icon character (⚙). The gear is absolutely positioned within the badge to avoid layout shift — it overlaps the badge's right padding area rather than pushing the badge wider. The gear is hidden by default and shown via CSS when the badge is hovered:

.rt-badge--subreddit {
  position: relative;
  /* extra right padding to make room for the gear on hover */
  padding-right: 18px;
}

.rt-badge-gear {
  position: absolute;
  right: 3px;
  top: 50%;
  transform: translateY(-50%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
}
.rt-badge--subreddit:hover .rt-badge-gear {
  opacity: 0.6;
  pointer-events: auto;
}
.rt-badge-gear:hover {
  opacity: 1 !important;
}

Because the gear is a child of the badge <a>, hovering over the gear maintains the parent's :hover state — no gap or flicker when moving the cursor from the text to the gear. The gear is absolutely positioned so it never shifts adjacent elements.

Gear icon properties:

  • position: absolute, anchored to the right side of the badge, vertically centered.
  • Font size: 10–11px, matching badge text proportion.
  • Color: inherits badge text color. At rest (parent hovered): opacity: 0.6. On direct gear hover: opacity: 1.
  • cursor: pointer.

Click behavior:

  • event.preventDefault() — prevents navigating the subreddit link.
  • event.stopPropagation() — prevents bubbling to the badge click handler.
  • Calls openPopover() with the badge element, subreddit name, and current settings.

Only subreddit badges get the gear icon. Comments badges do not.

Subreddit Name Data Attribute

During processPost(), the subreddit name (without the r/ prefix) is stored as a data-rt-subreddit attribute on the badge container element. All code that needs the subreddit name (popover, updateColors()) reads from this attribute instead of parsing textContent. This avoids a bug where the gear icon's text content (⚙) would be included in the extracted name, breaking override lookups.

Popover — Structure

The popover is a <div class="rt-color-popover"> appended to document.body with position: absolute and a high z-index (e.g., 10000).

Contents

┌──────────────────────────┐
│ r/javascript         [×] │  ← header with subreddit name and close button
├──────────────────────────┤
│ [Light ▾]                │  ← mode toggle (defaults to current mode)
├──────────────────────────┤
│ Background  [■] #fde8e0  │  ← color swatch + hex text input
│ Border      [■] #c84a20  │
│ Text        [■] #c84a20  │
├──────────────────────────┤
│ Preview: [ r/javascript ]│  ← live preview badge
├──────────────────────────┤
│ [Reset Default]    [Save]│  ← action buttons
│                  [Cancel]│
└──────────────────────────┘
  • Header: Subreddit name as a label (e.g., "r/javascript"), truncated with ellipsis if long. Close button (×) on the right.
  • Mode toggle: A <select> dropdown with "Light" and "Dark" options. Defaults to the currently active mode. Switching the toggle swaps the color inputs to show the values for the selected mode. Both modes are saved independently on Save.
  • Color rows: Three rows, one per property (background, border, text). Each row has a label, an <input type="color"> swatch, and a <input type="text"> for hex value. The two inputs are synced bidirectionally.
  • Live preview: A <span> styled as an rt-badge rt-badge--subreddit, with the subreddit name as text content. Inline styles update in real time as the user adjusts any color input.
  • Footer: "Reset to Default" button (left), "Save" and "Cancel" buttons (right).

Positioning

On open, the popover is positioned below the clicked badge, aligned to its left edge:

  1. Get the badge's getBoundingClientRect().
  2. Calculate top = rect.bottom + window.scrollY + 6px (6px gap).
  3. Calculate left = rect.left + window.scrollX.
  4. If the popover would overflow the viewport bottom, flip above: top = rect.top + window.scrollY - popoverHeight - 6px.
  5. Clamp left so the popover doesn't overflow left or right edges.

A small CSS arrow (::before pseudo-element) points toward the badge.

Lifecycle

  • Open: Clicking a gear icon calls openPopover(). If the popover is already open for a different subreddit, it re-positions and re-populates (no stacking). If the popover is already open for the same subreddit, the click toggles it closed (cancel behavior).
  • Close without saving: Clicking Cancel, clicking the × button, clicking outside the popover, pressing Escape, or toggling the same gear icon. The click-outside handler checks event.target against the popover via contains() and also ignores clicks when a color input inside the popover has focus (prevents dismissal when Firefox's native color picker dialog fires document-level click events).
  • Close with saving: Clicking Save.
  • Dark mode toggle while open: If dark mode changes while the popover is open (e.g., user toggles RES night mode), the popover's visual theme updates via the existing dark mode observer, but the color values in the inputs are not changed — they reflect the user's in-progress edits regardless of mode.

Color Editing and Preview

Initial values

When the popover opens for a subreddit:

  • The mode toggle defaults to the currently active mode (light or dark).
  • If a mode-aware override exists in settings.subredditColors[subredditName][mode], those values populate the inputs.
  • If no override exists for the selected mode, the inputs show the current global subreddit badge colors for that mode, derived from settings[prefix + ".subredditBgColor"] etc. This way the user starts from the "current appearance" and can adjust from there.
  • Switching the mode toggle swaps the inputs to show values for the other mode (override if it exists, otherwise global defaults). In-progress edits for the previous mode are held in memory until Save or Cancel.

Input syncing

  • Changing the <input type="color"> updates the text input's value.
  • Changing the text input updates the color picker if the value is a valid hex color (/^#[0-9a-f]{6}$/i). Invalid values show a subtle red border on the text input but do not update the picker.

Live preview

  • As any color input changes (via input event), the preview badge's inline styles update immediately.
  • The preview badge is styled identically to a real subreddit badge (same classes), with inline style overrides for the three color properties.
  • Actual page badges are not updated until Save is clicked.

Save, Cancel, and Reset

Save

  1. Writes the in-progress colors for both modes into settings.subredditColors[subredditName] using the mode-aware schema: { light: {bg, border, text}, dark: {bg, border, text} }. If the user only edited one mode, the other mode's values are preserved from the existing override (or omitted if none existed, so the global defaults continue to apply).
  2. Calls RedditTweaks.saveSettings(settings) → writes to browser.storage.local.
  3. The existing storage.onChanged listener in main.js fires, calling updateColors(), which re-applies per-subreddit overrides to all badges on the page — all instances of that subreddit update, not just the one that was clicked.
  4. Closes the popover.

Cancel

  • Dismisses the popover without writing anything.
  • Triggered by: clicking Cancel, clicking ×, clicking outside the popover, pressing Escape.
  • Page badges remain unchanged.

Reset to Default

  1. Deletes settings.subredditColors[subredditName] entirely (removes both light and dark overrides).
  2. Saves the updated settings.
  3. The badge reverts to global colors via the updateColors() flow.
  4. The popover inputs update to show the global defaults for the currently selected mode so the user sees what "default" means.
  5. The popover stays open so the user can confirm or adjust further.

Sync with popup

Both the inline popover and the popup's per-subreddit overrides section read/write the same subredditColors object in browser.storage.local. Changes made inline appear in the popup next time it's opened, and vice versa. No additional sync logic is needed.

Styling

Popover

  • White background (#fff), 1px solid #ccc border, border-radius: 6px.
  • box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) for depth.
  • Padding: 12px.
  • Max-width: 260px.
  • Font: same stack as the extension (-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif), 13px base.
  • CSS arrow (via ::before pseudo-element) pointing toward the anchor badge.

Dark mode variant

The popover detects the current mode using the same isDarkMode(settings) logic in badges.js. When dark:

  • Background: #2a2a2a.
  • Border: #555.
  • Text: #e0e0e0.
  • Input borders adjusted for contrast.
  • Applied via .rt-dark class on the popover element, consistent with the existing badge dark mode approach.

Color input rows

  • Flex layout: label (left), color swatch + hex text (right).
  • Color swatches: 28px × 20px, 1px solid #ccc border, border-radius: 3px.
  • Hex text inputs: 70px wide, monospace font, font-size: 12px.

Buttons

  • Save: Accent blue background (#4a90d9), white text, border-radius: 3px.
  • Cancel: Text-only or light secondary style, same as popup's .btn-secondary.
  • Reset to Default: Secondary style, positioned on the left side of the footer to avoid accidental clicks near Save.

Badge hover effect

The existing .rt-badge:hover { opacity: 0.8; } rule is replaced with filter: brightness(0.9) for all badges. CSS opacity creates a stacking context that multiplicatively reduces child element opacity (the gear icon would appear washed out). filter: brightness() achieves a similar visual dimming without affecting child element opacity.

All classes use the rt- prefix

Consistent with the rest of the extension: rt-color-popover, rt-popover-header, rt-popover-row, rt-popover-preview, rt-popover-footer, etc.

Mode-Aware Subreddit Overrides

Schema Change

The subredditColors storage schema changes from a flat {bg, border, text} per subreddit to a mode-aware structure:

// Before (v1):
subredditColors: {
  "javascript": { bg: "#...", border: "#...", text: "#..." }
}

// After (v2):
subredditColors: {
  "javascript": {
    light: { bg: "#...", border: "#...", text: "#..." },
    dark:  { bg: "#...", border: "#...", text: "#..." }
  }
}

Migration

On loadSettings(), if a subreddit entry has the old flat shape (has a bg key at the top level instead of light/dark), it is migrated in-place: the flat values are copied into both light and dark sub-objects, and the migrated settings are saved back. This is a one-time, non-destructive migration.

Impact on applyColorsToContainer()

applyColorsToContainer() changes to read from overrides[prefix] (where prefix is "light" or "dark") instead of directly from overrides. If the mode-specific sub-object doesn't exist, it falls back to global defaults as before.

Impact on Popup

The popup's per-subreddit overrides section (popup.js) is updated to read/write the new schema. Each subreddit entry in the popup shows two sets of color pickers (light and dark), or a mode toggle — matching the inline popover's approach. The collectSubredditOverrides() and addSubredditEntry() functions are updated accordingly.

File Changes

File Change
content/tweaks/badges.js Add data-rt-subreddit attribute in processPost(). Add gear icon injection. Update updateColors() to read subreddit name from data attribute. Update applyColorsToContainer() for mode-aware overrides. Replace badge hover opacity with filter: brightness().
content/tweaks/color-popover.js New file. Popover DOM creation, positioning, open/close, save/reset, color input syncing, click-outside handling, mode toggle.
content/styles/badges.css Add styles for .rt-badge-gear (absolute positioning, opacity transitions), .rt-color-popover and its children (layout, colors, light/dark variants). Replace .rt-badge:hover opacity with filter.
lib/settings.js Add v1 → v2 migration logic in loadSettings() for the subredditColors schema change.
content/main.js No changes.
popup/popup.js Update addSubredditEntry(), collectSubredditOverrides(), and renderSubredditOverrides() for mode-aware schema.
popup/popup.html Update per-subreddit override UI to show light/dark color pickers.
manifest.json Add content/tweaks/color-popover.js to the content_scripts.js array.

Four files modified, one new file, two files with minor schema-driven updates.

Edge Cases

  • Subreddit badge absent: No gear icon added, no popover trigger. Follows existing behavior where the subreddit badge is skipped.
  • Popover open when page scrolls: Popover uses position: absolute and scrolls with page content. No repositioning needed.
  • Popover open when RES loads new posts: New posts get processed normally. The open popover stays in place.
  • Multiple rapid gear clicks: Single popover instance re-positions and re-populates. No stacking.
  • Invalid hex input: Text input shows subtle error border. Save uses the color picker's last valid value.
  • Very long subreddit names: Header truncates with text-overflow: ellipsis.
  • Extension disabled mid-session: Page reloads (existing behavior), removing all injected elements including the popover.
  • Native color picker dialog: The click-outside handler ignores clicks when a color input inside the popover has focus, preventing dismissal while Firefox's native color picker is open.
  • Old schema migration: Existing per-subreddit overrides in the flat {bg, border, text} format are migrated to {light: {...}, dark: {...}} on first load. The migration copies the flat values into both modes, preserving the user's existing customizations.

Testing

Manual testing additions:

  1. Hovering over a subreddit badge shows the gear icon; moving off hides it.
  2. Moving from badge text to gear icon does not flicker/hide the gear.
  3. Gear icon does not cause layout shift (adjacent badges don't move on hover).
  4. Clicking the gear opens the popover positioned near the badge.
  5. Clicking the gear does not navigate the subreddit link.
  6. Popover shows current override values (or global defaults if no override exists).
  7. Mode toggle defaults to current mode; switching it loads the other mode's values.
  8. Editing colors in one mode, switching modes, then switching back preserves in-progress edits.
  9. Changing colors updates the live preview badge in real time.
  10. Actual page badges do not change until Save is clicked.
  11. Clicking Save persists overrides for both modes and updates all badges for that subreddit.
  12. Clicking Cancel (or outside, or Escape) dismisses without saving.
  13. Using the native color picker dialog does not dismiss the popover.
  14. Reset to Default removes overrides for both modes, reverts badge to global colors, and shows global values in the popover.
  15. Inline changes appear in the popup's per-subreddit overrides section.
  16. Popup changes to per-subreddit overrides are reflected when the inline popover is next opened.
  17. Gear icon does not appear on comments badges.
  18. Popover dark mode variant works when RES night mode is active.
  19. Only one popover open at a time — clicking a different gear re-positions the popover.
  20. Existing per-subreddit overrides (old flat schema) are migrated correctly and still work after the update.