Skip to content
Closed
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
11 changes: 10 additions & 1 deletion gui/src/styles-compatibility-matrix.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@
Namespace: lab-matrix-
============================================================================ */

.main-inner:has(.lab-page) {
/*
Scoped to the VISIBLE compatibility panel by id, not to `.lab-page`: the lab renders no
`.lab-page` while it is loading (skeleton), so a `:has(.lab-page)` test dropped the width
to 980px during load and snapped to 1200px afterwards — the tab's size was not fixed. It
also matched from any tab once the lab had been opened (panels stay mounted), leaking the
width across tabs. `#models-panel-compatibility:not([hidden])` is true only while this is
the active tab, so the width is held steady across load/error and never leaks. Matches the
catalog and routing rules in styles-models-workspace.css.
*/
.main-inner:has(#models-panel-compatibility:not([hidden])) {
max-width: 1200px;
box-sizing: border-box;
}
Expand Down
29 changes: 22 additions & 7 deletions gui/src/styles-models-workspace.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,29 @@
/*
The catalog wants a wider column than the 980px default.

Scoped to a VISIBLE catalog panel, not merely a present one: panels mount lazily and
then stay mounted so drafts survive a tab hop, so a bare `:has(.models-workspace-shell)`
keeps matching after the catalog has been opened once. Routing would then render at
980px on a direct visit and 1200px afterwards — a width that depends on browsing
history. No surface renders the shell outside a tabpanel any more, so the old
direct-child arm is gone with the standalone pages it served.
Scoped to the VISIBLE catalog panel by its id: `#models-panel-catalog:not([hidden])` is
true only while catalog is the active tab (an inactive panel carries `hidden`), so this
never leaks onto another tab even though panels stay mounted after their first visit.

It deliberately does NOT also require `.models-workspace-shell`: the shell is absent
while the catalog is loading (skeleton) or after a cold failure (error notice), so gating
on it dropped the width back to 980px in those states and snapped it to 1200px only once
data arrived — the catalog tab's size was not fixed. The panel-id test alone holds the
width steady across load, empty, and error, matching the routing rule below.
*/
.main-inner:has(#models-panel-catalog:not([hidden]) .models-workspace-shell) {
.main-inner:has(#models-panel-catalog:not([hidden])) {
max-width: 1200px;
}

/*
Routing shares the catalog/compatibility 1200px column so hopping between the Models
tabs never resizes the page. Scoped to the VISIBLE routing panel for the same reason
the catalog rule is: panels stay mounted after their first visit, so an unscoped `:has`
would leak this width onto whatever tab is open. Without this rule routing fell back to
the 980px `.main-inner` default — a visible width jump on every hop to it, and one that
only appeared once the compatibility tab's own `:has(.lab-page)` had leaked 1200px in.
*/
.main-inner:has(#models-panel-routing:not([hidden])) {
max-width: 1200px;
}

Expand Down
40 changes: 40 additions & 0 deletions gui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,46 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }
}
.main-inner.main-inner--combos > .page-sub { margin-bottom: 10px; }

/* Keep the Models tab strip border aligned with its tab buttons in the full-bleed layout. */
.main-inner.main-inner--combos > .page-tabs {
margin-inline: 36px;
padding-inline: 0;
}

/*
Full-bleed is for the combos WORKSPACE grid only. Its loading and error fallbacks render
no `.combos-workspace-shell` (Combos.tsx returns a bare skeleton or an error notice), yet
`main-inner--combos` is applied on tab selection alone — so with no workspace to fill, the
lone subtitle/notice/retry stretched edge to edge (padding:0, max-width:none, flex column)
while every sibling Models tab stayed boxed. That is the "콤보만 영역이 이상해짐" report.

When no workspace shell is present, drop the full-bleed and box the page like the other
tabs: reset the container, its chrome inset, and the fill panel back to normal flow.
*/
/*
Each rule below is a SINGLE selector on purpose. Vite's Rolldown CSS minifier corrupts a
comma-separated selector list whose selectors carry `:not(:has(...))` — it emits a stray
`)` before the block and the browser then drops the whole rule. A single `:not(:has())`
selector minifies correctly, so the chrome inset is handled without a list: the container
drops its own horizontal padding and the page chrome keeps the 36px inline padding it
already gets above (`.main-inner--combos > .page-head` etc.), which the panel matches.
*/
.main-inner.main-inner--combos:not(:has(.combos-workspace-shell)) {
max-width: 1200px;
margin: 0 auto;
padding: 32px 0 64px;
min-height: 0;
height: auto;
overflow: visible;
display: block;
}
.main-inner.main-inner--combos:not(:has(.combos-workspace-shell)) > .models-tab-panel--fill:not([hidden]) {
flex: 0 1 auto;
height: auto;
display: block;
padding-inline: 36px;
}

/* ---- page header ---- */
.page-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 6px; }
.page-head h2 { font-size: var(--text-title); }
Expand Down
51 changes: 51 additions & 0 deletions gui/tests/models-tab-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect, test } from "bun:test";
import { effectiveDeclaration, withoutComments } from "./helpers/css-declarations";

async function readStylesheet(path: string): Promise<string> {
return withoutComments(await Bun.file(new URL(path, import.meta.url)).text());
}

test("Models tab strips keep their full-bleed container borders aligned", async () => {
const baseStyles = await readStylesheet("../src/styles.css");
const workspaceStyles = await readStylesheet("../src/styles-models-workspace.css");
const compatibilityStyles = await readStylesheet("../src/styles-compatibility-matrix.css");

// The Combos workspace removes the outer container padding. Replacing the tab strip's
// padding with an equal inline margin keeps its border aligned with the tab buttons.
expect(effectiveDeclaration(
baseStyles,
".main-inner.main-inner--combos > .page-tabs",
"margin-inline",
)).toBe("36px");
expect(effectiveDeclaration(
baseStyles,
".main-inner.main-inner--combos > .page-tabs",
"padding-inline",
)).toBe("0");

// Loading, empty, and error fallbacks do not render the workspace shell. Keep those
// shell-free states boxed instead of allowing the full-bleed Combos container to stretch
// their notice and retry controls edge to edge.
const shellFreeContainer = ".main-inner.main-inner--combos:not(:has(.combos-workspace-shell))";
expect(effectiveDeclaration(baseStyles, shellFreeContainer, "max-width")).toBe("1200px");
expect(effectiveDeclaration(baseStyles, shellFreeContainer, "margin")).toBe("0 auto");
expect(effectiveDeclaration(baseStyles, shellFreeContainer, "padding")).toBe("32px 0 64px");

const shellFreePanel = `${shellFreeContainer} > .models-tab-panel--fill:not([hidden])`;
expect(effectiveDeclaration(baseStyles, shellFreePanel, "display")).toBe("block");
expect(effectiveDeclaration(baseStyles, shellFreePanel, "padding-inline")).toBe("36px");

// Every Models workspace tab uses the same column width, including loading/error states
// where the panel content itself may not have mounted yet.
for (const selector of [
".main-inner:has(#models-panel-catalog:not([hidden]))",
".main-inner:has(#models-panel-routing:not([hidden]))",
]) {
expect(effectiveDeclaration(workspaceStyles, selector, "max-width")).toBe("1200px");
}
expect(effectiveDeclaration(
compatibilityStyles,
".main-inner:has(#models-panel-compatibility:not([hidden]))",
"max-width",
)).toBe("1200px");
});
Loading