Skip to content

Add the Neutral theme and a per-user theme picker - #293

Open
salman1993 wants to merge 16 commits into
smohammed/ui-themesfrom
claude/neutral-theme-9c200c
Open

salman1993 wants to merge 16 commits into
smohammed/ui-themesfrom
claude/neutral-theme-9c200c

Conversation

@salman1993

Copy link
Copy Markdown
Collaborator

What changed and why

Stacked on #290 (the theme system). This PR adds a second theme, Neutral, and lets each person pick their theme. Classic stays the default, so Classic readers see the same page, with one exception: footnote markers and other raised text no longer push their line apart (see the last bullet).

  • Neutral is a quieter reading look: warm grays, the system font, no glass or page backdrop, the document flat on the page, a 68ch line length, quieter code blocks with a corner language label, 6px image corners, and its own Mermaid palette, in light and dark mode. It lives in engine/app/assets/stylesheets/coplan/themes/neutral.css and sets custom properties only, the hooks from docs/THEMES.md. Each color gives both modes in light-dark(). Only the Mermaid hex palette has dark-mode blocks, because Mermaid reads it with getComputedStyle.
  • Neutral's headings use the shared size scale that Classic uses: a 28px plan title over 24px, 20px and 18px headings and 16px body text. H4s are weight 600, so they don't read as body text. On phones the title is 26px, so it stays above the document's H1.
  • Shadows come in three heights: a hairline for page cards and pills, a small drop for buttons and hover lifts, and the full drop for menus, dialogs and floating panels. In dark mode the floating-panel shadow is transparent and the 1px border marks the edge.
  • New hooks for every theme: --image-radius (Classic keeps --radius) and --reading-h4-weight. Decks reset --image-radius.
  • Picker: Settings has an Appearance group with a Theme row (Default, Classic, Neutral) next to the existing light/dark Mode row. Both rows are radio groups named by their labels. A pick applies at once without a reload, saves with PATCH /settings/app_theme, and reverts if the save fails.
  • Appearance menu: a half-filled circle button in the top bar, between search and notifications, opens a menu with System/Light/Dark and Classic/Neutral. The theme that's showing is checked. It stays in step with Settings, including on pages Turbo restores from its cache.
  • Storage: new column coplan_users.app_theme, default "default" (follow the host's config.default_theme). Themes.resolve turns a stored choice into the theme it shows. A stored theme that a later CoPlan release removes falls back to the default.
  • Decks keep their own fonts and colors: .deck resets the theme's fonts, image radius and all twelve Mermaid palette variables, and Mermaid reads its label font and palette from the diagram's container.
  • Shared fixes found while building Neutral:
    • In edit mode the editable plan title reads the same title hooks as the plain title, so clicking Edit no longer resizes it. Classic's values don't change.
    • sup and sub in rendered Markdown have line-height: 0 in every theme, so a footnote marker keeps its line's height. The editor's citation button sets its own line height so it stays clickable.

Hosts must copy the new migration with bin/rails co_plan:install:migrations.

Evidence

Screenshots to follow.

How to try it

  1. Run the migration, then sign in at http://localhost:3000/sign_in as hampton@example.com (no password).
  2. Click the half-filled circle in the top bar and pick Neutral. The page switches without a reload.
  3. Open /hampton/demo-content/rich-text/reading-specimen-webhook-delivery-through-a-durable-outbox. Expect warm grays, the system font, the document flat on the page, headings stepping down from the plan title, and right-aligned code labels. Switch between System, Light and Dark in the same menu.
  4. Click Edit. The title keeps its size and weight.
  5. Open Settings and pick Default in the Theme row. The page returns to the site default, Classic, and the bar's menu checks Classic.

Testing

  • New browser specs in spec/system/app_theme_spec.rb cover the picker (no-reload switch, Default, failed-save revert, quick picks in order), the bar's Appearance menu (in step with Settings, on screen at 320px, correct after a Turbo cache restore), Neutral's flat document, reading type and heading scale in both modes, editor typography, and deck diagram fonts and colors. changed_sections_ux_spec.rb now checks the mobile update dots under both themes.

  • spec/lib/deck_theme_isolation_spec.rb (from Add app theme abstraction with Classic as the default #290) passes with neutral.css.

  • bundle exec rspec on 7cc0c85: 3090 examples, 4 failures. Three were editor citation specs: the shared sup line-height made the editor's citation button zero-height. 12a5ba8 fixes that, and the three spec files (90 examples) now pass. The fourth, a template-notes spec, passed on rerun.

  • Added or updated real JavaScript-enabled browser specs in spec/system/ for changed UI behavior, covering the user interaction and visible result; or explained why no browser UI is involved.

  • Added model, service, or request specs where needed for domain behavior, authorization, or API cases.

  • Ran bundle exec rspec, or explained why it could not run and listed the specs that did run.

  • Tested affected UI behavior in both light and dark mode, or explained why mode testing does not apply.

  • Attached a video or images demonstrating the change, if this PR adds or changes a feature or fixes a UI bug.

🤖 Generated with Claude Code

salman1993 and others added 15 commits October 30, 2026 16:58
Mermaid labels took their font from <html>, so a theme that changes
--font-sans would restyle diagrams inside slides too. foundation.css now
names the built-in stacks --font-builtin-sans and --font-builtin-mono,
and decks set --font-sans and --font-mono back to them. Mermaid reads
the font from the diagram's container, and its configuration cache keys
on the font as well as the palette, so a deck and a document diagram on
one page each get their own. Classic looks the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Neutral is the reading look from the warm-neutral reading view, ported
into theme hooks: warm grays, the system font, no glass or page
backdrop, the document flat on the page, quieter code blocks and its
own Mermaid palette, in light and dark. It also hides an opening H1
that repeats the plan title; render_content_regions(title:) marks those
documents, and Classic ignores the mark.

Each person picks a theme under Settings > Appearance > Theme, next to
the existing light/dark Mode row. coplan_users.app_theme stores
"default" (follow the host's config.default_theme) or a theme key. The
pick applies without a reload, saves with PATCH /settings/app_theme,
and reverts if the save fails. A stored theme the host later removes
falls back to the default and doesn't block saving the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A half-filled circle beside notifications opens a small popover with
the light/dark mode (System, Light, Dark) and the app theme (Classic or
Neutral, the one showing checked). Picks apply at once, save in the
background like Settings, and the popover stays open to compare. It
positions on beforetoggle, so it never paints a frame at the wrong spot.

The menu reuses the mode and theme controllers. They now keep each
other's radios in step, so the menu and Settings agree when both are on
screen, and the app theme controller takes the stored choice from the page,
since the menu has no Default option to read it from. Segmented controls
also get a visible keyboard focus ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Themes.default now returns a Theme, and Themes.resolve(choice) owns the
fallback from a stored choice to the theme it shows: the named theme, else
the host default, else Classic. The helpers and both pickers use it.

Also note why the picker has its own coplan:app-theme event, and say that
CoPlan may remove a theme in a later release rather than "retire" one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mermaid took the label font from the diagram's container but the palette
from <html>, with a separate deck check to skip it. Both now come from the
container, and .deck resets every palette variable to initial, so slides
keep Mermaid's built-in themes the same way they keep the deck's fonts.
The configuration cache also keys on the palette values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Neutral hid an opening H1 that repeated the plan title. That made the
theme change what the page shows, not only how it looks. Drop the rule,
MarkdownHelper#repeats_title?, the title: argument and the title in the
content cache key. Neutral's document H1 is now 1.25rem, under the 1.75rem
plan title, so a repeated title reads as a heading below it.

THEMES.md now says themes set custom properties only, and that a fix for
the repeated title belongs to every theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Neutral repeated every color in a forced-dark block and again in a
system-dark block. Each color now carries both values in light-dark(),
which follows the mode's color-scheme. Shadows have the same three layers
in both modes, with transparent where a mode has none, so each mode looks
as it did. Only the Mermaid hex palette keeps per-mode blocks, because
getComputedStyle returns light-dark() unresolved.

A computed-style diff of the reader, editor and Settings, light, dark and
system, desktop and phone, matched before and after apart from those
transparent shadow layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phones size the plan title with --plan-title-size-narrow, 1.25rem by
default, the same as Neutral's new document H1. Neutral sets it to
1.5rem, so a repeated title still reads as the heading below the title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Neutral set its own H1, H2 and H3 sizes, and its 20px H1 came out smaller
than its 22px H2. Drop all three, so headings take the shared scale Classic
uses: 24, 20 and 18px under the 28px plan title. Neutral keeps its heading
leading, spacing, weight and tracking.

Also drop the 1.5rem phone plan title from b9194aa: it made a heavy header
at 390px. On phones the H1 is now larger than the 20px plan title; the fix
for that is the all-themes handling of a repeated title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only lightness changes, so each color keeps its hue and chroma.

- Dark muted text and the code label go from L 0.68 to 0.75 (APCA
  Lc -45 to -57), and dark code comments to 0.72. They were dimmer
  than Classic's dark muted text.
- Dark keyword, tag and deletion colors go from L 0.76 to 0.8, so
  they are as bright as the other syntax colors (Lc -63).
- The light deletion background drops to C 0.025, inside sRGB.
- Sequence diagram lifelines reach 3:1 against the diagram in both
  modes (#8f8c84 light, #6b6964 dark).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Floating panels already have a 1px border, so the shadow drops its
  ring. In dark mode the shadow is now transparent: the black ring
  made the edge look thick and muddy, and the drop couldn't be seen.
- Add an --image-radius hook (Classic keeps --radius). Neutral sets
  6px, so images match code blocks, diagrams and tables. The image
  resize handle follows it, and decks reset it.
- The dark diagram background moves to #252422, the same tint as a code
  block. Clusters go to #282725 to stay above it, and lifelines to
  #706e69 to keep 3:1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From a UI review of Neutral:

- Set the reading measure to 68ch. 80ch ran 100-115 characters a line,
  because a ch is the width of "0", not an average letter.
- Give shadows three heights instead of one. Page cards and pills get a
  hairline drop, buttons and hover lifts a small one, and menus, dialogs
  and floating panels keep the full drop.
- Add --reading-h4-weight and set it to 600 in Neutral, so an H4 (body
  size) no longer reads as body text at weight 500.
- Bring back --plan-title-size-narrow, so the plan title stays above the
  document's H1 on phones.
- Keep sup and sub from raising their line, in every theme. Neutral's
  1.6 leading made footnote markers push lines apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From a ship check of Neutral:

- The editable title now reads the same title hooks as the plain
  title. Before, `.page-header h1` set it to 24px at weight 700, so
  clicking Edit in Neutral shrank the 28px/500 title, made it bold, and
  moved the body by about 5px. Classic's values don't change.
- The Theme and Mode segments on Settings are radio groups named by
  their row labels. A screen reader now says which choice a radio
  belongs to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80e1d12 gave sup and sub a line-height of 0. The editor's citation
button inherits its font from the sup, so it took that line height too
and drew at zero height: it couldn't be clicked, in every theme. The
button now sets its own line height.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@salman1993

salman1993 commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator Author

screenshots of the Neutral theme:

Screenshot 2026-10-31 at 12 26 11 PM Screenshot 2026-10-31 at 12 26 26 PM Screenshot 2026-10-31 at 12 26 16 PM image-1793464176221 image-1793464167741 Screenshot 2026-10-31 at 12 26 02 PM

@salman1993
salman1993 marked this pull request as ready for review October 31, 2026 18:11
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T18:14:09.138736Z 12a5ba8 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12a5ba81bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Settings and the bar's Appearance menu each have an app-theme
controller, and each kept its own save queue. A pick in Settings
followed quickly by one in the menu sent both saves at once, and if
the first landed last the server kept the older theme while the page
showed the newer one. The queue now lives at module level, so every
control's saves go out in the order they were picked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant