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>
This commit is contained in:
2026-08-26 15:54:08 -04:00
parent 49b7d471b6
commit b8f7a96715

View File

@@ -14,10 +14,20 @@ Single shared popover. One `<div class="rt-color-popover">` element is injected
## 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 hidden by default and shown via CSS when the badge is hovered:
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:
```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 (`<a class="rt-badge rt-badge--subreddit">`) gets a `<span
}
```
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.
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:**
- `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 `<a>`, 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 `<div class="rt-color-popover">` appended to `document.body` with `position: absolute` and a high `z-index` (e.g., `10000`).
@@ -57,6 +70,8 @@ The popover is a `<div class="rt-color-popover">` 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 `<div class="rt-color-popover">` 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 `<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).
@@ -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.