# 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 `
` 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 (``) gets a `` 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: ```css .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 ``, 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 `
` 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 `` swatch, and a `` for hex value. The two inputs are synced bidirectionally. - **Live preview:** A `` 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 `` 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.