Skip to content

Heading sizes: same level shows different sizes, and levels look too alike #367

Description

@HMarzban

What happened

Edward Saperia reported two problems with heading sizes. Both come from the automatic heading size algorithm (HeadingScale). This is urgent, because the outline is what makes a pad skimmable.

  1. Sizes keep changing. Headings of the same level appear at different sizes on the same page.
  2. Levels look alike. The sizes are not different enough to skim. In his pad, Teams (h2) does not stand out from the h3 headings around it. Those are Table G, Table H, Table Z, and Team1 to Team3.

What we expect

The algorithm sets heading size by itself, and a reader can tell the levels apart at a glance. Two headings of the same level in one section always have the same size. An h2 clearly stands above the h3 headings around it.

Steps to reproduce

Problem 1, the same level at two sizes:

  1. Make an outline: Title, then h2 "A", h3 "a1", h2 "B", h3 "b1".
  2. In the table of contents, drag section "B" above section "A".
  3. Read the computed font size of both h3 headings.

Problem 2, levels look alike:

  1. Make one section that uses many levels, for example h2, h3, h4, h5 and h6.
  2. Compare the sizes of neighbouring levels.

Findings

We ran the real plugin and the real moveSection in a Bun script on sample outlines. This measured plugin output, not pixels in a browser. We have not seen Edward's document, so we do not know which levels it uses.

  • A moved section loses its size. The plugin keeps old decorations when the level sequence does not change. A moved section keeps the same sequence, so its headings get no size decoration and fall back to the CSS default. After the drag in step 2, "b1" is 14pt and "a1" is 12pt. Both are h3.
  • More levels mean smaller steps. Sizes spread evenly between 20pt and 12pt across the levels a section uses. Two levels give a step of 8pt, three give 4pt, four give 2.67pt, five give 2pt and six give 1.6pt. A step of 1.6pt is hard to see.
  • The title takes a rank but ignores the size. The first h1 counts as level 1 in section 1. The CSS draws it at a fixed 28pt and ignores its computed size. Every other level in that section shrinks toward the minimum for nothing. With a title plus h2 and h3, h2 is 16pt. Without the title's rank it would be 20pt.
  • A new level resizes the whole section. With a title, h2 and h3, h2 is 16pt and h3 is 12pt. Add one h4 and h2 becomes 17.33pt and h3 becomes 14.67pt. This reads as sizes that keep changing.
  • Each h1 restarts the ranking. An h2 in section 1 can be 17.33pt while an h2 in section 2 is 16pt.

Acceptance criteria

  • After a section move that keeps the level sequence, every heading shows its rank size. Two headings of the same level in one section have the same computed font size.
  • The same holds after cut and paste, undo and redo, and a remote edit.
  • The review sets a minimum step between neighbouring levels. It records that step in apps/webapp/src/components/TipTap/CLAUDE.md §HeadingScale. In a section that uses all six levels, no two neighbouring levels are closer than that step.
  • In a section with h2 and h3 headings, an h2 is clearly larger than the h3 headings around it. Edward's Teams example passes.
  • The review states whether the title still takes a rank, and whether the size can change across section boundaries. It writes the answer in the same section.
  • A test fails before the fix and passes after it. It covers the moved section case.
  • Typing in a heading still causes no full rebuild of the decorations.

Blocked by

None — can start now.

Agent brief

Type: HITL for the review decision. AFK for the moved section fix and its test. The maintainer picks the minimum step and the title rule before the algorithm changes.

Category: bug

Current behavior: See Findings. buildDecorations ranks the distinct levels of each section. The plugin apply step rebuilds only when the level fingerprint changes or a remote y-sync$ transaction arrives. Otherwise it maps the old decorations. A node that a step deletes and inserts again keeps no decoration under that mapping.

Desired behavior: Same level, same size in one section, on every edit path. Neighbouring levels are visibly different. The rules are written down.

Where to start: apps/webapp/src/components/TipTap/extensions/heading-scale/heading-scale.ts (buildDecorations, computeHeadingFingerprint, plugin apply). apps/webapp/src/components/TipTap/extensions/shared/move-section.ts (moveSection). apps/webapp/src/styles/editor/_heading-node.scss (--hd-size, the fixed first h1, the per-level fallbacks). Search by symbol. File names are hints as of 2026-09-29.

Rules that apply: apps/webapp/src/components/TipTap/CLAUDE.md §HeadingScale is a mandatory spec. It ranks levels inside a section, sets each h1 as a new section, puts the title in section 1, and forbids fixed per-level point maps. Changing the rules is the point of this review. Get a maintainer ruling, then update that section in the same change. Use decorations only, and never write sizes into the document. §Editor Performance: avoid full rebuilds on every keystroke. CONTEXT.md §Pad outline holds the names. AGENTS.md §Test Policy allows a test here, because it pins a reported bug.

Verify: Start make dev-local and confirm the webapp port. Build the outline from Steps to reproduce. In the browser console, run [...document.querySelectorAll('.ProseMirror h2, .ProseMirror h3')].map(h => [h.tagName, h.textContent, getComputedStyle(h).fontSize]). Same level must give the same size. Check light and dark themes, then desktop and a phone. Also open a pad with two h1 headings and compare the sizes across the boundary.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    EditorTiptap & ProsemirrorUIUrgentbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions