From ab2b296d767d2c607ab838b5666ccc22d5db49da Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Thu, 3 Sep 2026 10:47:17 -0400 Subject: [PATCH] Fix window frame chrome --- docs/src/main.tsx | 4 +++ docs/src/window-regression-fixtures.tsx | 13 ++++++-- src/styles.css | 12 ++++--- tests/browser/docs-geometry.spec.ts | 42 +++++++++++++++++++++++++ tests/styles.test.ts | 8 ++++- 5 files changed, 72 insertions(+), 7 deletions(-) diff --git a/docs/src/main.tsx b/docs/src/main.tsx index 9222714..ab8fa74 100644 --- a/docs/src/main.tsx +++ b/docs/src/main.tsx @@ -809,6 +809,10 @@ function App() { Use explicit header, action-row, or chrome modes only when the layout should not adapt. +
  • + Floating chrome joins its title tab to the frame rail. Stacked chrome keeps a + full-width title bar inside the outer frame. +
  • Menu popups clamp to the available viewport at narrow widths.
  • diff --git a/docs/src/window-regression-fixtures.tsx b/docs/src/window-regression-fixtures.tsx index c5e43da..34d188a 100644 --- a/docs/src/window-regression-fixtures.tsx +++ b/docs/src/window-regression-fixtures.tsx @@ -161,8 +161,17 @@ export function WindowRegressionFixtures() { - - + + Changes saved diff --git a/src/styles.css b/src/styles.css index cfbeddd..8b78d68 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1139,16 +1139,18 @@ } .greyui-window-frame { + --greyui-window-shadow: 3px 3px 6px rgb(0 0 0 / 22%); + position: relative; display: grid; isolation: isolate; min-width: 0; max-width: 100%; box-sizing: border-box; - padding: var(--greyui-tab-height) 3px 3px; + padding: calc(var(--greyui-tab-height) + 3px) 3px 3px; border: 0; background: transparent; - box-shadow: 3px 3px 6px rgb(0 0 0 / 22%); + box-shadow: none; } .greyui-window-frame::before { @@ -1160,13 +1162,15 @@ background: var(--greyui-panel); box-shadow: inset 1px 1px 0 var(--greyui-border-light), - inset -1px -1px 0 var(--greyui-border-dark); + inset -1px -1px 0 var(--greyui-border-dark), + var(--greyui-window-shadow); pointer-events: none; } .greyui-window[data-active="false"] .greyui-window-frame { + --greyui-window-shadow: 2px 2px 4px rgb(0 0 0 / 18%); + opacity: 0.94; - box-shadow: 2px 2px 4px rgb(0 0 0 / 18%); } .greyui-window[data-collapsed="true"] .greyui-window-frame { diff --git a/tests/browser/docs-geometry.spec.ts b/tests/browser/docs-geometry.spec.ts index 66a9c14..033f818 100644 --- a/tests/browser/docs-geometry.spec.ts +++ b/tests/browser/docs-geometry.spec.ts @@ -108,10 +108,52 @@ for (const viewport of viewports) { "position", "absolute", ); + await expectFloatingFrameRail(suite, 520); + await expectFloatingFrameRail(suite, 640); + await expectFloatingFrameRail(suite, 820); + await expectStackedTitleJoin(suite, 280); + await expectStackedTitleJoin(suite, 760); } }); } +async function expectFloatingFrameRail(suite: Locator, width: number) { + const geometry = await windowGeometry(suite, width); + + expect(geometry.framePaddingTop).toBe(geometry.tabHeight + 3); + expect(geometry.bodyTop - geometry.tabBottom).toBeCloseTo(3, 5); +} + +async function expectStackedTitleJoin(suite: Locator, width: number) { + const geometry = await windowGeometry(suite, width); + + expect(geometry.bodyTop - geometry.tabBottom).toBeCloseTo(0, 5); +} + +async function windowGeometry(suite: Locator, width: number) { + return suite.locator(`[data-regression-width='${width}']`).evaluate((fixture) => { + const frame = fixture.querySelector(".greyui-window-frame"); + const tab = fixture.querySelector(".greyui-window-tab"); + const body = fixture.querySelector(".greyui-window-body"); + if (frame === null || tab === null || body === null) { + throw new Error( + `Missing Window geometry for ${fixture.dataset.regressionWidth ?? "unknown"}`, + ); + } + + const frameStyle = getComputedStyle(frame); + const tabRect = tab.getBoundingClientRect(); + const bodyRect = body.getBoundingClientRect(); + + return { + bodyTop: bodyRect.top, + framePaddingTop: Number.parseFloat(frameStyle.paddingTop), + tabBottom: tabRect.bottom, + tabHeight: tabRect.height, + }; + }); +} + test("edge menu and form popups remain inside the viewport", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/"); diff --git a/tests/styles.test.ts b/tests/styles.test.ts index f07a4e3..2f1b2d6 100644 --- a/tests/styles.test.ts +++ b/tests/styles.test.ts @@ -64,13 +64,19 @@ describe("default theme contrast", () => { css.indexOf(".greyui-window-frame {"), css.indexOf(".greyui-window-frame::before"), ); + const frameSurfaceRule = css.slice( + css.indexOf(".greyui-window-frame::before"), + css.indexOf('.greyui-window[data-active="false"] .greyui-window-frame'), + ); const bodyRule = css.slice( css.indexOf(".greyui-window-body {"), css.indexOf(".greyui-window-content {"), ); expect(rootRule).not.toContain("box-shadow:"); - expect(frameRule).toContain("box-shadow:"); + expect(frameRule).toContain("padding: calc(var(--greyui-tab-height) + 3px) 3px 3px;"); + expect(frameRule).toContain("box-shadow: none;"); + expect(frameSurfaceRule).toContain("var(--greyui-window-shadow)"); expect(css).toMatch(/\.greyui-window-frame::before\s*\{[\s\S]*?border:/); expect(bodyRule).not.toContain("border:"); expect(bodyRule).not.toContain("box-shadow:");