From f49180e83dc1e0ea47b9f4ac9ebffe6f989a65c1 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 24 Jun 2026 13:49:24 +0300 Subject: [PATCH] feat(CompareBoard): add fitColumns honest column gating --- .../lib/measure/fitColumns.test.ts | 28 +++++++++++++ .../CompareBoard/lib/measure/fitColumns.ts | 41 +++++++++++++++++++ .../CompareBoard/lib/measure/index.ts | 8 ++++ 3 files changed, 77 insertions(+) create mode 100644 src/features/CompareBoard/lib/measure/fitColumns.test.ts create mode 100644 src/features/CompareBoard/lib/measure/fitColumns.ts create mode 100644 src/features/CompareBoard/lib/measure/index.ts diff --git a/src/features/CompareBoard/lib/measure/fitColumns.test.ts b/src/features/CompareBoard/lib/measure/fitColumns.test.ts new file mode 100644 index 0000000..e5ab855 --- /dev/null +++ b/src/features/CompareBoard/lib/measure/fitColumns.test.ts @@ -0,0 +1,28 @@ +import { + describe, + expect, + it, +} from 'vitest'; +import { fitColumns } from './fitColumns'; + +describe('fitColumns', () => { + it('packs as many honest columns as fit, gap-aware', () => { + // each needs 600, gap 40, available 1280 -> 1 col=600, 2 cols=1240, 3=1880 + expect(fitColumns({ naturalWidth: 600, available: 1280, gap: 40, maxColumns: 3 })).toBe(2); + }); + it('never exceeds maxColumns even with room', () => { + expect(fitColumns({ naturalWidth: 100, available: 5000, gap: 20, maxColumns: 3 })).toBe(3); + }); + it('never returns less than 1', () => { + expect(fitColumns({ naturalWidth: 9000, available: 300, gap: 20, maxColumns: 3 })).toBe(1); + }); + it('fits a column at the exact boundary (inclusive)', () => { + // 2 cols: 2*600 + 1*40 = 1240 == available -> fits + expect(fitColumns({ naturalWidth: 600, available: 1240, gap: 40, maxColumns: 3 })).toBe(2); + // one px short -> only 1 + expect(fitColumns({ naturalWidth: 600, available: 1239, gap: 40, maxColumns: 3 })).toBe(1); + }); + it('respects a maxColumns of 1 even with unlimited room', () => { + expect(fitColumns({ naturalWidth: 100, available: 5000, gap: 20, maxColumns: 1 })).toBe(1); + }); +}); diff --git a/src/features/CompareBoard/lib/measure/fitColumns.ts b/src/features/CompareBoard/lib/measure/fitColumns.ts new file mode 100644 index 0000000..f2d9491 --- /dev/null +++ b/src/features/CompareBoard/lib/measure/fitColumns.ts @@ -0,0 +1,41 @@ +/** + * Inputs for column gating. + */ +export interface FitColumnsInput { + /** + * The widest pairing's Pretext natural (shrink-wrap) width in px. + */ + naturalWidth: number; + /** + * Total available width in px for the columns row. + */ + available: number; + /** + * Gap in px between columns. + */ + gap: number; + /** + * Hard cap on columns that still preserve an honest measure (2–3). + */ + maxColumns: number; +} + +/** + * How many equal honest columns fit. Uses the real per-pairing required width + * (Pretext shrink-wrap) — the 45–75ch rule is only a fallback bound elsewhere. + * `n` columns occupy `n*naturalWidth + (n-1)*gap`. Clamped to [1, maxColumns]. + * + * @param input - Natural width, available width, gap, and column cap. + * @returns The number of columns that fit, in [1, maxColumns]. + */ +export function fitColumns({ naturalWidth, available, gap, maxColumns }: FitColumnsInput): number { + let fit = 1; + for (let n = 2; n <= maxColumns; n++) { + if (n * naturalWidth + (n - 1) * gap <= available) { + fit = n; + } else { + break; + } + } + return fit; +} diff --git a/src/features/CompareBoard/lib/measure/index.ts b/src/features/CompareBoard/lib/measure/index.ts new file mode 100644 index 0000000..9f11529 --- /dev/null +++ b/src/features/CompareBoard/lib/measure/index.ts @@ -0,0 +1,8 @@ +export { + fitColumns, + type FitColumnsInput, +} from './fitColumns'; +export { + measureRoleHeight, + type RoleHeightInput, +} from './measureFrameHeight';