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.
- Sizes keep changing. Headings of the same level appear at different sizes on the same page.
- 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:
- Make an outline: Title, then h2 "A", h3 "a1", h2 "B", h3 "b1".
- In the table of contents, drag section "B" above section "A".
- Read the computed font size of both h3 headings.
Problem 2, levels look alike:
- Make one section that uses many levels, for example h2, h3, h4, h5 and h6.
- 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
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.
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.
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:
Problem 2, levels look alike:
Findings
We ran the real plugin and the real
moveSectionin 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.Acceptance criteria
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.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.
buildDecorationsranks the distinct levels of each section. The pluginapplystep rebuilds only when the level fingerprint changes or a remotey-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, pluginapply).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-localand 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.