diff --git a/docs/superpowers/plans/2026-08-26-card-layout.md b/docs/superpowers/plans/2026-08-26-card-layout.md new file mode 100644 index 0000000..6e63375 --- /dev/null +++ b/docs/superpowers/plans/2026-08-26-card-layout.md @@ -0,0 +1,682 @@ +# Card Layout Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Wrap each old Reddit post in a shadcn-styled card with configurable gap, page background, and independent toggle from badges. + +**Architecture:** Add `rt-card` class to each `div.thing.link` via JS, apply card styling via scoped CSS. Convert internal float layout to flexbox. Shared dark mode detection in `settings.js`. Unified MutationObserver in `main.js` for both badge and card processing. + +**Tech Stack:** Vanilla JS (IIFE modules on `window.RedditTweaks`), plain CSS, Firefox WebExtension manifest v2. + +## Global Constraints + +- old.reddit.com only — `www.reddit.com` is out of scope +- No build tools, no frameworks, no npm dependencies +- All CSS values for the card must match shadcn/ui's default neutral theme exactly (see spec `docs/superpowers/specs/2026-08-26-card-layout-design.md` for token table) +- Modules use IIFE pattern and expose API on `window.RedditTweaks` +- Imports at top of file, no inline imports + +--- + +### Task 1: Extract shared utilities to `settings.js` + +Move `isDarkMode()` and `getColorPrefix()` from `badges.js` into `settings.js` as shared infrastructure. Add new setting defaults. Update all consumers. + +**Files:** +- Modify: `lib/settings.js` +- Modify: `content/tweaks/badges.js` +- Modify: `content/tweaks/color-popover.js` + +**Interfaces:** +- Produces: `window.RedditTweaks.isDarkMode(settings)` → `boolean` +- Produces: `window.RedditTweaks.getColorPrefix(settings)` → `"light" | "dark"` +- Produces: New DEFAULTS keys: `badgeLayout` (true), `cardLayout` (true), `cardBackground` ("reddit"), `cardGap` (16) + +- [ ] **Step 1: Add dark mode functions and new defaults to `settings.js`** + +Add `isDarkMode`, `getColorPrefix`, and new setting keys to `lib/settings.js`. Insert after the `resetSettings` function, before the `window.RedditTweaks` assignment: + +```js +function isDarkMode(settings) { + if (settings.darkMode === "light") return false; + if (settings.darkMode === "dark") return true; + return document.body.classList.contains("res-nightmode"); +} + +function getColorPrefix(settings) { + return isDarkMode(settings) ? "dark" : "light"; +} +``` + +Add to `DEFAULTS`: + +```js +badgeLayout: true, +cardLayout: true, +cardBackground: "reddit", +cardGap: 16, +``` + +Add to the `window.RedditTweaks` assignment: + +```js +window.RedditTweaks.isDarkMode = isDarkMode; +window.RedditTweaks.getColorPrefix = getColorPrefix; +``` + +- [ ] **Step 2: Update `badges.js` to use shared functions** + +In `content/tweaks/badges.js`: + +Remove the `isDarkMode` and `getColorPrefix` function definitions (lines 8-17). + +Replace all internal calls to `isDarkMode(settings)` with `window.RedditTweaks.isDarkMode(settings)` and `getColorPrefix(settings)` with `window.RedditTweaks.getColorPrefix(settings)`. + +Update the exported API — keep `isDarkMode` and `getColorPrefix` as aliases for backward compatibility during this transition, but point them at the shared versions: + +```js +window.RedditTweaks.badgeTweak = { + apply: apply, + updateColors: updateColors, + isDarkMode: window.RedditTweaks.isDarkMode, + getColorPrefix: window.RedditTweaks.getColorPrefix, +}; +``` + +- [ ] **Step 3: Update `color-popover.js` to use shared functions** + +In `content/tweaks/color-popover.js`, change the `getHelpers()` function (lines 16-21): + +```js +function getHelpers() { + return { + isDarkMode: window.RedditTweaks.isDarkMode, + getColorPrefix: window.RedditTweaks.getColorPrefix, + }; +} +``` + +- [ ] **Step 4: Verify extension still works** + +Load the extension in Firefox (`about:debugging` → Load Temporary Add-on). Navigate to `old.reddit.com`. Verify: +- Badges render with correct colors +- Dark mode detection works (if RES is installed, toggle night mode) +- Color popover opens and displays correct mode +- No console errors + +- [ ] **Step 5: Commit** + +```bash +git add lib/settings.js content/tweaks/badges.js content/tweaks/color-popover.js +git commit -m "refactor: extract isDarkMode/getColorPrefix to settings.js as shared utilities" +``` + +--- + +### Task 2: Badge layout toggle + FOUC CSS rename + +Add `badgeLayout` toggle to the popup and gate badge processing behind it. Rename the FOUC CSS class from `rt-enabled` to `rt-badges-enabled`. + +**Files:** +- Modify: `content/styles/badges.css` +- Modify: `content/main.js` +- Modify: `popup/popup.html` +- Modify: `popup/popup.js` + +**Interfaces:** +- Consumes: `settings.badgeLayout` (boolean, from Task 1 DEFAULTS) +- Produces: `html.rt-badges-enabled` class on `` (replaces `html.rt-enabled`) + +- [ ] **Step 1: Rename FOUC selectors in `badges.css`** + +In `content/styles/badges.css`, change the FOUC prevention selectors (lines 1-5): + +From: +```css +html.rt-enabled .thing.link .tagline .subreddit, +html.rt-enabled .thing.link .flat-list li:has(> .comments) { + display: none !important; +} +``` + +To: +```css +html.rt-badges-enabled .thing.link .tagline .subreddit, +html.rt-badges-enabled .thing.link .flat-list li:has(> .comments) { + display: none !important; +} +``` + +- [ ] **Step 2: Update `main.js` to gate badges behind `badgeLayout`** + +Replace the `init()` function in `content/main.js`: + +```js +async function init() { + const settings = await window.RedditTweaks.loadSettings(); + if (!settings.enabled) return; + + if (settings.badgeLayout) { + document.documentElement.classList.add("rt-badges-enabled"); + window.RedditTweaks.badgeTweak.apply(settings); + } + + browser.storage.onChanged.addListener(function (changes) { + if ( + (changes.enabled && changes.enabled.newValue !== changes.enabled.oldValue) || + (changes.badgeLayout && changes.badgeLayout.newValue !== changes.badgeLayout.oldValue) + ) { + location.reload(); + return; + } + + window.RedditTweaks.loadSettings().then(function (updated) { + if (settings.badgeLayout) { + window.RedditTweaks.badgeTweak.updateColors(updated); + } + }); + }); +} +``` + +- [ ] **Step 3: Add badge toggle to popup HTML** + +In `popup/popup.html`, add a badge toggle after the existing enabled checkbox. Change the first section: + +```html +
+ + +
+``` + +- [ ] **Step 4: Wire badge toggle in `popup.js`** + +In `popup/popup.js`, add `badgeLayout` to the `populateControls` function: + +```js +document.getElementById("badgeLayout").checked = settings.badgeLayout; +``` + +Add change listener in `init()`, next to the existing `enabled` listener: + +```js +document.getElementById("badgeLayout").addEventListener("change", saveFromControls); +``` + +Add to `saveFromControls()` — in the settings object construction, add: + +```js +badgeLayout: document.getElementById("badgeLayout").checked, +``` + +- [ ] **Step 5: Verify badge toggle works** + +Reload extension. Open popup: +- Badge layout checkbox is checked by default +- Unchecking it and reloading old.reddit.com: original subreddit/comment links are visible, no badges +- Checking it back: badges return after reload +- No console errors + +- [ ] **Step 6: Commit** + +```bash +git add content/styles/badges.css content/main.js popup/popup.html popup/popup.js +git commit -m "feat: add badgeLayout toggle, rename FOUC class to rt-badges-enabled" +``` + +--- + +### Task 3: Create card CSS + +Create `content/styles/cards.css` with all card styling per the spec. Add it to the manifest. + +**Files:** +- Create: `content/styles/cards.css` +- Modify: `manifest.json` + +**Interfaces:** +- Produces: CSS rules scoped under `.rt-card`, `html.rt-card-enabled`, `html.rt-card-bg-shadcn` + +- [ ] **Step 1: Create `content/styles/cards.css`** + +```css +/* --- Post list container --- */ +html.rt-card-enabled #siteTable { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--rt-card-gap, 1rem); +} + +html.rt-card-enabled #siteTable > .clearleft { + display: none; +} + +/* --- Card container --- */ +.rt-card { + background: #ffffff; + color: #0a0a0a; + border: 1px solid #e5e5e5; + border-radius: 0.875rem; + padding: 1.5rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + transition: border-color 0.15s; + display: flex; + align-items: flex-start; + gap: 0.75rem; + width: 100%; + overflow: hidden; +} + +.rt-card:hover { + border-color: #d4d4d4; +} + +/* --- Card internal layout overrides --- */ +.rt-card .midcol, +.rt-card .thumbnail { + float: none; + flex-shrink: 0; +} + +.rt-card .entry { + flex: 1; + min-width: 0; +} + +.rt-card .clearleft { + display: none; +} + +/* --- Card dark mode --- */ +.rt-dark .rt-card { + background: #171717; + color: #fafafa; + border-color: rgba(255, 255, 255, 0.1); +} + +.rt-dark .rt-card:hover { + border-color: rgba(255, 255, 255, 0.18); +} + +/* --- Page background override --- */ +html.rt-card-bg-shadcn .content[role="main"] { + background: #f5f5f5; +} + +html.rt-card-bg-shadcn.rt-dark .content[role="main"] { + background: #262626; +} +``` + +- [ ] **Step 2: Add `cards.css` to `manifest.json`** + +In the `content_scripts[0].css` array, add `content/styles/cards.css` after `badges.css`: + +```json +"css": ["content/styles/badges.css", "content/styles/cards.css"], +``` + +- [ ] **Step 3: Verify no parse errors** + +Reload extension. Open `old.reddit.com`. No console CSS parse errors. Cards don't render yet (no JS adding the `rt-card` class), but the CSS is loaded. + +- [ ] **Step 4: Commit** + +```bash +git add content/styles/cards.css manifest.json +git commit -m "feat: add card CSS with shadcn design tokens" +``` + +--- + +### Task 4: Create card JS + unified observer + +Create the card tweak module. Refactor `main.js` to use a single shared MutationObserver that dispatches to both badge and card processors. Remove the duplicate observer from `badges.js`. + +**Files:** +- Create: `content/tweaks/cards.js` +- Modify: `content/tweaks/badges.js` +- Modify: `content/main.js` +- Modify: `manifest.json` + +**Interfaces:** +- Consumes: `window.RedditTweaks.isDarkMode(settings)` (from Task 1) +- Consumes: `settings.cardLayout`, `settings.cardBackground`, `settings.cardGap` (from Task 1) +- Produces: `window.RedditTweaks.cardTweak.apply(settings)` — processes posts + applies card classes +- Produces: `window.RedditTweaks.cardTweak.updateSettings(settings)` — live-update gap/background +- Produces: `window.RedditTweaks.cardTweak.processNewPosts(settings)` — process unprocessed posts (called by shared observer) +- Modifies: `window.RedditTweaks.badgeTweak` — adds `processNewPosts(settings)` for shared observer, removes internal observer + +- [ ] **Step 1: Create `content/tweaks/cards.js`** + +```js +(function () { + "use strict"; + + function processPost(post) { + if (post.classList.contains("rt-card")) return; + post.classList.add("rt-card"); + } + + function processPosts() { + var posts = document.querySelectorAll("div.thing.link:not(.rt-card)"); + for (var i = 0; i < posts.length; i++) { + processPost(posts[i]); + } + } + + function applyDarkMode(settings) { + if (window.RedditTweaks.isDarkMode(settings)) { + document.documentElement.classList.add("rt-dark"); + } else { + document.documentElement.classList.remove("rt-dark"); + } + } + + function applyBackground(settings) { + if (settings.cardBackground === "shadcn") { + document.documentElement.classList.add("rt-card-bg-shadcn"); + } else { + document.documentElement.classList.remove("rt-card-bg-shadcn"); + } + } + + function applyGap(settings) { + var siteTable = document.getElementById("siteTable"); + if (siteTable) { + siteTable.style.setProperty("--rt-card-gap", settings.cardGap + "px"); + } + } + + function apply(settings) { + document.documentElement.classList.add("rt-card-enabled"); + applyDarkMode(settings); + applyBackground(settings); + applyGap(settings); + processPosts(); + } + + function updateSettings(settings) { + applyDarkMode(settings); + applyBackground(settings); + applyGap(settings); + } + + window.RedditTweaks = window.RedditTweaks || {}; + window.RedditTweaks.cardTweak = { + apply: apply, + updateSettings: updateSettings, + processNewPosts: processPosts, + }; +})(); +``` + +- [ ] **Step 2: Expose `processNewPosts` from `badges.js`, remove both internal observers** + +In `content/tweaks/badges.js`: + +Remove the `contentObserver` variable declaration (line 4: `let contentObserver = null;`). + +Remove the `darkModeObserver` variable declaration (line 5: `let darkModeObserver = null;`). + +Remove the `getObserverTarget()` function (lines 106-112). + +Remove the `startContentObserver(settings)` function (lines 114-135). + +Remove the `startDarkModeObserver()` function (lines 137-149). + +Remove both observer calls from the `apply()` function: `startContentObserver(settings)` (line 168) and `startDarkModeObserver()` (line 169). + +Add `processNewPosts` to the exported API: + +```js +window.RedditTweaks.badgeTweak = { + apply: apply, + updateColors: updateColors, + isDarkMode: window.RedditTweaks.isDarkMode, + getColorPrefix: window.RedditTweaks.getColorPrefix, + processNewPosts: processPosts, +}; +``` + +- [ ] **Step 3: Add unified observer to `main.js`** + +In `content/main.js`, add shared observer functions before `init()`. + +First, the dark mode observer (moved from `badges.js`). This watches for RES night mode toggles and dispatches to both modules: + +```js +function startDarkModeObserver(settings) { + var observer = new MutationObserver(function () { + if (settings.darkMode === "auto") { + window.RedditTweaks.loadSettings().then(function (updated) { + if (settings.badgeLayout) { + window.RedditTweaks.badgeTweak.updateColors(updated); + } + if (settings.cardLayout) { + window.RedditTweaks.cardTweak.updateSettings(updated); + } + }); + } + }); + + observer.observe(document.body, { + attributes: true, + attributeFilter: ["class"], + }); +} +``` + +Then the content observer for new posts: + +```js +function startPostObserver(settings) { + var target = + document.getElementById("siteTable") || + document.querySelector('.content[role="main"]') || + document.body; + + var observer = new MutationObserver(function (mutations) { + var hasNewPosts = false; + for (var i = 0; i < mutations.length; i++) { + var added = mutations[i].addedNodes; + for (var j = 0; j < added.length; j++) { + var node = added[j]; + if (node.nodeType !== Node.ELEMENT_NODE) continue; + if ((node.matches && node.matches("div.thing.link")) || + (node.querySelector && node.querySelector("div.thing.link"))) { + hasNewPosts = true; + break; + } + } + if (hasNewPosts) break; + } + if (hasNewPosts) { + window.RedditTweaks.loadSettings().then(function (updated) { + if (settings.badgeLayout) { + window.RedditTweaks.badgeTweak.processNewPosts(updated); + } + if (settings.cardLayout) { + window.RedditTweaks.cardTweak.processNewPosts(updated); + } + }); + } + }); + + observer.observe(target, { childList: true, subtree: true }); +} +``` + +Update `init()` to call `cardTweak.apply`, start the shared observer, and observe dark mode changes for cards: + +```js +async function init() { + const settings = await window.RedditTweaks.loadSettings(); + if (!settings.enabled) return; + + if (settings.badgeLayout) { + document.documentElement.classList.add("rt-badges-enabled"); + window.RedditTweaks.badgeTweak.apply(settings); + } + + if (settings.cardLayout) { + window.RedditTweaks.cardTweak.apply(settings); + } + + if (settings.badgeLayout || settings.cardLayout) { + startPostObserver(settings); + startDarkModeObserver(settings); + } + + browser.storage.onChanged.addListener(function (changes) { + if ( + (changes.enabled && changes.enabled.newValue !== changes.enabled.oldValue) || + (changes.badgeLayout && changes.badgeLayout.newValue !== changes.badgeLayout.oldValue) || + (changes.cardLayout && changes.cardLayout.newValue !== changes.cardLayout.oldValue) + ) { + location.reload(); + return; + } + + window.RedditTweaks.loadSettings().then(function (updated) { + if (settings.badgeLayout) { + window.RedditTweaks.badgeTweak.updateColors(updated); + } + if (settings.cardLayout) { + window.RedditTweaks.cardTweak.updateSettings(updated); + } + }); + }); +} +``` + +- [ ] **Step 4: Add `cards.js` to `manifest.json`** + +In the `content_scripts[0].js` array, add `content/tweaks/cards.js` before `main.js`: + +```json +"js": [ + "lib/settings.js", + "content/tweaks/badges.js", + "content/tweaks/color-picker.js", + "content/tweaks/color-popover.js", + "content/tweaks/cards.js", + "content/main.js" +], +``` + +Also remove `"*://www.reddit.com/*"` from the `matches` array (out of scope per spec): + +```json +"matches": ["*://old.reddit.com/*"], +``` + +- [ ] **Step 5: Verify cards render** + +Reload extension. Navigate to `old.reddit.com`: +- Each post is wrapped in a card: white background, 1px `#e5e5e5` border, 14px rounded corners, subtle shadow +- Posts are separated by 16px gaps +- Vote arrows, thumbnail, and entry are horizontally aligned (no floats) +- Hovering a card darkens the border to `#d4d4d4` +- Badges still render inside cards (if badge layout is enabled) +- No console errors + +- [ ] **Step 6: Commit** + +```bash +git add content/tweaks/cards.js content/tweaks/badges.js content/main.js manifest.json +git commit -m "feat: add card layout tweak with unified post observer" +``` + +--- + +### Task 5: Popup controls for card settings + +Add card layout controls to the extension popup: enable toggle, background style dropdown, card spacing dropdown. + +**Files:** +- Modify: `popup/popup.html` +- Modify: `popup/popup.js` + +**Interfaces:** +- Consumes: `settings.cardLayout`, `settings.cardBackground`, `settings.cardGap` (from Task 1 DEFAULTS) + +- [ ] **Step 1: Add card layout section to popup HTML** + +In `popup/popup.html`, add this section after the Dark Mode section and before the Light Mode Colors section: + +```html +
+

