Skip to content

feat(web): let the reader scale the interface font size - #893

Open
oisano11 wants to merge 1 commit into
JetBrains:mainfrom
oisano11:feat/font-scale-slider
Open

oisano11 wants to merge 1 commit into
JetBrains:mainfrom
oisano11:feat/font-scale-slider

Conversation

@oisano11

@oisano11 oisano11 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds a Font size control to Settings → Appearance that scales the whole interface from 80% to 160% in 5% steps.

Why

Every type size in the interface comes from apps/web/src/styles/typography.json, which is a build-time source of truth. That is the right design for the design system, but it leaves a reader who needs larger text with exactly one recourse: rebuild the app. This gives them a control instead.

How

The slider writes scaled values for the generated --tr-font-size-* and --tr-code-row-height custom properties onto documentElement:

  • styles/typography.json stays the source of truth. styles/fontScale.ts reads the base sizes out of it (BASE_FONT_SIZES, and BASE_CODE_ROW_HEIGHT computed from codeRow the same way renderCss() does) and never edits a value — so raising a size in the JSON can never leave the slider scaling a stale number.
  • At the default scale (100%) every property is removed, so the generated CSS applies verbatim. The override exists only while the reader wants it.
  • The choice is stored through the stable preference adapter and reapplied at startup in main.tsx.
  • styles/TYPOGRAPHY.md records this as the one permitted runtime exception, so the "typography.json is the only source of truth" rule keeps its meaning.

Code rows follow the reader

CSS-only scaling would have left the two code surfaces disagreeing with their own row height:

  • Monaco reads fontSize / lineHeight as JS options, and lineHeight there is the 1.3 multiplier rather than pixels — so once MonacoEditor re-renders its updateOptions call carries the new size. It re-renders because ResourcePreferences changed.
  • Pierre is the harder half: its virtualizer takes the row height as a metric it reads once, when the instance is created (useFileInstance passes it to the constructor and never calls setMetrics afterwards, and the React layer does not expose the instance). So ResourcePreferences now carries codeRowHeight in pixels, usePierreVirtualizer builds its metrics from it, and PierreFile / PierreDiff key the Pierre component on that number. Only the Pierre instance remounts — the scroller, the virtualizer and the scroll position survive, and VirtualizedFile.cleanUp disconnects before the replacement connects.
  • Reading the CSS property inside the hook would not have worked either: applyFontScale writes it from an effect, so during the render that changed the choice the property still holds the previous row.

Scope

  • Not a change to the type scale itself — the default rendering is byte-identical to today.
  • xterm reads its font size when a terminal instance is created, so an already-open terminal follows on its next mount, not mid-drag. Making that live means reaching into the xterm instance; happy to do it here if you would rather have it in one PR.
  • Three new keys: settings.appearance.fontScale.{label,description,value}, in en.json and zh-CN.json.

Testing

  • bun test apps/web/src/ → 1960 pass / 0 fail across 180 files
  • bun test packages/resource-renderers/ → 212 pass / 0 fail across 41 files
  • bun run check:i18n → OK (catalog sorted, every key referenced)
  • bun run check:deps, check:boundaries, typography:check → OK
  • turbo run typecheck → 22 packages successful
  • bun run lint → 1724 files, no findings
  • Verified in a packaged build: dragging to 140% takes --tr-font-size-s11 from 11px → 15px and --tr-code-row-height from 14px → 20px; back to 100% restores the generated values exactly.

@oisano11

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (d299f20d → 7bab8daf) so this is no longer 22 commits stale — the diff is unchanged at 8 files, +386, and the three new catalog keys still sit in en.json / zh-CN.json with no key collisions against the zh-CN catalog that landed in #866 / #871.

Re-ran the gates against the new base: bun test apps/web/src/ → 1957 pass / 0 fail, packages/locales → 42 pass, check:i18n → OK, turbo run typecheck → 8/8 tasks, biome check → clean.

The two CI runs on this branch are both action_required (fork PR awaiting approval), so no checks have executed yet. Could someone approve the workflow run so the gates are visible on the PR?

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes — please address the blocking inline finding.

Comment thread apps/web/src/styles/fontScale.ts
Comment thread apps/web/src/panels/FontScaleSettings.tsx Outdated
@oisano11
oisano11 force-pushed the feat/font-scale-slider branch from aacb64a to 9471347 Compare October 27, 2026 02:43
@oisano11

Copy link
Copy Markdown
Contributor Author

Both findings were real — thank you. Fixed in 94713475, pushed on top of the rebase.

Blocking finding (Pierre row height). You were right that metrics is captured once: useFileInstance passes it as a constructor argument and its layout effect only calls setOptions / render / setSelectedLines, so mutating the prop never reaches VirtualizedFile. The React layer also never exposes the instance, so setMetrics — which does exist and would have been the cleaner path — is unreachable from here.

So the fix is the remount you suggested, wired through the seam you named:

  • ResourcePreferences now carries codeRowHeight: number (the scaled row in pixels, not a scale — a renderer should not have to know how the app derives it). AppResourceHost supplies it from scaledCodeRowHeight(readFontScale()) and republishes via a new subscribeFontScale, consumed with useSyncExternalStore.
  • usePierreVirtualizer reads it through a new useOptionalResourcePreferences() (optional, so hosts without a preferences provider keep working — rendererMount.test.tsx does exactly that) and builds metrics with useMemo instead of useState. Reading the CSS property there would have been wrong for a second reason: applyFontScale writes --tr-code-row-height from an effect, so the property still holds the old row during the render that changed the reader's choice.
  • PierreFile / PierreDiff key <File> / <FileDiff> on the returned lineHeight, so only the Pierre instance is rebuilt — PierreScroller, the virtualizer and the scroll position survive. I checked VirtualizedFile.cleanUp disconnects from the virtualizer before the new instance connects, so the remount does not trip Virtualizer.connect's "already connected" guard.
  • packages/resource-renderers/SPEC.md and TYPOGRAPHY.md now say why the row height travels as a preference.

Monaco needed nothing: --tr-line-height-default is the 1.3 multiplier, so updateOptions already follows a font-size change — which is why you flagged only Pierre.

@oisano11

Copy link
Copy Markdown
Contributor Author

Applied — the fill is now measured from the minimum, not from zero. fontScalePosition(scale) returns 100 * (scale - FONT_SCALE_MIN) / (FONT_SCALE_MAX - FONT_SCALE_MIN), so the track is empty at 80%, a quarter full at the 100% default, and only fills at 160%. fontScalePercent still drives the 125% readout and the aria-valuetext. Added two cases in fontScale.test.ts covering the three reference points and out-of-range clamping.

Every type size comes from `styles/typography.json`, so a reader who needs
larger text has had no recourse but a rebuild. Add a Font size control to
Settings -> Appearance that scales the generated `--tr-font-size-*` and
`--tr-code-row-height` custom properties on `documentElement`.

`styles/typography.json` stays the source of truth: the module reads the base
sizes from it and never edits a value. At the default scale every property is
removed, so the generated CSS applies verbatim; the override exists only while
the reader wants it. The choice is stored through the stable preference adapter
and reapplied at startup.
@oisano11
oisano11 force-pushed the feat/font-scale-slider branch from 9471347 to 8cc2b1e Compare October 28, 2026 04:31

This branch has not been deployed

No deployments
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