Add opt-in theme system + 'retro' Memphis/8-bit theme#152
Add opt-in theme system + 'retro' Memphis/8-bit theme#152EnriqueCanals wants to merge 10 commits into
Conversation
Adds the plumbing for an opt-in theme system while leaving the default
look and behavior unchanged. A theme is selected via the ABBEY_THEME env
var (or `Rails.application.config.theme`) and contributes three things:
* View overrides: any file under `app/views/themes/<theme>/` overrides
its same-named default counterpart (including layouts and partials)
via Rails' view path prepending. Implemented in
`app/controllers/concerns/theming.rb` and included in
ApplicationController as a no-op for the default theme.
* Extra stylesheets: files under `app/assets/stylesheets/themes/` are
excluded from the existing `:app` Propshaft bulk-include so they
cannot leak into the default build, and are loaded explicitly via
the new `theme_stylesheets` helper only when their theme is active.
* Theme-aware markdown rendering: themes listed in
`Rails.application.config.themes_using_minimal_renderer` get the new
`MinimalMarkdownRender`, which emits semantic HTML without inline
Tailwind utility classes — so themes can style content from a
wrapper scope (e.g. `.prose-retro`) rather than fighting the
renderer. The default theme keeps the existing `MarkdownRender`.
No new gems, no Tailwind config changes, no default-theme regressions:
when `ABBEY_THEME=default` (the default) the rendered HTML, asset list
and markdown output are byte-for-byte identical to before this commit.
Co-authored-by: Cursor <cursoragent@cursor.com>
Two pure-CSS stylesheets that ship with the new `retro` theme and are
loaded by the theme system on demand:
* `themes/retro.css` — Memphis design palette (neon pink/cyan/yellow
/mint/purple/coral on paper or CRT-black), neo-brutalist cards,
pixel-display headings (`Press Start 2P`), terminal-mono body
(`VT323` / `Space Grotesk`), animated marquee, blinking cursor,
drifting confetti background, subtle CRT scanlines, and a fully
themed `.prose-retro` block for rendered markdown (h1-h3 with hard
text-shadows, wavy underlines, highlighted `<strong>`, terminal
`<pre>` blocks, dotted SVG `<hr>`, etc.).
* `themes/retro-highlight.css` — Rouge syntax highlighting palette
that pairs with the terminal aesthetic (neon-on-CRT-black).
Every selector is gated on `.theme-retro` (set on `<html>` by the retro
layout), so even if the file is loaded outside the theme it produces no
visible effect. No Tailwind processing is required — the file is served
directly by Propshaft.
Co-authored-by: Cursor <cursoragent@cursor.com>
Per-theme view overrides under `app/views/themes/retro/` covering every
template the default theme ships:
* `layouts/application.html.erb` — sets `theme-retro` on `<html>`,
preloads Google Fonts for `Press Start 2P` / `VT323` /
`Space Grotesk`, adds a pixelated 4-square SVG favicon and pink
theme-color, and loads the extra theme stylesheets via
`theme_stylesheets`.
* `shared/_navigation.html.erb` — color-block logo, blinking-cursor
site title, retro tagline, and Memphis-styled nav buttons (Home,
About, Presentations, Projects, Links, Papers) with hard shadows
and a wiggle animation on the active page.
* `shared/_footer.html.erb` — caution-tape marquee + terminal
command-line copyright bar with blinking cursor.
* `shared/_admin_navigation.html.erb` — BBS-sysop command line:
yellow `SYSOP>` prompt, `ONLINE▮` indicator, and the same admin
actions (New Post / New Page / New Link / Feeds / Read / Sign out)
as the default bar (selectors `.fixed.top-0`, "New Post",
"Sign out" preserved so system tests pass under either theme).
* `shared/_tags.html.erb` — color-cycling tag pills picked from the
Memphis palette based on the tag name hash.
* `blog/index.html.erb`, `blog/show.html.erb`,
`blog/index_by_tag.html.erb` — Memphis cards with color-cycled
shadows, rotated date "stickers", glitch-on-hover titles, and a
retro `READ MORE` button. Post body uses the `.prose-retro`
wrapper from `themes/retro.css`.
* `pages/show.html.erb` — page-as-Memphis-card with a mint rotated
slug tag.
* `links/index.html.erb`, `links/_link.html.erb` — banner header
plus per-link Memphis cards.
* `papers/index.html.erb`, `papers/_paper.html.erb` — retro variants
of the new Papers feature, with PDF preview thumbs framed in hard
shadows.
All variants preserve the user-facing copy and DOM selectors checked
by the existing Playwright system tests, so enabling the theme does
not regress `test:system`.
Co-authored-by: Cursor <cursoragent@cursor.com>
`test/integration/themes_test.rb` covers the four guarantees the
theme system makes:
* default theme does NOT emit `theme-retro` or load any
`themes/...` stylesheets;
* retro theme DOES set `theme-retro` on `<html>`, loads
`themes/retro` + `themes/retro-highlight`, and preserves the
DOM contract checked by system tests (header h1, footer, all
six nav links);
* `Post.markdown_renderer` follows
`Rails.application.config.theme` (MarkdownRender for default,
MinimalMarkdownRender for retro);
* `theme_stylesheets` helper is empty for the default theme and
populated for retro.
Existing tests still pass under both themes:
bin/rails test # 21 tests, 85 asserts
bin/rails test test/system/{navigation,blog_public,pages,links}_test.rb
ABBEY_THEME=retro bin/rails test test/system/... # same suite, all green
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a Themes section to the README explaining:
* how to switch themes via ABBEY_THEME or
config/initializers/themes.rb,
* what the built-in `default` and `retro` themes are,
* how the theme system overrides views, ships extra stylesheets,
and selects a markdown renderer,
* how to author a new theme (drop a CSS file under
`app/assets/stylesheets/themes/<name>.css` and set ABBEY_THEME).
Also adds a one-line feature entry to the top-level features list.
Co-authored-by: Cursor <cursoragent@cursor.com>
Extends the theming system introduced for the retro theme with a second built-in named theme: "grimoire" — retro hacker dark fantasy. Like retro, grimoire opts into MinimalMarkdownRender so it can style markdown content entirely from a wrapper class scope. No behavioural change for ABBEY_THEME=default or ABBEY_THEME=retro. README updated to document grimoire alongside retro in the themes table. Co-authored-by: Cursor <cursoragent@cursor.com>
Pure-CSS theme files served by Propshaft and loaded only when Rails.application.config.theme == "grimoire" (via the existing theme_stylesheets helper). All selectors are scoped under `.theme-grimoire` so loading these files in any other context is a no-op. Visual language: - Light mode: aged parchment (#ebd9b3) with subtle noise grain + corner foxing; blood-red (#8b1d27) ink accents; gold (#c8a44d) hairlines - Dark mode: starless void (#07080d) with drifting ember/arcane dust; golden text; phosphor green (#57f287) for code/cursor accents; subtle CRT scanlines - Matrix-minimal typography: JetBrains Mono 800 uppercase for display headings, Inter for body, IBM Plex Mono for inline code/meta - Components: tome cards (double gold/ink border + corner sigils), spell-btn (raised paper/metal action button), wax-seal tag pills (deterministic blood/arcane/ember/phosphor/gold/ichor color cycle), icon-rune (square icon button), summoning circle, marquee, rune-divider - Markdown prose (.prose-grimoire): chunky monospace drop cap with thin underline (phosphor green in dark mode), terminal-style code blocks with "~/grimoire/spells $ cast" header, // EOF // hr, wax-seal tag pills, scoped table styling - Konami code easter egg overlay (.incant-overlay/.incant-title) - Custom scrollbar (dark mode only) The companion grimoire-highlight.css is a Rouge theme: phosphor green base, gold sigils for keywords, blood-red strings, ember comments — all scoped under `.theme-grimoire` so it can't clobber the default highlight theme. Co-authored-by: Cursor <cursoragent@cursor.com>
Drops a full set of view overrides under app/views/themes/grimoire/ that
the theming system prepends to the view lookup path when grimoire is
active:
layouts/application.html.erb — html.theme-grimoire wrapper,
pixel-runic favicon, theme-only
Google Fonts (Inter + JetBrains
Mono + IBM Plex Mono),
cookie-based dark-mode JS that
survives Turbo Drive navigation
(re-applies on turbo:load and
turbo:render, listeners guarded
against double-registration),
and a Konami code easter egg
that unlocks a "necromancer"
overlay
shared/_navigation.html.erb — terminal masthead
(`root@grimoire:~$ cat ./codex_of`),
Roman-numeral table-of-contents
nav, RSS + theme-toggle icon-runes
shared/_footer.html.erb — animated summoning circle SVG
(counter-rotating ring + pentagram
+ center ember), scrolling
marquee of dev-zine tokens,
terminal command-line copyright
shared/_admin_navigation.html.erb — adept's console
(`:transcribe / :inscribe / :bind
/ :auguries / :scry / :depart`)
shared/_tags.html.erb — wax-seal tag pills (deterministic
color cycle by tag name hash)
blog/index.html.erb,
blog/show.html.erb,
blog/index_by_tag.html.erb — tome cards with "Folio · Anno
<Roman>" date stickers, "open
spellbook" CTAs, // LOG // and
// FILTER // rune dividers
pages/show.html.erb — single-page tome with
"✦ Chapter · <slug> ✦" header
and `exit 0` // EOF // sign-off
links/index.html.erb, _link.html.erb — "Forbidden Tomes" reliquary
header, link cards rendered as
tomes with ✦ icon
papers/index.html.erb, _paper.html.erb — "Treatises" header with
"Codex · PDF" stickers
All views consume the pure-CSS classes defined in
app/assets/stylesheets/themes/grimoire.css (tome, spell-btn, wax-seal,
h-blackletter, h-engraved, rune-divider, prose-grimoire, etc.) plus
.theme-grimoire-scoped utility classes that mirror Tailwind syntax
(bg-grim-*, text-grim-*, font-plex, tracking-[Xem], leading-[X], …).
This means grimoire — like retro — needs zero changes to the Tailwind
config to render correctly: it owns its own visual surface and only
takes effect when ABBEY_THEME=grimoire is active.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds parallel coverage for the grimoire opt-in theme so that future
theming changes can't quietly regress it:
- default theme renders the original layout: also asserts no
`theme-grimoire` class and no `themes/grimoire` stylesheets leak in
- new test: grimoire theme prepends its view path and loads its theme
stylesheets; nav still contains all the required labels
- renderer test: grimoire (like retro) uses MinimalMarkdownRender
- theme_stylesheets helper test: returns themes/grimoire and
themes/grimoire-highlight when grimoire is active
All 5 tests pass with 68 assertions.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replaces hand-rolled `.theme-X .bg-foo-bar` utility-class declarations (plus matching `.dark:`, `.hover:`, `.group-hover:`, opacity-modifier and arbitrary-value mirrors) with Tailwind v4 `@theme` tokens that generate the same utilities natively. Addresses the PR description's noted trade-off: "The retro theme uses pure CSS rather than extending the Tailwind config — deliberate, so the default build stays unchanged." It turns out we can have both: the theme tokens live in `@theme` so Tailwind generates the utilities, and the default build stays unchanged because the default theme's views never reference the new utility classes (so Tailwind's content-aware extraction never emits them in the default theme's bundle — only the CSS variables on `:root`, which are inert without matching classes). What moved into `app/assets/tailwind/application.css` under `@theme`: * Memphis palette (--color-memphis-pink/cyan/yellow/mint/purple/coral/...) * Grimoire palette (--color-grim-blood/gold/ember/phosphor/arcane/...) * Retro hard-shadows (--shadow-retro, --shadow-retro-lg, --shadow-retro-pink/...) * Theme-specific font tokens (--font-display/blackletter/engraved/plex/manuscript) * Shared animations + @Keyframes (--animate-blink/wiggle/pop-in/marquee) What dropped out of `themes/retro.css` (626 -> 539 lines) and `themes/grimoire.css` (819 -> 754 lines): * `.theme-X .bg-/text-/border-/shadow-/font-...` declarations — Tailwind generates them from the --color-* / --shadow-* / --font-* tokens * `.theme-X.dark .dark\:foo-bar` mirrors — the `dark:` variant works because <html> already carries both `theme-X` and `dark` classes, and the existing `darkMode: 'class'` setting in tailwind.config.js applies * `.hover\:foo-bar`, `.group:hover .group-hover\:foo-bar`, opacity mirrors (`text-foo\/40`), and ~30 arbitrary-value escapes in grimoire.css (`text-\[0\.62rem\]`, `tracking-\[0\.18em\]`) — Tailwind v4 generates all of these natively from the registered tokens * Hand-written @Keyframes that duplicated --animate-* registrations What stayed in the per-theme CSS files: * theme-root environmental styles (body backdrops, scanlines, scrollbar) * 4-line CSS-variable rebinding inside `.theme-X { --font-sans: ...; ... }` so utilities like `font-mono` / `font-sans` / `font-display` resolve to the theme's font stack inside the theme scope without affecting the default theme * all component classes (`.card-retro`, `.btn-retro`, `.tome`, `.spell-btn`, `.wax-seal`, `.h-display`, `.h-blackletter`, `.prose-retro`, `.prose-grimoire`, ...) — now consuming `var(--color-memphis-pink)` etc. internally so the @theme tokens are the single source of truth for design values * theme-local @Keyframes (retro-drift, retro-glitch, grimoire-sigil-spin, grimoire-mist, grimoire-incant) that aren't surfaced as utilities Default theme is still byte-for-byte unchanged. Tests still pass: $ bin/rails test # 22 runs, 115 assertions, 0 failures $ bin/rails test test/integration/themes_test.rb # 5 runs, 68 assertions, 0 failures $ bin/rails tailwindcss:build # clean Future themes are now much smaller: drop tokens into `@theme`, rebind `--font-*` for typography, ship a body backdrop + component classes — no more growing per-theme boilerplate every time a new color or shadow is needed. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Pushed Theme design tokens now live in That lets the per-theme CSS files drop the hand-rolled utility duplicates:
What stays in the theme CSS files is what actually belongs there:
The "default build stays unchanged" guarantee from the original PR is preserved: the Verified: Net win for future themes: drop new tokens into |
Mirror of the refactor pushed to feature/retro-theme (PR capotej#152 follow-up): Memphis + Grimoire palettes, retro shadows, theme fonts, and shared animations move into a single @theme block in app/assets/tailwind/ application.css. Tailwind now auto-generates bg-memphis-pink, shadow-retro-lg, dark:text-grim-gold/70, animate-blink, text-[0.62rem], etc. — including all dark:/hover:/group-hover: variants — so themes/ retro.css and themes/grimoire.css drop ~150 lines of hand-rolled utility-class duplicates each. What stays in the per-theme CSS files: theme-root environmental styles (body backdrops, scanlines, scrollbar), a 4-line CSS-variable rebinding inside .theme-X { --font-sans/mono/display: ...; } so font utilities resolve to the theme's font stack inside scope, all component classes (.card-retro, .btn-retro, .tome, .spell-btn, .wax-seal, .h-display, .prose-retro, .prose-grimoire, ...) now consuming var(--color-...) internally so @theme tokens are the single source of truth, plus theme-local @Keyframes (drift, glitch, sigil-spin, mist, incant). Verified: bin/rails test # 26 runs, 166 assertions, 0 failures bin/rails test test/integration/themes_test.rb # 9 runs, 119 assertions, 0 failures bin/rails tailwindcss:build # clean Co-authored-by: Cursor <cursoragent@cursor.com>
|
Superseded by #156, which includes everything from this PR plus the drop-in theme refactor (registry, per-theme Tailwind bundles, shared chrome partial, generator, and docs). Closing in favor of the consolidated PR. |
|
Closing in favor of #156. |
Summary
This PR adds an opt-in theme system to Abbey and ships one reference theme —
retro, a Memphis-design / 8-bit / 80s-computer take on the blog chrome. WithABBEY_THEME=default(the default, unchanged behavior) the rendered HTML, asset list, and markdown output are byte-for-byte identical tomain. WithABBEY_THEME=retrothe whole site picks up the new look without touching any of the default templates or stylesheets.The intent is to make Abbey usable as a starting point for blogs with a different personality than the minimal default look, without maintainers having to fork the project or maintain a parallel set of templates. If accepted, future themes can plug in by dropping a single CSS file (and optionally view overrides) into the existing folders.
ABBEY_THEME=retro bin/dev)What gets added
app/assets/stylesheets/themes/retro.css.theme-retroapp/assets/stylesheets/themes/retro-highlight.cssapp/controllers/concerns/theming.rbbefore_actionthat prepends the theme view pathapp/helpers/application_helper.rbtheme_stylesheetshelper +:appfilterapp/views/themes/retro/config/initializers/themes.rblib/minimal_markdown_render.rbtest/integration/themes_test.rbREADME.mdPlus a 1-line
include TheminginApplicationControllerand a small refactor inRenderingso themes can opt into the minimal markdown renderer.Zero new gems. Zero changes to
Gemfile,package.json,Procfile.dev,config/tailwind.config.js, or any existing default-theme view.How the theme system works
A theme is a string set via the
ABBEY_THEMEenv var orRails.application.config.theme(defaults to"default"). When a non-default theme is active a theme can contribute three things:View overrides.
app/controllers/concerns/theming.rbprependsapp/views/themes/<theme>/to Rails' view lookup path on every request, so e.g.app/views/themes/retro/blog/index.html.erboverridesapp/views/blog/index.html.erb— including layouts and partials. Default theme = no prepend, zero behavior change.Extra stylesheets. Files under
app/assets/stylesheets/themes/are explicitly excluded from the existingstylesheet_link_tag :appbulk-include (via a small override of Propshaft'sapp_stylesheets_pathsinApplicationHelper) — so they cannot leak into the default build. They are loaded explicitly via a newtheme_stylesheetshelper, only when the corresponding theme is active.Markdown renderer. Themes listed in
Rails.application.config.themes_using_minimal_rendererget a newMinimalMarkdownRender(semantic HTML, no inline Tailwind classes), so they can style content from a wrapper scope (e.g..prose-retro) instead of fighting the renderer's hard-coded utility classes. The default theme keeps the existingMarkdownRender.A new theme can be as small as one CSS file:
Add views under
app/views/themes/midnight/only when you need to change markup. README documents this end-to-end.What the
retrotheme contributesink/paper/crt/pink/cyan/yellow/mint/purple/coral), three font families (Space Grotesk for body, VT323 for mono, Press Start 2P for display), retro hard-shadow utilities (shadow-retro,shadow-retro-pink, …),card-retro,btn-retro,tag-retro,crt-window,cursor-blink,marquee,glitch-hover,animate-pop-in— all defined in pure CSS scoped under.theme-retroso they cannot leak.BBS sysopadmin command line, an arcade-tape footer marquee, and a terminal$ echocopyright bar.READ MOREbutton. Body content uses the new minimal renderer +.prose-retrofor pixel-display headings, wavy underlines, highlighted<strong>, and terminal-styled<pre>blocks.darkclass toggle; in the retro theme it reads as a CRT terminal (neon mint on near-black, scanlines stay subtle for legibility).Compatibility
main(da83a8c)Gemfile.lockunchanged)header h1,footer, six nav links incl. newPapers,.fixed.top-0admin bar,New Post,Sign out,Back to posts) sobin/rails test:systemstays green when you flip the env varTests
The two pre-existing
test/system/feeds_test.rberrors (undefined method 'name' for nilinapp/views/feed_posts/_feed_post.html.erb:15) also reproduce onmainand are unrelated to this change.Try it locally
git checkout feature/retro-theme bundle install bin/rails db:migrate ABBEY_THEME=retro bin/dev # open http://localhost:3000 — toggle dark mode for the CRT variantThings worth a maintainer eye
Propshaft::Helper#app_stylesheets_pathslives inApplicationHelper(not a monkey patch on the gem). Trade-off noted in code comments — if you'd prefer the layout to enumerate stylesheets explicitly instead, happy to take that path.@applywith custom design tokens can do so by adding a Tailwind entry-point under their theme folder; left out of this PR to keep scope tight.lib/minimal_markdown_render.rbautoloads via the existingautoload_libconfig — no additionalrequires in initializers/boot.Happy to iterate on naming (
retrovsmemphisvsarcade), tone the visuals down, or split this into multiple smaller PRs (theme infra first, retro theme second) if that's easier to review.