Skip to content

feat(editor): give the recipe editor a real typographic system - #286

Merged
czearing merged 1 commit into
mainfrom
feat/editor-typography-system
Jul 31, 2026
Merged

feat(editor): give the recipe editor a real typographic system#286
czearing merged 1 commit into
mainfrom
feat/editor-typography-system

Conversation

@czearing

Copy link
Copy Markdown
Contributor

Measured the rendered TextEditor stories in Chromium before changing anything. Six defects were real: h1 and h2 were both 24px because the theme mapped both to sectionHeading; blockquotes were byte-identical to paragraphs; <hr> carried no class and fell back to Chrome's grey default instead of --ui-Divider; all spacing came from user-agent margins because Typography declares none; lines ran ~95-100 characters; and .listItem hardcoded its own font-size.

Approach

  • global.css gains a token vocabulary beside the existing --ui-* / --brand-* sets: a nine-step type scale, leading, tracking, weight, and --prose-* rhythm tokens. No stylesheet keeps raw literals.
  • Typography.module.css stays a margin-free shared primitive — it is consumed by RecipeHeader, RecipeCardGallery, RecipeCardCarousel, Settings, NewRecipeDialog, RecipeSearchFlyout and the recipes page, all of which space text with flex gap. It is only tokenised at unchanged computed sizes, plus letter-spacing and text-wrap: balance.
  • TextEditor.module.css owns the rhythm under .input > … selectors, which outrank the single-class primitives without touching them.
  • editorTheme composes both layers using Lexical's space-separated class lists, adds the missing hr key, and finally separates h1 from h2.

Why paragraphs keep zero margin

importRecipeMarkdown uses shouldPreserveNewLines, so markdown blank lines become real empty paragraphs. Adjacent paragraphs are therefore a soft line continuation and must sit flush, while a blank paragraph is the author's own one-line gap. Adding paragraph margins would double-space exactly where the author already asked for space. Structural blocks add their own margin-top on top of that, so separation is guaranteed either way.

Measured before → after

before after
heading ladder 24 / 24 / 20px 28 / 22 / 18px
block measure unbounded, 824px (~95-100 chars) 624px (~72 chars)
body leading 24px 26.4px
above vs below an H2 20px / 20px ~54px / ~26px
blockquote identical to a paragraph 3px brand rule, indented, muted italic
<hr> UA grey rgb(128,128,128) --ui-Divider

The measure is rem, not ch: ch resolves against each element's own font, which produced five different column widths (752 / 720 / 621 / 576px) in one document.

Validation

  • 22 TextEditor tests, including 4 new editor-theme regressions asserting the heading levels stay distinct, quotes stay separate from paragraphs, every rhythm-bearing block is themed, and lists share one rhythm class
  • Full suite: 360 passed across 42 suites
  • npm run lint clean, npm run build exit 0
  • Playwright before/after measurement of all three TextEditor stories at 1440x1000, light and dark, zero console errors

The editor had a type scale but no type system. Measuring the rendered
stories in Chromium showed h1 and h2 were both 24px because the theme
mapped them to the same class, blockquotes were byte-identical to
paragraphs, `<hr>` carried no class at all and fell back to Chrome's grey
default, every gap came from user-agent margins because Typography
declares none, and lines ran ~95-100 characters wide.

Adds a token vocabulary to :root — a nine-step type scale plus leading,
tracking, weight and prose-rhythm tokens — so no stylesheet holds raw
literals. Typography stays a margin-free shared primitive (it is used by
the recipe header, card gallery, carousel, settings and search flyout,
which position text with flex gap) and is only tokenised, so the rhythm
lives in TextEditor.module.css scoped under `.input >`, which outranks
the primitives without disturbing them. The theme composes both layers
via Lexical's space-separated class lists, adds the missing `hr` key, and
separates h1 from h2.

Paragraphs keep zero margin by design: markdown blank lines import as
real empty paragraphs under shouldPreserveNewLines, so adjacent
paragraphs are a soft line continuation and must sit flush, while a blank
paragraph is the author's own one-line gap. Structural blocks add their
own margin-top on top of that.

Measured: heading ladder 28/22/18px, one shared 624px measure (~72
characters) across every block, body leading 24px to 26.4px, and ~54px
above an H2 against ~26px below for correct proximity. The measure is
expressed in rem rather than ch because ch resolves against each
element's own font, which had produced five different column widths.

Validation: 22 TextEditor tests including 4 new theme regressions, 360
tests across the suite, lint clean, build green, and before/after
Chromium measurement of all three TextEditor stories in light and dark
with zero console errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: da97c874-01d2-4c51-a1ac-2c15f6f7ae48
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
book-cook Ready Ready Preview Jul 31, 2026 12:11am

@czearing
czearing enabled auto-merge (squash) July 31, 2026 00:07
@czearing
czearing merged commit be878c7 into main Jul 31, 2026
8 of 10 checks passed
@czearing
czearing deleted the feat/editor-typography-system branch July 31, 2026 00:12
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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