refactor(layout): swap pane width caps for viewport-proportional runtime cap
Replace the hard 350px sidebar / 450px right caps with max(800, viewport*0.7) so wide displays get a roomy file/terminal panel and the sidebar can grow with the screen. Pinned minimum is raised from 50px to 180px so panels don't collapse into unusable slivers. Caps now live in lib/state/layout-manager/caps.ts; constants module no longer exports per-column max-px constants.
This commit is contained in:
@@ -11,7 +11,6 @@ import {
|
||||
CENTER_GROUP,
|
||||
RIGHT_TOP_GROUP,
|
||||
RIGHT_BOTTOM_GROUP,
|
||||
LAYOUT_RIGHT_MAX_PX,
|
||||
panel,
|
||||
} from "@/lib/state/layout-manager/constants";
|
||||
import type { LayoutState } from "@/lib/state/layout-manager/types";
|
||||
@@ -46,7 +45,6 @@ function officeLayout(): LayoutState {
|
||||
id: "right",
|
||||
pinned: true,
|
||||
width: 350,
|
||||
maxWidth: LAYOUT_RIGHT_MAX_PX,
|
||||
groups: [
|
||||
{ id: RIGHT_TOP_GROUP, panels: [panel("files"), panel("changes")] },
|
||||
{ id: RIGHT_BOTTOM_GROUP, panels: [panel("terminal-default")] },
|
||||
|
||||
@@ -3,10 +3,10 @@ import {
|
||||
SIDEBAR_LOCK,
|
||||
SIDEBAR_GROUP,
|
||||
CENTER_GROUP,
|
||||
LAYOUT_SIDEBAR_MAX_PX,
|
||||
LAYOUT_RIGHT_MAX_PX,
|
||||
RIGHT_TOP_GROUP,
|
||||
RIGHT_BOTTOM_GROUP,
|
||||
computePinnedMaxPx,
|
||||
LAYOUT_PINNED_MIN_PX,
|
||||
resolveGroupIds,
|
||||
} from "./layout-manager";
|
||||
import type { LayoutGroupIds } from "./layout-manager";
|
||||
@@ -16,11 +16,12 @@ export { getRootSplitview } from "./layout-manager";
|
||||
|
||||
/** After fromJSON() restores a session layout, apply fixups and return group IDs. */
|
||||
export function applyLayoutFixups(api: DockviewApi): LayoutGroupIds {
|
||||
const cap = computePinnedMaxPx();
|
||||
const sb = api.getPanel("sidebar");
|
||||
if (sb) {
|
||||
sb.group.locked = SIDEBAR_LOCK;
|
||||
sb.group.header.hidden = false;
|
||||
sb.group.api.setConstraints({ maximumWidth: LAYOUT_SIDEBAR_MAX_PX });
|
||||
sb.group.api.setConstraints({ maximumWidth: cap, minimumWidth: LAYOUT_PINNED_MIN_PX });
|
||||
}
|
||||
|
||||
const oldChanges = api.getPanel("diff-files");
|
||||
@@ -34,7 +35,7 @@ export function applyLayoutFixups(api: DockviewApi): LayoutGroupIds {
|
||||
for (const gid of [RIGHT_TOP_GROUP, RIGHT_BOTTOM_GROUP]) {
|
||||
const group = api.groups.find((g) => g.id === gid);
|
||||
if (group) {
|
||||
group.api.setConstraints({ maximumWidth: LAYOUT_RIGHT_MAX_PX });
|
||||
group.api.setConstraints({ maximumWidth: cap, minimumWidth: LAYOUT_PINNED_MIN_PX });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ export type { BuiltInPreset } from "./layout-manager";
|
||||
export {
|
||||
LAYOUT_SIDEBAR_RATIO,
|
||||
LAYOUT_RIGHT_RATIO,
|
||||
LAYOUT_SIDEBAR_MAX_PX,
|
||||
LAYOUT_RIGHT_MAX_PX,
|
||||
computePinnedMaxPx,
|
||||
LAYOUT_PINNED_MIN_PX,
|
||||
} from "./layout-manager";
|
||||
export { applyLayoutFixups } from "./dockview-layout-builders";
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
RIGHT_TOP_GROUP,
|
||||
RIGHT_BOTTOM_GROUP,
|
||||
TERMINAL_DEFAULT_ID,
|
||||
LAYOUT_SIDEBAR_MAX_PX,
|
||||
} from "./constants";
|
||||
import { computePinnedMaxPx, LAYOUT_PINNED_MIN_PX } from "./caps";
|
||||
|
||||
export type LayoutGroupIds = {
|
||||
centerGroupId: string;
|
||||
@@ -71,23 +71,34 @@ export function applyLayout(
|
||||
|
||||
api.fromJSON(serialized);
|
||||
|
||||
// Lock sidebar group and enforce max-width constraint.
|
||||
// Lock sidebar group and enforce max/min-width constraints.
|
||||
// Constraints are not serialized with layouts, so we must reapply after fromJSON.
|
||||
const runtimeCap = computePinnedMaxPx();
|
||||
const sb = api.getPanel("sidebar");
|
||||
if (sb) {
|
||||
sb.group.locked = SIDEBAR_LOCK;
|
||||
sb.group.header.hidden = false;
|
||||
sb.group.api.setConstraints({ maximumWidth: LAYOUT_SIDEBAR_MAX_PX });
|
||||
sb.group.api.setConstraints({
|
||||
maximumWidth: runtimeCap,
|
||||
minimumWidth: LAYOUT_PINNED_MIN_PX,
|
||||
});
|
||||
}
|
||||
|
||||
// Enforce max-width on other pinned columns (e.g. right panel group).
|
||||
// Enforce constraints on other pinned columns (e.g. right panel group).
|
||||
// Column.maxWidth (when set) wins over the runtime cap so presets like
|
||||
// compact can pin the sidebar tight; otherwise we inherit the viewport-
|
||||
// proportional cap so wide screens get a roomy right panel.
|
||||
for (const col of state.columns) {
|
||||
if (col.id === "sidebar" || !col.pinned || !col.maxWidth) continue;
|
||||
if (col.id === "sidebar" || !col.pinned) continue;
|
||||
const cap = col.maxWidth ?? runtimeCap;
|
||||
for (const group of col.groups) {
|
||||
for (const p of group.panels) {
|
||||
const pnl = api.getPanel(p.id);
|
||||
if (pnl) {
|
||||
pnl.group.api.setConstraints({ maximumWidth: col.maxWidth });
|
||||
pnl.group.api.setConstraints({
|
||||
maximumWidth: cap,
|
||||
minimumWidth: LAYOUT_PINNED_MIN_PX,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { computePinnedMaxPx, LAYOUT_PINNED_MIN_PX } from "./caps";
|
||||
|
||||
describe("computePinnedMaxPx", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("floors at 800px on narrow viewports", () => {
|
||||
expect(computePinnedMaxPx(1024)).toBe(800);
|
||||
expect(computePinnedMaxPx(100)).toBe(800);
|
||||
});
|
||||
|
||||
it("scales to viewport * 0.7 above the floor", () => {
|
||||
expect(computePinnedMaxPx(2000)).toBe(1400);
|
||||
expect(computePinnedMaxPx(3000)).toBe(2100);
|
||||
});
|
||||
|
||||
it("reads window.innerWidth when no argument passed", () => {
|
||||
vi.stubGlobal("window", { innerWidth: 1800 } as Window);
|
||||
expect(computePinnedMaxPx()).toBe(Math.round(1800 * 0.7));
|
||||
});
|
||||
|
||||
it("uses 1440 fallback when window is undefined", () => {
|
||||
vi.stubGlobal("window", undefined);
|
||||
expect(computePinnedMaxPx()).toBe(Math.round(1440 * 0.7));
|
||||
});
|
||||
});
|
||||
|
||||
describe("LAYOUT_PINNED_MIN_PX", () => {
|
||||
it("keeps pinned panels usable", () => {
|
||||
expect(LAYOUT_PINNED_MIN_PX).toBeGreaterThanOrEqual(150);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Runtime caps for pinned columns (sidebar / right).
|
||||
*
|
||||
* The previous hard caps (350 sidebar, 450 right) were too strict on wide
|
||||
* displays — users wanted to drag the right panel out to half the screen for
|
||||
* file review or terminal work. Cap scales with viewport so wider screens get
|
||||
* more room, while small screens still keep the center column usable.
|
||||
*/
|
||||
|
||||
const MIN_CAP_PX = 800;
|
||||
const VIEWPORT_RATIO = 0.7;
|
||||
const FALLBACK_VIEWPORT = 1440;
|
||||
|
||||
/**
|
||||
* Compute the maximum pixel width for a pinned column at the current viewport.
|
||||
* Returns max(800, viewportWidth * 0.7). Falls back to 1440 when called in an
|
||||
* SSR/non-browser context.
|
||||
*/
|
||||
export function computePinnedMaxPx(viewportWidth?: number): number {
|
||||
const vw =
|
||||
viewportWidth ?? (typeof window !== "undefined" ? window.innerWidth : FALLBACK_VIEWPORT);
|
||||
return Math.max(MIN_CAP_PX, Math.round(vw * VIEWPORT_RATIO));
|
||||
}
|
||||
|
||||
/** Minimum pixel width for any pinned column. Below this the panel becomes
|
||||
* unusable (icons clipped, scrollbars stacked). */
|
||||
export const LAYOUT_PINNED_MIN_PX = 180;
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { LayoutPanel } from "./types";
|
||||
|
||||
// Layout sizing constants (single source of truth)
|
||||
// Layout sizing constants (single source of truth).
|
||||
// Pinned column max width is computed at runtime — see ./caps.ts.
|
||||
export const LAYOUT_SIDEBAR_RATIO = 2.5 / 10;
|
||||
export const LAYOUT_RIGHT_RATIO = 2.5 / 10;
|
||||
export const LAYOUT_SIDEBAR_MAX_PX = 350;
|
||||
export const LAYOUT_RIGHT_MAX_PX = 450;
|
||||
|
||||
// Well-known group/panel IDs
|
||||
export const SIDEBAR_GROUP = "group-sidebar";
|
||||
|
||||
@@ -15,8 +15,6 @@ export type {
|
||||
export {
|
||||
LAYOUT_SIDEBAR_RATIO,
|
||||
LAYOUT_RIGHT_RATIO,
|
||||
LAYOUT_SIDEBAR_MAX_PX,
|
||||
LAYOUT_RIGHT_MAX_PX,
|
||||
SIDEBAR_GROUP,
|
||||
CENTER_GROUP,
|
||||
RIGHT_TOP_GROUP,
|
||||
@@ -28,6 +26,9 @@ export {
|
||||
panel,
|
||||
} from "./constants";
|
||||
|
||||
// Runtime caps for pinned columns
|
||||
export { computePinnedMaxPx, LAYOUT_PINNED_MIN_PX } from "./caps";
|
||||
|
||||
// Presets
|
||||
export {
|
||||
defaultLayout,
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { compactLayout, defaultLayout, getPresetSidebarColumn } from "./presets";
|
||||
import { computePinnedMaxPx } from "./caps";
|
||||
|
||||
describe("layout presets", () => {
|
||||
it("keeps the compact workbench on Dockview while prioritizing the center panel", () => {
|
||||
const compact = compactLayout();
|
||||
const compactSidebar = compact.columns.find((column) => column.id === "sidebar");
|
||||
const defaultSidebarMaxWidth =
|
||||
// Default sidebar inherits the runtime cap (no per-column maxWidth);
|
||||
// compact pins itself tighter.
|
||||
const defaultSidebarCap =
|
||||
defaultLayout().columns.find((column) => column.id === "sidebar")?.maxWidth ??
|
||||
Number.POSITIVE_INFINITY;
|
||||
computePinnedMaxPx();
|
||||
|
||||
expect(compact.columns.map((column) => column.id)).toEqual(["sidebar", "center"]);
|
||||
const compactSidebarWidth = compactSidebar?.width ?? Number.POSITIVE_INFINITY;
|
||||
const compactSidebarMaxWidth = compactSidebar?.maxWidth ?? Number.POSITIVE_INFINITY;
|
||||
expect(compactSidebarWidth).toBeLessThan(defaultSidebarMaxWidth);
|
||||
expect(compactSidebarMaxWidth).toBeLessThan(defaultSidebarMaxWidth);
|
||||
expect(compactSidebarWidth).toBeLessThan(defaultSidebarCap);
|
||||
expect(compactSidebarMaxWidth).toBeLessThan(defaultSidebarCap);
|
||||
expect(compact.columns.find((column) => column.id === "center")?.groups[0].panels[0].id).toBe(
|
||||
"chat",
|
||||
);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { LayoutColumn, LayoutState } from "./types";
|
||||
import {
|
||||
LAYOUT_SIDEBAR_MAX_PX,
|
||||
LAYOUT_RIGHT_MAX_PX,
|
||||
SIDEBAR_GROUP,
|
||||
CENTER_GROUP,
|
||||
RIGHT_TOP_GROUP,
|
||||
@@ -10,6 +8,8 @@ import {
|
||||
} from "./constants";
|
||||
|
||||
const COMPACT_SIDEBAR_WIDTH = 220;
|
||||
// Compact preset intentionally caps the sidebar tight (small toolbar look),
|
||||
// so it overrides the runtime cap rather than inheriting it.
|
||||
const COMPACT_SIDEBAR_MAX_PX = 260;
|
||||
|
||||
export function defaultLayout(): LayoutState {
|
||||
@@ -18,7 +18,6 @@ export function defaultLayout(): LayoutState {
|
||||
{
|
||||
id: "sidebar",
|
||||
pinned: true,
|
||||
maxWidth: LAYOUT_SIDEBAR_MAX_PX,
|
||||
groups: [{ id: SIDEBAR_GROUP, panels: [panel("sidebar")] }],
|
||||
},
|
||||
{
|
||||
@@ -29,7 +28,6 @@ export function defaultLayout(): LayoutState {
|
||||
id: "right",
|
||||
pinned: true,
|
||||
width: 350,
|
||||
maxWidth: LAYOUT_RIGHT_MAX_PX,
|
||||
groups: [
|
||||
{ id: RIGHT_TOP_GROUP, panels: [panel("files"), panel("changes")] },
|
||||
{ id: RIGHT_BOTTOM_GROUP, panels: [panel("terminal-default")] },
|
||||
@@ -68,7 +66,6 @@ export function planLayout(): LayoutState {
|
||||
{
|
||||
id: "sidebar",
|
||||
pinned: true,
|
||||
maxWidth: LAYOUT_SIDEBAR_MAX_PX,
|
||||
groups: [{ id: SIDEBAR_GROUP, panels: [panel("sidebar")] }],
|
||||
},
|
||||
{
|
||||
@@ -89,7 +86,6 @@ export function previewLayout(): LayoutState {
|
||||
{
|
||||
id: "sidebar",
|
||||
pinned: true,
|
||||
maxWidth: LAYOUT_SIDEBAR_MAX_PX,
|
||||
groups: [{ id: SIDEBAR_GROUP, panels: [panel("sidebar")] }],
|
||||
},
|
||||
{
|
||||
@@ -110,7 +106,6 @@ export function vscodeLayout(): LayoutState {
|
||||
{
|
||||
id: "sidebar",
|
||||
pinned: true,
|
||||
maxWidth: LAYOUT_SIDEBAR_MAX_PX,
|
||||
groups: [{ id: SIDEBAR_GROUP, panels: [panel("sidebar")] }],
|
||||
},
|
||||
{
|
||||
@@ -119,7 +114,6 @@ export function vscodeLayout(): LayoutState {
|
||||
},
|
||||
{
|
||||
id: "right",
|
||||
maxWidth: LAYOUT_RIGHT_MAX_PX,
|
||||
groups: [{ panels: [panel("vscode")] }],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import type { DockviewApi, SerializedDockview } from "dockview-react";
|
||||
import type { LayoutState, LayoutColumn, LayoutGroup, LayoutPanel, LayoutNode } from "./types";
|
||||
import { computeColumnWidths, computeGroupHeights } from "./sizing";
|
||||
import {
|
||||
SIDEBAR_LOCK,
|
||||
KNOWN_PANEL_IDS,
|
||||
STRUCTURAL_COMPONENTS,
|
||||
LAYOUT_SIDEBAR_MAX_PX,
|
||||
LAYOUT_RIGHT_MAX_PX,
|
||||
} from "./constants";
|
||||
import { SIDEBAR_LOCK, KNOWN_PANEL_IDS, STRUCTURAL_COMPONENTS } from "./constants";
|
||||
|
||||
// Dockview serialized grid node types (internal format)
|
||||
type SerializedLeafNode = {
|
||||
@@ -287,12 +281,7 @@ export function fromDockviewApi(api: DockviewApi): LayoutState {
|
||||
|
||||
columns.push({
|
||||
id: columnId,
|
||||
...(isPinned
|
||||
? {
|
||||
pinned: true,
|
||||
maxWidth: columnId === "right" ? LAYOUT_RIGHT_MAX_PX : LAYOUT_SIDEBAR_MAX_PX,
|
||||
}
|
||||
: {}),
|
||||
...(isPinned ? { pinned: true } : {}),
|
||||
width,
|
||||
groups: flatGroups,
|
||||
// Only store tree for columns with nested structure (branches)
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import type { LayoutColumn, LayoutGroup } from "./types";
|
||||
import { LAYOUT_SIDEBAR_RATIO } from "./constants";
|
||||
import { computePinnedMaxPx, LAYOUT_PINNED_MIN_PX } from "./caps";
|
||||
|
||||
/**
|
||||
* Get the effective pinned width for a column,
|
||||
* considering user overrides, maxWidth cap, and ratio-based default.
|
||||
*
|
||||
* The default cap is viewport-proportional (see ./caps.ts) — column.maxWidth
|
||||
* still wins when explicitly set (e.g. compact preset narrowing the sidebar).
|
||||
*/
|
||||
export function getPinnedWidth(
|
||||
column: LayoutColumn,
|
||||
totalWidth: number,
|
||||
override?: number,
|
||||
): number {
|
||||
const max = column.maxWidth ?? computePinnedMaxPx();
|
||||
const min = column.minWidth ?? LAYOUT_PINNED_MIN_PX;
|
||||
if (override !== undefined) {
|
||||
const max = column.maxWidth ?? Infinity;
|
||||
const min = column.minWidth ?? 50;
|
||||
return Math.max(min, Math.min(override, max));
|
||||
}
|
||||
const ratioWidth = Math.round(totalWidth * LAYOUT_SIDEBAR_RATIO);
|
||||
const max = column.maxWidth ?? Infinity;
|
||||
const min = column.minWidth ?? 50;
|
||||
return Math.max(min, Math.min(ratioWidth, max));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user