Repository source: docs/plans/thread_compact_view/thread_compact_view_plan_v1.md
Compact view for thread listings
Context
Users currently see thread lists (board/tag pages) as full cards: title, meta line, full body-preview text, and reply count, with generous spacing between cards. This makes it hard to scan many threads at once. We're adding a "compact view" that hides the body preview and tightens spacing, so more threads fit on screen at once. It should be a global, persistent preference (like the existing theme setting), apply to every page that lists threads as cards, and also be overridable via a URL query parameter for shareable/explicit links.
Scope confirmed with user: applies to all thread-listing pages — the board's all/liked views and newest/oldest/top sorts (templates/pages/board.php), and per-tag thread lists (templates/pages/tag.php). The tags index (templates/pages/tags.php) already shows a compact preview list with no body text, so it needs no changes. Thread detail pages (templates/pages/thread.php) are out of scope — this is about scanning the list, not reading an open thread.
Persistence confirmed with user: primarily a toggle button + localStorage, mirroring the existing theme toggle exactly, with a ?density= URL query param as an override that also updates the saved preference.
Existing patterns being reused
FOUC-free preference toggle:
templates/layout.php:7-18inline script readszenmemes-themefromlocalStorageand setsdata-themeon<html>before first paint;public/assets/theme_toggle.jshandles the interactive toggle and writes back tolocalStorage. We replicate this exactly for density instead of theme.Script/asset wiring:
src/ForumRewrite/View/TemplateRenderer.php:65-67passes script paths (e.g.themeToggleScriptPath) intolayout.php, which emits<script defer>tags (layout.php:29) and loads partials liketheme_menu.phpintosite-header-actions(layout.php:49).Duplicate thread-card markup:
board.php:41-51andtag.php:9-21render nearly identical thread cards (title/meta/body-preview/reply-count, differing only in a pinned marker vs. a labels line). Extracting a shared partial avoids implementing compact-mode markup twice.
Implementation
1. New shared partial: templates/partials/thread_card.php
Extract the thread-card markup currently duplicated in board.php:41-51 and tag.php:9-21 into one partial, parameterized with thread and a small badges option (pinned marker for board, labels line for tag). Structure it as:
<article class="card thread-card" data-heat="...">
<h2>...</h2>
<p class="meta">...</p>
<p class="thread-card__preview">body preview</p>
<p class="meta">reply count</p>
</article>
The thread-card__preview class is the hook compact mode hides. Update board.php and tag.php to call $partial('partials/thread_card.php', [...]) in place of their inline loops. Wrap the <section class="stack"> in both templates with an added thread-list class (e.g. class="stack thread-list") so compact spacing rules don't leak into unrelated .stack usages elsewhere (forms, thread.php's reply stack, etc.).
2. Preference storage & FOUC prevention: templates/layout.php
Extend the existing inline script (lines 7-18) to also:
Read a
densityquery param fromlocation.search; if it'scompactorcomfortable, apply it immediately and write it tolocalStorageunder a new keyzenmemes-thread-density.Otherwise, read
zenmemes-thread-densityfromlocalStorage.Set
data-thread-density="compact"on<html>when compact is active (mirrors howdata-themeis set); omit the attribute for the default/comfortable state.
3. Toggle control
New partial
templates/partials/thread_density_toggle.php: a single button (simpler than the theme popover since there are only two states), e.g.<button data-role="thread-density-toggle" data-action="thread-density-toggle" aria-pressed="false">Compact</button>, styled similarly to.theme-menu__trigger.Add it into
site-header-actionsinlayout.php:49, next totheme_menu.php, so it's globally visible (consistent with the theme toggle's placement).New script
public/assets/thread_density_toggle.js, modeled directly ontheme_toggle.js: on load, sync from thedensityquery param (if present) orlocalStorage; on click, toggledata-thread-density, updatearia-pressed/label, and persist tolocalStorage.Wire it up in
TemplateRenderer.php:66('threadDensityToggleScriptPath' => $this->assetPath('/assets/thread_density_toggle.js'),) and add the corresponding<script defer>tag inlayout.phpnear line 29.
4. CSS: public/assets/site.css
Append near the end of the stylesheet (after the per-theme override blocks, so cascade order wins over theme-specific .card/.stack rules):
:root[data-thread-density="compact"] .thread-card__preview { display: none; }
:root[data-thread-density="compact"] .thread-list .thread-card { padding: 0.45rem 0.6rem; }
:root[data-thread-density="compact"] .thread-list > * + * { margin-top: 0.35rem; }
Plus small button styling for the new toggle, consistent with existing .theme-toggle styling.
Files touched
templates/partials/thread_card.php(new)templates/partials/thread_density_toggle.php(new)public/assets/thread_density_toggle.js(new, modeled onpublic/assets/theme_toggle.js)templates/pages/board.php(use shared partial, addthread-listclass)templates/pages/tag.php(use shared partial, addthread-listclass)templates/layout.php(extend FOUC script, add toggle partial + script tag)src/ForumRewrite/View/TemplateRenderer.php(wire new script path)public/assets/site.css(compact-mode rules + toggle button styling)tests/LocalAppSmokeTest.php(extend)
Verification
Extend
tests/LocalAppSmokeTest.php's route-rendering assertions to check the toggle button markup (data-role="thread-density-toggle") is present on board/tag pages, and that the extractedthread_card.phppartial still renders the expectedclass="card thread-card", title link, and reply-count text on bothboard.phpandtag.phproutes.Manual browser check via the
runskill: load/,/threads/?view=liked,/threads/?sort=top, and/tags/<some-tag>; click the compact toggle and confirm body previews hide and spacing tightens on all of them; reload and confirm the preference persists; clearlocalStorage, visit/?density=compactfresh, and confirm it applies immediately and then persists on subsequent navigation without the param.