Add implementation plan for card layout feature

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 20:58:51 -04:00
parent cab7c61c07
commit d659238289

View File

@@ -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 `<html>` (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
<div class="section">
<label class="toggle-row">
<span>Enabled</span>
<input type="checkbox" id="enabled">
</label>
<label class="toggle-row">
<span>Badge layout</span>
<input type="checkbox" id="badgeLayout">
</label>
</div>
```
- [ ] **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
<div class="section">
<h2>Card Layout</h2>
<label class="toggle-row">
<span>Enable card layout</span>
<input type="checkbox" id="cardLayout">
</label>
<label for="cardBackground">Background Style</label>
<select id="cardBackground">
<option value="reddit">Reddit Default</option>
<option value="shadcn">shadcn Style</option>
</select>
<label for="cardGap">Card Spacing</label>
<select id="cardGap">
<option value="8">8px</option>
<option value="12">12px</option>
<option value="16">16px</option>
<option value="24">24px</option>
<option value="32">32px</option>
</select>
</div>
```
- [ ] **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"
```