Files
kandev/apps/web/lib/state/layout-manager/sizing.ts
T

157 lines
5.3 KiB
TypeScript

import type { LayoutColumn, LayoutGroup } from "./types";
import { LAYOUT_INITIAL_RATIO } from "./constants";
import { computePinnedMaxPxFor, computeSidebarMaxPx, LAYOUT_PINNED_MIN_PX } from "./caps";
import { getGlobalSidebarWidth } from "@/lib/local-storage";
// Legacy hard caps used to clamp the *initial* default width. Users can still
// drag past these via setConstraints (which uses the larger runtime cap),
// but a fresh task env opens at the same width it always did.
const LEGACY_SIDEBAR_INITIAL_CAP = 350;
const LEGACY_RIGHT_INITIAL_CAP = 450;
/**
* Get the effective pinned width for a column.
*
* - User overrides (from a prior resize, captured in the in-memory
* pinnedWidths map) are clamped to the runtime cap.
* - The initial default (no override) preserves legacy behavior: ratio-based
* width clamped to the old hard cap. New environments open exactly as
* they did before this PR.
*/
export function getPinnedWidth(
column: LayoutColumn,
totalWidth: number,
override?: number,
): number {
const runtimeMax = column.maxWidth ?? computePinnedMaxPxFor(column.id);
const min = column.minWidth ?? LAYOUT_PINNED_MIN_PX;
if (override !== undefined) {
return Math.max(min, Math.min(override, runtimeMax));
}
// No override: the left sidebar honors the persisted GLOBAL width pref so it
// opens identically across every task. Clamp-to-fit against the current
// screen (the raw value stays in storage, so a wider monitor restores it).
if (column.id === "sidebar") {
const pref = getGlobalSidebarWidth();
if (pref !== null) {
const max = column.maxWidth ?? computeSidebarMaxPx(totalWidth);
return Math.max(min, Math.min(pref, max));
}
}
const ratioWidth = Math.round(totalWidth * LAYOUT_INITIAL_RATIO);
const initialCap =
column.maxWidth ??
(column.id === "sidebar" ? LEGACY_SIDEBAR_INITIAL_CAP : LEGACY_RIGHT_INITIAL_CAP);
return Math.max(min, Math.min(ratioWidth, initialCap));
}
type ColumnBucket = {
pinnedTotal: number;
pinnedWidths: number[];
explicitIndices: number[];
explicitTotal: number;
flexIndices: number[];
};
/** First pass: classify columns and compute pinned widths. */
function classifyColumns(
columns: LayoutColumn[],
totalWidth: number,
pinnedOverrides: Map<string, number>,
): ColumnBucket {
const bucket: ColumnBucket = {
pinnedTotal: 0,
pinnedWidths: new Array(columns.length).fill(0),
explicitIndices: [],
explicitTotal: 0,
flexIndices: [],
};
for (let i = 0; i < columns.length; i++) {
const col = columns[i];
if (col.pinned) {
const w = getPinnedWidth(col, totalWidth, pinnedOverrides.get(col.id));
bucket.pinnedWidths[i] = w;
bucket.pinnedTotal += w;
} else if (col.width !== undefined && col.width > 0) {
bucket.explicitIndices.push(i);
bucket.explicitTotal += col.width;
} else {
bucket.flexIndices.push(i);
}
}
return bucket;
}
/** Second pass: compute non-pinned column widths. */
function computeFlexWidths(
columns: LayoutColumn[],
bucket: ColumnBucket,
remainingSpace: number,
): number[] {
const widths = [...bucket.pinnedWidths];
if (bucket.explicitIndices.length > 0 && bucket.flexIndices.length === 0) {
// All non-pinned have explicit widths → scale proportionally
const scale = bucket.explicitTotal > 0 ? remainingSpace / bucket.explicitTotal : 1;
for (const i of bucket.explicitIndices) {
widths[i] = Math.round((columns[i].width ?? 0) * scale);
}
return widths;
}
if (bucket.explicitIndices.length > 0) {
// Mix of explicit and flex — use explicit as-is (capped), flex splits remainder
let explicitUsed = 0;
for (const i of bucket.explicitIndices) {
const w = Math.min(columns[i].width ?? 0, remainingSpace);
widths[i] = w;
explicitUsed += w;
}
const flexSpace = Math.max(0, remainingSpace - explicitUsed);
const perFlex =
bucket.flexIndices.length > 0 ? Math.floor(flexSpace / bucket.flexIndices.length) : 0;
for (const i of bucket.flexIndices) {
widths[i] = perFlex;
}
return widths;
}
// All non-pinned are flex → equal split
const perFlex =
bucket.flexIndices.length > 0 ? Math.floor(remainingSpace / bucket.flexIndices.length) : 0;
for (const i of bucket.flexIndices) {
widths[i] = perFlex;
}
return widths;
}
/**
* Compute absolute pixel widths for each column.
*
* Strategy:
* 1. Pinned columns: use getPinnedWidth (ratio-based default or user override)
* 2. Non-pinned columns with explicit width (from captured layouts): scale
* proportionally to fill remaining space
* 3. Non-pinned columns without width: split remaining space equally
*/
export function computeColumnWidths(
columns: LayoutColumn[],
totalWidth: number,
pinnedWidths: Map<string, number>,
): number[] {
const bucket = classifyColumns(columns, totalWidth, pinnedWidths);
const remainingSpace = Math.max(0, totalWidth - bucket.pinnedTotal);
return computeFlexWidths(columns, bucket, remainingSpace);
}
/**
* Compute absolute pixel heights for groups within a column.
* Equal distribution among groups.
*/
export function computeGroupHeights(groups: LayoutGroup[], totalHeight: number): number[] {
if (groups.length === 0) return [];
const h = Math.floor(totalHeight / groups.length);
return groups.map(() => h);
}