From b8f7a967158f801bf06097a0690aa549fd12779d Mon Sep 17 00:00:00 2001 From: cottongin Date: Wed, 26 Aug 2026 15:54:08 -0400 Subject: [PATCH] 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 --- .../2026-08-26-inline-color-popover-design.md | 123 +++++++++++++----- 1 file changed, 93 insertions(+), 30 deletions(-) diff --git a/docs/superpowers/specs/2026-08-26-inline-color-popover-design.md b/docs/superpowers/specs/2026-08-26-inline-color-popover-design.md index dcbd410..460bb26 100644 --- a/docs/superpowers/specs/2026-08-26-inline-color-popover-design.md +++ b/docs/superpowers/specs/2026-08-26-inline-color-popover-design.md @@ -14,10 +14,20 @@ Single shared popover. One `
` element is injected ## Gear Icon — Trigger -Each subreddit badge (``) gets a `` appended as a child, containing a gear icon character (⚙). The gear is hidden by default and shown via CSS when the badge is hovered: +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; @@ -31,13 +41,12 @@ Each subreddit badge (``) gets 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. +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:** -- `display: inline` always, visibility controlled via `opacity` + `pointer-events` (allows CSS transition). +- `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`. -- Left margin: `4px` to separate from subreddit text. - `cursor: pointer`. **Click behavior:** @@ -47,6 +56,10 @@ Because the gear is a child of the badge ``, hovering over the gear maintains 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`). @@ -57,6 +70,8 @@ The popover is a `
` appended to `document.body` wi ┌──────────────────────────┐ │ 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 │ @@ -69,6 +84,7 @@ The popover is a `
` appended to `document.body` wi ``` - **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). @@ -88,7 +104,7 @@ 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. +- **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. @@ -97,8 +113,10 @@ A small CSS arrow (`::before` pseudo-element) points toward the badge. ### Initial values When the popover opens for a subreddit: -- If an override exists in `settings.subredditColors[subredditName]`, those values populate the inputs. -- If no override exists, the inputs show the current global subreddit badge colors for the active mode (light or dark), derived from `settings[prefix + ".subredditBgColor"]` etc. This way the user starts from the "current appearance" and can adjust from there. +- 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 @@ -115,7 +133,7 @@ When the popover opens for a subreddit: ### Save -1. Writes `{bg, border, text}` into `settings.subredditColors[subredditName]`. +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. @@ -128,10 +146,10 @@ When the popover opens for a subreddit: ### Reset to Default -1. Deletes `settings.subredditColors[subredditName]` (removes the key entirely). +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 so the user sees what "default" means. +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 @@ -170,23 +188,61 @@ The popover detects the current mode using the same `isDarkMode(settings)` logic - **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 gear icon injection in `processPost()`. Add popover creation, open/close/save/reset functions. | -| `content/styles/badges.css` | Add styles for `.rt-badge-gear`, `.rt-color-popover` and its children, light and dark variants. | -| `lib/settings.js` | No changes. | +| `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` | No changes. | -| `popup/popup.html` | No changes. | -| `manifest.json` | 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. | -Two files modified, zero new files. +Four files modified, one new file, two files with minor schema-driven updates. ## Edge Cases @@ -197,6 +253,8 @@ Two files modified, zero new files. - **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 @@ -204,16 +262,21 @@ 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. Clicking the gear opens the popover positioned near the badge. -4. Clicking the gear does not navigate the subreddit link. -5. Popover shows current override values (or global defaults if no override exists). -6. Changing colors updates the live preview badge in real time. -7. Actual page badges do not change until Save is clicked. -8. Clicking Save persists the override and updates all badges for that subreddit on the page. -9. Clicking Cancel (or outside, or Escape) dismisses without saving. -10. Reset to Default removes the override, reverts badge to global colors, and shows global values in the popover. -11. Inline changes appear in the popup's per-subreddit overrides section. -12. Popup changes to per-subreddit overrides are reflected when the inline popover is next opened. -13. Gear icon does not appear on comments badges. -14. Popover dark mode variant works when RES night mode is active. -15. Only one popover open at a time — clicking a different gear re-positions the popover. +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.