Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/database/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2842,6 +2842,13 @@ body.is-resizing .dbv-tl__labels-resize {
* full-height flex / nested scroll / outer padding neutralization lives in
* the SDK sheet now. */

/* The Properties|Comments strip (shared `EntityCommentsPanel`) mounts INSIDE
* the padded body here, not at the panel's top edge like Preview / Notes, so
* its own `--space-4` inset would double up against the body's. */
.db-inspector__props-host .bs-panel-tabs {
padding-inline: 0;
}

/* Two-column property grid — label left, value right, baseline-aligned.
* Used by the System metadata block (created / updated / id). */
.db-inspector__props {
Expand Down
4 changes: 3 additions & 1 deletion apps/files/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1392,7 +1392,9 @@ body:not([data-inspector="open"]) .inspector {
.inspector__body {
flex: 1 1 auto;
overflow-y: auto;
padding: var(--space-2);
/* The inset every inspector body shares (Database / Preview / Notes),
* which is also where the tab labels above start. */
padding: var(--space-4);
font-size: var(--text-size-md);
}

Expand Down
4 changes: 3 additions & 1 deletion packages/editor/src/comments/comments-styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@
flex: 0 0 auto;
display: flex;
gap: var(--space-1, var(--space-1));
padding: var(--space-1, var(--space-1)) var(--space-2, var(--space-2));
/* Inline inset matches the panel body's (`--space-4`) so the strip's
* outer edges line up with the rows, headings and buttons below it. */
padding: var(--space-1, var(--space-1)) var(--space-4, var(--space-4));
border-bottom: var(--border-width) solid var(--border, rgba(127, 127, 127, 0.18));
}

Expand Down
8 changes: 7 additions & 1 deletion packages/sdk/src/properties-panel/properties-panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,13 @@
align-items: start;
gap: var(--space-2);
/* Tighter vertical rhythm so the list reads as a dense property sheet
* rather than an airy form — horizontal padding stays for the hover row. */
* rather than an airy form — horizontal padding stays for the hover row.
* The hover face BLEEDS into the host's gutter instead of indenting the
* text: the negative inline margin cancels the horizontal padding, so a
* label starts on the same x as the section headings, buttons and meta
* rows around it (they sit on the host's inset) rather than 8px further
* in. Every host that renders these rows pads at least `--space-2`. */
margin-inline: calc(-1 * var(--space-2));
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-md);
}
Expand Down
29 changes: 29 additions & 0 deletions packages/shell/src/main/apps/window-container-split.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SPLIT_DIVIDER_PX, SPLIT_MIN_PANE_PX } from "../window/split-layout";
import { SplitOrder, SplitPane } from "../window/split-state";
import {
type BaseWindowHandle,
PANE_EDGES_CHANNEL,
type Rectangle,
type Tab,
type TabViewFactory,
Expand Down Expand Up @@ -409,4 +410,32 @@ describe("WindowContainer split view", () => {
expect(side.route).toBe("brainstorm://entity/c");
expect(t.primary.route).toBe("brainstorm://entity/a");
});

it("reserves the OS-control gutter only on the window edge each pane runs along", () => {
const t = setup();
const edges = (tab: Tab) =>
(view(tab).webContents.send as ReturnType<typeof vi.fn>).mock.calls
.filter((c) => c[0] === PANE_EDGES_CHANNEL)
.map((c) => c[1]);
// A lone pane spans the whole window: traffic lights AND overlay controls.
expect(edges(t.primary).at(-1)).toEqual({ left: true, right: true });

const side = t.container.openSidePane({ ...t.spec("Side"), appId: OTHER });
// Primary first: it alone sits under the traffic lights; the side pane
// alone under the Win/Linux controls.
expect(edges(t.primary).at(-1)).toEqual({ left: true, right: false });
expect(edges(side).at(-1)).toEqual({ left: false, right: true });

// A divider drag re-lays out on every pointer move: no re-send without a change.
const before = edges(t.primary).length;
t.container.layout();
expect(edges(t.primary).length).toBe(before);

t.container.swapPanes();
expect(edges(t.primary).at(-1)).toEqual({ left: false, right: true });
expect(edges(side).at(-1)).toEqual({ left: true, right: false });

t.container.closeSidePane();
expect(edges(t.primary).at(-1)).toEqual({ left: true, right: true });
});
});
48 changes: 47 additions & 1 deletion packages/shell/src/main/apps/window-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@ export type Tab = {
icon: string | null;
};

/** Which window edges a pane's header runs along. The app header reserves the
* OS window controls only on the edges it actually abuts: in a split, the
* left pane alone sits under the macOS traffic lights and the right pane
* alone under the Win/Linux `titleBarOverlay`, so neither reserves the other
* side's gutter. Sent to the pane's preload on `PANE_EDGES_CHANNEL`. */
export type PaneEdges = { left: boolean; right: boolean };
export const PANE_EDGES_CHANNEL = "window:pane-edges-changed";
const WINDOW_EDGES: PaneEdges = { left: true, right: true };
const LEFT_PANE_EDGES: PaneEdges = { left: true, right: false };
const RIGHT_PANE_EDGES: PaneEdges = { left: false, right: true };