Card Layout

+ + + + + +
+``` + +- [ ] **Step 2: Wire card controls in `popup.js`** + +In `populateControls(settings)`, add: + +```js +document.getElementById("cardLayout").checked = settings.cardLayout; +document.getElementById("cardBackground").value = settings.cardBackground; +document.getElementById("cardGap").value = String(settings.cardGap); +``` + +In `saveFromControls()`, add to the settings object: + +```js +cardLayout: document.getElementById("cardLayout").checked, +cardBackground: document.getElementById("cardBackground").value, +cardGap: parseInt(document.getElementById("cardGap").value, 10), +``` + +In `init()`, add change listeners: + +```js +document.getElementById("cardLayout").addEventListener("change", saveFromControls); +document.getElementById("cardBackground").addEventListener("change", saveFromControls); +document.getElementById("cardGap").addEventListener("change", saveFromControls); +``` + +- [ ] **Step 3: Verify settings work end-to-end** + +Reload extension. Open popup: +- Card layout checkbox is checked by default +- Background dropdown shows "Reddit Default" +- Card spacing shows "16px" +- Changing card spacing to "24px": cards on old.reddit.com update gap live (no reload) +- Changing background to "shadcn Style": page background becomes `#f5f5f5` (live) +- Unchecking card layout: page reloads, cards disappear, posts show flat list +- Settings persist after closing and reopening popup +- Settings persist after browser restart + +- [ ] **Step 4: Commit** + +```bash +git add popup/popup.html popup/popup.js +git commit -m "feat: add card layout controls to extension popup" +```