From 11d5ba0e632fa0a9d55c8eaf28daf0e58e19ac3e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 6 Jun 2026 08:59:21 +0300 Subject: [PATCH] refactor(ComparisonView): extract strut-height and settled-text from Line Pull the baseline-strut height math into a documented computeStrutHeight util (named constants for the empirical 0.34 / 1.1 factors, with a unit test) and the per-side native text runs into a SettledText component. Strut statics move to Tailwind classes with only the height as style:height; drop the now-redundant style-string $derived bindings. --- src/widgets/ComparisonView/lib/index.ts | 1 + .../computeStrutHeight.test.ts | 28 ++++++++++++ .../computeStrutHeight/computeStrutHeight.ts | 45 +++++++++++++++++++ .../ComparisonView/ui/Line/Line.svelte | 45 +++++-------------- .../ui/SettledText/SettledText.svelte | 37 +++++++++++++++ 5 files changed, 123 insertions(+), 33 deletions(-) create mode 100644 src/widgets/ComparisonView/lib/utils/computeStrutHeight/computeStrutHeight.test.ts create mode 100644 src/widgets/ComparisonView/lib/utils/computeStrutHeight/computeStrutHeight.ts create mode 100644 src/widgets/ComparisonView/ui/SettledText/SettledText.svelte diff --git a/src/widgets/ComparisonView/lib/index.ts b/src/widgets/ComparisonView/lib/index.ts index f84d781..942760e 100644 --- a/src/widgets/ComparisonView/lib/index.ts +++ b/src/widgets/ComparisonView/lib/index.ts @@ -1,3 +1,4 @@ +export { computeStrutHeight } from './utils/computeStrutHeight/computeStrutHeight'; export { createDotCrossfade, getDotTransitionParams, diff --git a/src/widgets/ComparisonView/lib/utils/computeStrutHeight/computeStrutHeight.test.ts b/src/widgets/ComparisonView/lib/utils/computeStrutHeight/computeStrutHeight.test.ts new file mode 100644 index 0000000..b6a2dfa --- /dev/null +++ b/src/widgets/ComparisonView/lib/utils/computeStrutHeight/computeStrutHeight.test.ts @@ -0,0 +1,28 @@ +import { + describe, + expect, + it, +} from 'vitest'; +import { computeStrutHeight } from './computeStrutHeight'; + +describe('computeStrutHeight', () => { + it('uses the centering height when the line-height is generous', () => { + // centering = 40/2 + 16*0.34 = 25.44; floor = 16*1.1 = 17.6 → centering wins. + expect(computeStrutHeight(40, 16)).toBeCloseTo(25.44, 5); + }); + + it('falls back to the ascent floor when the line-height is tight', () => { + // centering = 16/2 + 16*0.34 = 13.44; floor = 16*1.1 = 17.6 → floor wins. + expect(computeStrutHeight(16, 16)).toBeCloseTo(17.6, 5); + }); + + it('treats the floor and centering height as equal at the crossover line-height', () => { + // centering == floor when lineHeight = 1.52 * fontSize → 24.32 for 16px. + expect(computeStrutHeight(24.32, 16)).toBeCloseTo(17.6, 5); + }); + + it('scales with font size', () => { + // centering = 60/2 + 32*0.34 = 40.88; floor = 32*1.1 = 35.2 → centering wins. + expect(computeStrutHeight(60, 32)).toBeCloseTo(40.88, 5); + }); +}); diff --git a/src/widgets/ComparisonView/lib/utils/computeStrutHeight/computeStrutHeight.ts b/src/widgets/ComparisonView/lib/utils/computeStrutHeight/computeStrutHeight.ts new file mode 100644 index 0000000..cafe65b --- /dev/null +++ b/src/widgets/ComparisonView/lib/utils/computeStrutHeight/computeStrutHeight.ts @@ -0,0 +1,45 @@ +/** + * Fraction of the font size added to half the line height to drop the strut's + * baseline from the line box's vertical middle down to the text's optical + * center. Empirical: ~0.34em approximates a Latin font's midline-to-baseline + * offset, so glyphs sit centered rather than riding high in the line. + */ +const BASELINE_OFFSET_RATIO = 0.34; + +/** + * Minimum strut height as a multiple of the font size. Floors the strut above + * the fonts' ascent (~1em) so that at tight line-heights it stays the tallest + * inline box and keeps ownership of the line baseline. Empirical: 1.1 clears the + * tallest ascenders in the catalog's Latin fonts. + */ +const MIN_HEIGHT_RATIO = 1.1; + +/** + * Pixel height for a slider line's invisible baseline strut. + * + * The slider renders each line with a zero-width strut span whose box is + * deliberately the tallest inline box on the line. The browser pins a line box's + * baseline to its tallest inline box; fixing the strut's height independent of + * which bulk runs or window chars are currently mounted keeps the baseline (and + * every glyph) from jumping as the slider sweeps runs in and out. With + * `overflow: hidden` the strut's baseline sits at its bottom edge, so this height + * also sets the text's vertical position within the line box. + * + * The result is `max(centeringHeight, ascentFloor)`: + * - `centeringHeight = lineHeightPx / 2 + fontSizePx * BASELINE_OFFSET_RATIO` + * centers the text — half the line box places the strut's bottom edge at the + * vertical middle, and the offset term nudges the baseline down to the glyphs' + * optical center. + * - `ascentFloor = fontSizePx * MIN_HEIGHT_RATIO` keeps the strut taller than the + * fonts' ascent when the line-height is tight (where `centeringHeight` would + * shrink below a real glyph box and let another box steal the baseline). + * + * @param lineHeightPx Line height in pixels (typography line-height × font size). + * @param fontSizePx Rendered font size in pixels. + * @returns Strut height in pixels. + */ +export function computeStrutHeight(lineHeightPx: number, fontSizePx: number): number { + const centeringHeight = lineHeightPx / 2 + fontSizePx * BASELINE_OFFSET_RATIO; + const ascentFloor = fontSizePx * MIN_HEIGHT_RATIO; + return Math.max(centeringHeight, ascentFloor); +} diff --git a/src/widgets/ComparisonView/ui/Line/Line.svelte b/src/widgets/ComparisonView/ui/Line/Line.svelte index 92350b2..df864c5 100644 --- a/src/widgets/ComparisonView/ui/Line/Line.svelte +++ b/src/widgets/ComparisonView/ui/Line/Line.svelte @@ -14,8 +14,10 @@ import { windowSizeForLine, } from '$entities/Font'; import { getTypographySettingsStore } from '$features/AdjustTypography'; +import { computeStrutHeight } from '../../lib'; import { getComparisonStore } from '../../model'; import Character from '../Character/Character.svelte'; +import SettledText from '../SettledText/SettledText.svelte'; interface Props { /** @@ -44,36 +46,9 @@ const fontSizePx = $derived(typography.renderedSize); const lineHeightPx = $derived(typography.height * typography.renderedSize); const letterSpacingPx = $derived(typography.spacing * typography.renderedSize); -/** - * Class and style are single short bindings so the formatter keeps - * `{text}` on one line. A wrapped text expression would leak - * its indentation into the span content under `white-space: pre`. - */ -const BULK_LEFT_CLASS = - 'inline-block align-baseline leading-none text-swiss-black/75 dark:text-brand/75 transition-colors duration-300'; -const BULK_RIGHT_CLASS = - 'inline-block align-baseline leading-none text-neutral-950 dark:text-white transition-colors duration-300'; - -const leftStyle = $derived(`font-family:${fontA?.name ?? ''};font-size:${fontSizePx}px`); -const rightStyle = $derived(`font-family:${fontB?.name ?? ''};font-size:${fontSizePx}px`); - -/** - * Stops the whole line from jumping up or down as the slider moves. The browser - * pins a line box's baseline to its tallest inline box, so without a fixed - * reference the baseline (and every glyph) shifts the moment a bulk run appears - * or disappears, or the last window char morphs to a font with a taller ascent. - * This invisible strut is always the tallest box — `overflow: hidden` puts its - * baseline at its bottom edge — so it owns the line baseline and holds it still. - * Its height also sets the text's vertical position (the container is block, so - * nothing else centers it). - * - * Height factors are empirical: the first term centers the text, the `* 1.1` - * floor keeps the strut above the fonts' ascent at tight line-heights. - */ -const strutHeightPx = $derived(Math.max(lineHeightPx / 2 + fontSizePx * 0.34, fontSizePx * 1.1)); -const strutStyle = $derived( - `display:inline-block;width:0;overflow:hidden;vertical-align:baseline;height:${strutHeightPx}px`, -); +// Invisible strut that pins the line baseline so glyphs don't jump as the +// slider moves; `computeStrutHeight` explains the why and the formula. +const strutHeightPx = $derived(computeStrutHeight(lineHeightPx, fontSizePx)); + + +{text}