Yaak Proxy: scale resized numeric column widths with the interface size too

Every numeric width is kept in pixels at the default interface size, so
the least width matches what renders at any size and a resize follows
the pointer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Gregory SchierandClaude Opus 5.5 authored and Stetta task tk_mwbaj1badvtzl9xk committed 2026-11-10 16:13:16 -07:00
1 parent 82ac6e5c15
commit 7057a902c7
3 files changed
+57 -55

No files matched your search

+9 -14
View File
@@ -46,6 +46,7 @@ import {
resetWidth, resetWidth,
resetWidths, resetWidths,
resizeColumn, resizeColumn,
resizeDistance,
toggleColumn, toggleColumn,
unavailableColumns, unavailableColumns,
} from "../lib/columns"; } from "../lib/columns";
@@ -262,9 +263,12 @@ export const FlowListHeader = memo(function FlowListHeader({
sort={sort} sort={sort}
onSort={onSort} onSort={onSort}
onResizeStart={() => { onResizeStart={() => {
startWidth.current = shownWidth(gridRef.current, c); startWidth.current = c.current;
}}
onResizeMove={(dx) => {
const by = resizeDistance(c, dx, rootFontSize());
setPrefs((p) => resizeColumn(p, c.id, startWidth.current + by));
}} }}
onResizeMove={(dx) => setPrefs((p) => resizeColumn(p, c.id, startWidth.current + dx))}
onResetWidth={() => setPrefs((p) => resetWidth(p, c.id))} onResetWidth={() => setPrefs((p) => resetWidth(p, c.id))}
/> />
))} ))}
@@ -279,18 +283,9 @@ export const FlowListHeader = memo(function FlowListHeader({
); );
}); });
/** /** The interface font size now, which numeric columns' widths grow with */
* A column's content width as laid out, so a resize starts from what shows: a numeric column's function rootFontSize(): number {
* can be wider than its width in pixels, at its default or its least width, at a larger return Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
* interface size
*/
function shownWidth(grid: HTMLElement | null, c: Column): number {
if (c.fitChars == null) return c.current;
const cell = grid?.querySelector<HTMLElement>(`[data-column="${c.id}"]`);
if (cell == null) return c.current;
const style = getComputedStyle(cell);
const padding = Number.parseFloat(style.paddingLeft) + Number.parseFloat(style.paddingRight);
return cell.getBoundingClientRect().width - padding;
} }
interface HeaderCellProps { interface HeaderCellProps {
+21 -13
View File
@@ -26,6 +26,7 @@ import {
hideOtherColumns, hideOtherColumns,
moveColumn, moveColumn,
moveColumnBy, moveColumnBy,
NUMERIC_FIT_CHARS,
nextSort, nextSort,
operationOf, operationOf,
parseColumnPrefs, parseColumnPrefs,
@@ -34,6 +35,7 @@ import {
resetWidth, resetWidth,
resetWidths, resetWidths,
resizeColumn, resizeColumn,
resizeDistance,
showOperationFor, showOperationFor,
shownColumns, shownColumns,
sortFlows, sortFlows,
@@ -116,19 +118,16 @@ describe("shownColumns", () => {
prefs = resizeColumn(prefs, "path", 500); prefs = resizeColumn(prefs, "path", 500);
const columns = shownColumns(prefs); const columns = shownColumns(prefs);
const pad = (w: number) => `calc(${w}px + 0.75rem)`; const pad = (w: number) => `calc(${w}px + 0.75rem)`;
// Size and Time at their defaults, which grow with the interface size, and never narrower // Size and Time, in pixels at the default interface size of 15, grow with it
// than their widest values const size = "calc(4.8rem + 0.75rem)";
const size = "calc(max(4.8rem, 4rem) + 0.75rem)"; const time = `calc(${64 / 15}rem + 0.75rem)`;
const time = `calc(max(${64 / 15}rem, 4rem) + 0.75rem)`;
expect(gridTemplate(columns)).toBe( expect(gridTemplate(columns)).toBe(
["24px", ...[46, 20, 50, 240, 500, 120, 46, 50].map(pad), size, time].join(" "), ["24px", ...[46, 20, 50, 240, 500, 120, 46, 50].map(pad), size, time].join(" "),
); );
expect(gridWidth(columns)).toBe( expect(gridWidth(columns)).toBe(`calc(1096px + 4.8rem + ${64 / 15}rem + 10 * 0.75rem)`);
`calc(1096px + max(4.8rem, 4rem) + max(${64 / 15}rem, 4rem) + 10 * 0.75rem)`, // Resized, too, and at its least width it holds its widest value: eight characters of 0.5rem
); const resized = shownColumns(resizeColumn(prefs, "size", 10));
// Resized, a numeric column keeps the pixels it was given expect(gridTemplate(resized)).toContain(" calc(4rem + 0.75rem)");
const resized = shownColumns(resizeColumn(prefs, "size", 90));
expect(gridTemplate(resized)).toContain("calc(max(90px, 4rem) + 0.75rem)");
}); });
}); });
@@ -148,9 +147,10 @@ describe("numeric column widths", () => {
for (const id of numeric) { for (const id of numeric) {
const def = columnDef(id); const def = columnDef(id);
const chars = id === "size" || id === "requestSize" ? sizeChars : timeChars; const chars = id === "size" || id === "requestSize" ? sizeChars : timeChars;
expect(def.fitChars).toBeGreaterThanOrEqual(chars); expect(NUMERIC_FIT_CHARS).toBeGreaterThanOrEqual(chars);
expect(def.scales).toBe(true);
// In pixels at the default interface size, as the row's font sets them // In pixels at the default interface size, as the row's font sets them
const fit = (def.fitChars ?? 0) * CHAR_REM * BASE_FONT_SIZE; const fit = NUMERIC_FIT_CHARS * CHAR_REM * BASE_FONT_SIZE;
expect(def.minWidth).toBeGreaterThanOrEqual(fit); expect(def.minWidth).toBeGreaterThanOrEqual(fit);
expect(def.width).toBeGreaterThanOrEqual(fit); expect(def.width).toBeGreaterThanOrEqual(fit);
} }
@@ -168,6 +168,14 @@ describe("numeric column widths", () => {
for (const id of numeric) expect(columnDef(id).width).toBeGreaterThanOrEqual(header[id]); for (const id of numeric) expect(columnDef(id).width).toBeGreaterThanOrEqual(header[id]);
}); });
test("a resize moves a numeric column's edge with the pointer at any interface size", () => {
const size = columnDef("size");
// At 30, twice the default, a 20px drag is 10 of the pixels its width is kept in
expect(resizeDistance(size, 20, 30)).toBe(10);
expect(resizeDistance(size, -12, 12)).toBe(-15);
expect(resizeDistance(columnDef("host"), 20, 30)).toBe(20);
});
test("a stored width narrower than the least is raised, and a wider one kept", () => { test("a stored width narrower than the least is raised, and a wider one kept", () => {
const parsed = parseColumnPrefs({ widths: { size: 44, time: 50, requestSize: 120, host: 20 } }); const parsed = parseColumnPrefs({ widths: { size: 44, time: 50, requestSize: 120, host: 20 } });
expect(parsed.widths).toEqual({ size: 60, time: 60, requestSize: 120, host: 60 }); expect(parsed.widths).toEqual({ size: 60, time: 60, requestSize: 120, host: 60 });
@@ -175,7 +183,7 @@ describe("numeric column widths", () => {
expect(resizeColumn(defaultColumnPrefs, "latency", 10).widths).toEqual({ latency: 60 }); expect(resizeColumn(defaultColumnPrefs, "latency", 10).widths).toEqual({ latency: 60 });
// Reset, the defaults are back // Reset, the defaults are back
const reset = shownColumns(resetWidths(parsed)).find((c) => c.id === "size"); const reset = shownColumns(resetWidths(parsed)).find((c) => c.id === "size");
expect(reset).toMatchObject({ current: 72, resized: false }); expect(reset?.current).toBe(72);
expect(resetWidth(parsed, "time").widths).toEqual({ size: 60, requestSize: 120, host: 60 }); expect(resetWidth(parsed, "time").widths).toEqual({ size: 60, requestSize: 120, host: 60 });
}); });
}); });
+27 -28
View File
@@ -24,10 +24,10 @@ export interface ColumnDef {
/** The header or query parameter a custom column shows: `header.x-request-id` */ /** The header or query parameter a custom column shows: `header.x-request-id` */
path?: string; path?: string;
/** /**
* A numeric column's widest value, in characters of the row's monospace font: it is never * A numeric column's: its width, resized or not, grows with the interface size, as its values
* narrower than that at any interface size, and its default width grows with the interface size * do, so the least width always holds the widest value
*/ */
fitChars?: number; scales?: boolean;
} }
const builtin = ( const builtin = (
@@ -37,8 +37,8 @@ const builtin = (
minWidth: number, minWidth: number,
align: "left" | "right", align: "left" | "right",
defaultVisible: boolean, defaultVisible: boolean,
fitChars?: number, scales?: boolean,
): ColumnDef => ({ id, label, width, minWidth, align, defaultVisible, fitChars }); ): ColumnDef => ({ id, label, width, minWidth, align, defaultVisible, scales });
/** /**
* The widest size and duration a numeric cell shows, in characters: `999.9 KB` and `999m 59s`, * The widest size and duration a numeric cell shows, in characters: `999.9 KB` and `999m 59s`,
@@ -55,7 +55,10 @@ export const CHAR_REM = 0.5;
/** The default interface font size, `DEFAULT_SETTINGS_VALUES.interfaceFontSize`, widths are at */ /** The default interface font size, `DEFAULT_SETTINGS_VALUES.interfaceFontSize`, widths are at */
export const BASE_FONT_SIZE = 15; export const BASE_FONT_SIZE = 15;
/** A numeric column's least width, in pixels at the default interface size: its widest value */ /**
* A numeric column's least width, in pixels at the default interface size, which grow with it:
* its widest value
*/
const NUMERIC_MIN = NUMERIC_FIT_CHARS * CHAR_REM * BASE_FONT_SIZE; const NUMERIC_MIN = NUMERIC_FIT_CHARS * CHAR_REM * BASE_FONT_SIZE;
export const COLUMNS: ColumnDef[] = [ export const COLUMNS: ColumnDef[] = [
@@ -68,10 +71,10 @@ export const COLUMNS: ColumnDef[] = [
builtin("status", "Status", 46, 34, "left", true), builtin("status", "Status", 46, 34, "left", true),
builtin("type", "Type", 50, 34, "left", true), builtin("type", "Type", 50, 34, "left", true),
// Each default fits its label sorted, with the sort's arrow and place, and its widest value // Each default fits its label sorted, with the sort's arrow and place, and its widest value
builtin("requestSize", "Request Size", 96, NUMERIC_MIN, "right", false, NUMERIC_FIT_CHARS), builtin("requestSize", "Request Size", 96, NUMERIC_MIN, "right", false, true),
builtin("size", "Size", 72, NUMERIC_MIN, "right", true, NUMERIC_FIT_CHARS), builtin("size", "Size", 72, NUMERIC_MIN, "right", true, true),
builtin("time", "Time", 64, NUMERIC_MIN, "right", true, NUMERIC_FIT_CHARS), builtin("time", "Time", 64, NUMERIC_MIN, "right", true, true),
builtin("latency", "Latency", 68, NUMERIC_MIN, "right", false, NUMERIC_FIT_CHARS), builtin("latency", "Latency", 68, NUMERIC_MIN, "right", false, true),
builtin("started", "Started", 92, 60, "left", false), builtin("started", "Started", 92, 60, "left", false),
builtin("remote", "Remote Address", 150, 60, "left", false), builtin("remote", "Remote Address", 150, 60, "left", false),
builtin("version", "HTTP Version", 70, 44, "left", false), builtin("version", "HTTP Version", 70, 44, "left", false),
@@ -197,8 +200,6 @@ export const defaultColumnPrefs: ColumnPrefs = {
export interface Column extends ColumnDef { export interface Column extends ColumnDef {
/** Its width now, in pixels of content */ /** Its width now, in pixels of content */
current: number; current: number;
/** Whether the person set its width, rather than leaving it at its default */
resized: boolean;
} }
/** /**
@@ -225,11 +226,7 @@ export function shownColumns(prefs: ColumnPrefs, unavailable: ColumnId[] = []):
const all = allColumns(prefs).filter((c) => !unavailable.includes(c.id)); const all = allColumns(prefs).filter((c) => !unavailable.includes(c.id));
const shown = all.filter((c) => isVisible(prefs, c)); const shown = all.filter((c) => isVisible(prefs, c));
const columns = shown.length > 0 ? shown : all.filter((c) => c.id === "path"); const columns = shown.length > 0 ? shown : all.filter((c) => c.id === "path");
return columns.map((c) => ({ return columns.map((c) => ({ ...c, current: columnWidth(prefs, c.id) }));
...c,
current: columnWidth(prefs, c.id),
resized: prefs.widths[c.id] != null,
}));
} }
/** Columns the platform has nothing for: App, where it cannot tell which app sent a flow */ /** Columns the platform has nothing for: App, where it cannot tell which app sent a flow */
@@ -242,14 +239,17 @@ export function columnWidth(prefs: ColumnPrefs, id: ColumnId): number {
return Math.max(def.minWidth, Math.round(prefs.widths[id] ?? def.width)); return Math.max(def.minWidth, Math.round(prefs.widths[id] ?? def.width));
} }
/** /** A column's content width, as CSS: a numeric column's grows with the interface size */
* A column's content width, as CSS. A numeric column's default grows with the interface size,
* and at any width it holds its widest value, which grows with the interface size too.
*/
function columnTrack(c: Column): string { function columnTrack(c: Column): string {
if (c.fitChars == null) return `${c.current}px`; return c.scales ? `${c.current / BASE_FONT_SIZE}rem` : `${c.current}px`;
const width = c.resized ? `${c.current}px` : `${c.current / BASE_FONT_SIZE}rem`; }
return `max(${width}, ${c.fitChars * CHAR_REM}rem)`;
/**
* How far a resize moved, in the pixels a column's width is kept in: a numeric column's are at
* the default interface size, `rootFontSize` being the size now
*/
export function resizeDistance(c: ColumnDef, dx: number, rootFontSize: number): number {
return c.scales && rootFontSize > 0 ? (dx * BASE_FONT_SIZE) / rootFontSize : dx;
} }
/** The grid template for the header and every row: the mark, then each column and its padding */ /** The grid template for the header and every row: the mark, then each column and its padding */
@@ -260,10 +260,9 @@ export function gridTemplate(columns: Column[]): string {
/** The width of a row's grid, as CSS: the list scrolls sideways when it is wider than the list */ /** The width of a row's grid, as CSS: the list scrolls sideways when it is wider than the list */
export function gridWidth(columns: Column[]): string { export function gridWidth(columns: Column[]): string {
const fixed = columns.filter((c) => c.fitChars == null); const px = columns.filter((c) => !c.scales).reduce((sum, c) => sum + c.current, MARK_WIDTH);
const px = fixed.reduce((sum, c) => sum + c.current, MARK_WIDTH); const scaled = columns.filter((c) => c.scales).map((c) => ` + ${columnTrack(c)}`);
const numeric = columns.filter((c) => c.fitChars != null).map((c) => ` + ${columnTrack(c)}`); return `calc(${px}px${scaled.join("")} + ${columns.length} * ${CELL_PADDING})`;
return `calc(${px}px${numeric.join("")} + ${columns.length} * ${CELL_PADDING})`;
} }
/** Whether the picker may uncheck `id`: any column but the last one shown */ /** Whether the picker may uncheck `id`: any column but the last one shown */