export type WindowContainerOptions = {
appId: string;
baseWindow: BaseWindowHandle;
Expand Down Expand Up @@ -220,6 +231,9 @@ export class WindowContainer {
private stripVisible = false;
private revealed = false;
private readonly perTabDetach = new Map<string, () => void>();
/** Last edges sent per tab, so a resize-driven `layout()` only re-sends on
* a real change (the divider drag lays out on every pointer move). */
private readonly paneEdgesSent = new Map<string, string>();
private readonly changeListeners = new Set<() => void>();
private detachBaseWindow: (() => void) | null = null;
private disposed = false;
Expand Down Expand Up @@ -676,10 +690,39 @@ export class WindowContainer {
const primaryFirst = this.split.order === SplitOrder.PrimaryFirst;
active.view.setBounds(primaryFirst ? rects.left : rects.right);
this.side.view.setBounds(primaryFirst ? rects.right : rects.left);
this.syncPaneEdges(active);
this.syncPaneEdges(this.side);
// A resize moves the divider; the chrome draws it, so it must hear.
this.pushChromeState();
} else if (active) {
active.view.setBounds(body);
this.syncPaneEdges(active);
}
}

/** The window edges `tab`'s header runs along under the current layout. */
private edgesFor(tab: Tab): PaneEdges {
if (!this.isSplit()) return WINDOW_EDGES;
const sideFirst = this.split.order === SplitOrder.SideFirst;
const isLeft = (tab === this.side) === sideFirst;
return isLeft ? LEFT_PANE_EDGES : RIGHT_PANE_EDGES;
}

private syncPaneEdges(tab: Tab): void {
const edges = this.edgesFor(tab);
const key = `${edges.left}:${edges.right}`;
if (this.paneEdgesSent.get(tab.tabId) === key) return;
this.paneEdgesSent.set(tab.tabId, key);
this.sendPaneEdges(tab, edges);
}

private sendPaneEdges(tab: Tab, edges: PaneEdges): void {
const wc = tab.view?.webContents;
if (!wc || wc.isDestroyed()) return;
try {
wc.send(PANE_EDGES_CHANNEL, edges);
} catch {
// Renderer gone between layout and send — harmless.
}
}

Expand Down Expand Up @@ -864,8 +907,10 @@ export class WindowContainer {
this.reveal();
// A tab can finish loading after the strip is already up (opened into an
// existing multi-tab window); its preload assumed the single-tab gutter,
// so re-send the live state once it can receive it.
// so re-send the live state once it can receive it. Same for the pane
// edges: a side pane is laid out before its renderer exists.
this.sendStripVisible(tab, this.stripVisible);
this.sendPaneEdges(tab, this.edgesFor(tab));
};
// Shell window-management chords (close tab / cycle tabs) are intercepted
// before the app renderer sees them.
Expand Down Expand Up @@ -921,6 +966,7 @@ export class WindowContainer {
private unwireTab(tab: Tab): void {
this.perTabDetach.get(tab.tabId)?.();
this.perTabDetach.delete(tab.tabId);
this.paneEdgesSent.delete(tab.tabId);
}

private teardownTab(tab: Tab): void {
Expand Down
25 changes: 22 additions & 3 deletions packages/shell/src/preload/app-preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,9 @@ let lastChromeCss = "";
// the strip row ABOVE the app, so the header below stops reserving their gutter.
let chromeFullscreen = false;
let chromeStripVisible = false;
// Which window edges this pane's header runs along (mirrors `PaneEdges` in
// `main/apps/window-container.ts`). A lone pane spans both; a split pane one.
let chromePaneEdges = { left: true, right: true };

function whenDocumentReady(fn: () => void): void {
if (typeof document === "undefined") return;
Expand Down Expand Up @@ -877,13 +880,17 @@ function themeColorScheme(color: string): "dark" | "light" {
// Any other case → 12px
//
// When the tab strip is visible the OS controls live in the strip row, not over
// this header, so the gutter collapses to the default on every platform.
// this header, so the gutter collapses to the default on every platform. In a
// split view a pane reserves a gutter only on the window edge it runs along:
// the right pane never sits under the traffic lights, the left pane never under
// the Win/Linux controls (`window:pane-edges-changed`).
function applyChrome(): void {
try {
const reserveControls = !chromeFullscreen && !chromeStripVisible;
const padLeft = IS_MAC && reserveControls ? CHROME_PAD_MAC_LEFT : CHROME_PAD_DEFAULT;
const padLeft =
IS_MAC && reserveControls && chromePaneEdges.left ? CHROME_PAD_MAC_LEFT : CHROME_PAD_DEFAULT;
let padRight = CHROME_PAD_DEFAULT;
if (reserveControls) {
if (reserveControls && chromePaneEdges.right) {
if (IS_WIN) padRight = CHROME_PAD_WIN_RIGHT;
else if (IS_LINUX) padRight = CHROME_PAD_LINUX_RIGHT;
}
Expand Down Expand Up @@ -1144,6 +1151,18 @@ ipcRenderer.on("window:strip-visible-changed", (_event, stripVisible: boolean) =
applyChrome();
});

// Split view: the container tells each pane which window edges it runs along,
// so only the left pane reserves the traffic-light gutter and only the right
// pane the Win/Linux controls gutter. Shape-checked — main is trusted, but a
// malformed payload must not leave the header with an undefined inset.
ipcRenderer.on("window:pane-edges-changed", (_event, edges: unknown) => {
if (!edges || typeof edges !== "object") return;
const { left, right } = edges as { left?: unknown; right?: unknown };
if (typeof left !== "boolean" || typeof right !== "boolean") return;
chromePaneEdges = { left, right };
applyChrome();
});

// Window visibility (parked/hidden vs shown). Mirrors the main-process
// `window:visibility-changed` emit (see launch-setup.ts). On macOS a parked
// window's renderer never sees a Page Visibility change, so apps that run an
Expand Down
Loading