Files
reddit-tweaks-ffe/docs/superpowers/plans/2026-08-26-card-layout.md
2026-08-26 20:58:51 -04:00

20 KiB

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:

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:

badgeLayout: true,
cardLayout: true,
cardBackground: "reddit",
cardGap: 16,

Add to the window.RedditTweaks assignment:

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:

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):

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

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:

html.rt-enabled .thing.link .tagline .subreddit,
html.rt-enabled .thing.link .flat-list li:has(> .comments) {
  display: none !important;
}

To:

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:

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:

<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:

document.getElementById("badgeLayout").checked = settings.badgeLayout;

Add change listener in init(), next to the existing enabled listener:

document.getElementById("badgeLayout").addEventListener("change", saveFromControls);

Add to saveFromControls() — in the settings object construction, add:

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

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

/* --- 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:

"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
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

(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:

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:

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:

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:

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:

"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):

"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

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:

<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:

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:

cardLayout: document.getElementById("cardLayout").checked,
cardBackground: document.getElementById("cardBackground").value,
cardGap: parseInt(document.getElementById("cardGap").value, 10),

In init(), add change listeners:

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

git add popup/popup.html popup/popup.js
git commit -m "feat: add card layout controls to extension popup"