Skip to content

Commit 068d88c

Browse files
fix(ui): eliminate double scrollbar on Logs view
Keep the Logs page from rendering competing outer page and inner log-stream scrollbars. The Logs route now opts into an explicit content class for desktop fill-height layout, while mobile keeps the single-page scroll behavior with the capped log panel. Also adds regression coverage for the route class and CSS ownership selectors. Co-authored-by: Brian potter <brian@potterdigital.com>
1 parent 0f608bc commit 068d88c

8 files changed

Lines changed: 110 additions & 3 deletions

File tree

ui/src/styles/components.css

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3637,12 +3637,28 @@ td.data-table-key-col {
36373637
border: 1px solid var(--border);
36383638
border-radius: var(--radius-md);
36393639
background: var(--card);
3640-
max-height: calc(100vh - 280px); /* top nav + filter bar + page padding + breathing room */
3640+
/* Default sizing for non-fill-height hosts; .card--fill-height overrides below. */
3641+
max-height: calc(100vh - 280px);
36413642
min-height: 200px;
36423643
overflow: auto;
36433644
container-type: inline-size;
36443645
}
36453646

3647+
/* Fill-height card: flex column that consumes the remaining settings-workspace
3648+
body height. `.content--logs` owns the ancestor chain. */
3649+
.card--fill-height {
3650+
display: flex;
3651+
flex-direction: column;
3652+
flex: 1 1 auto;
3653+
min-height: 0;
3654+
}
3655+
3656+
.card--fill-height .log-stream {
3657+
flex: 1 1 auto;
3658+
min-height: 0;
3659+
max-height: none;
3660+
}
3661+
36463662
.log-row {
36473663
display: grid;
36483664
grid-template-columns: 90px 70px minmax(140px, 200px) minmax(0, 1fr);

ui/src/styles/config-quick.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@
88
width: 100%;
99
}
1010

11+
/* Logs opt into a fill-height chain so the log stream, not the page, owns
12+
desktop scrolling. Other settings routes keep the default `.content` scroll.
13+
14+
The chain works because the entire ancestor stack is converted to flex
15+
columns so the workspace can take the remaining viewport after the
16+
`.content-header` breadcrumb without triggering `.content`'s overflow scroll. */
17+
.content--logs {
18+
display: flex;
19+
flex-direction: column;
20+
overflow: hidden;
21+
}
22+
23+
.content--logs .settings-workspace {
24+
flex: 1 1 auto;
25+
min-height: 0;
26+
display: flex;
27+
flex-direction: column;
28+
height: auto;
29+
}
30+
31+
.content--logs .settings-workspace__body {
32+
flex: 1 1 auto;
33+
min-height: 0;
34+
display: flex;
35+
flex-direction: column;
36+
}
37+
1138
.settings-section-nav {
1239
display: flex;
1340
flex-wrap: wrap;

ui/src/styles/config-quick.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ describe("config-quick styles", () => {
7070
);
7171
});
7272

73+
it("scopes the logs fill-height chain to the explicit logs route class", () => {
74+
expect(css).toContain(".content--logs {");
75+
expectSelectorBlockToMatch(".content--logs", /overflow:\s*hidden;/);
76+
expectSelectorBlockToMatch(".content--logs .settings-workspace", /display:\s*flex;/);
77+
expectSelectorBlockToMatch(".content--logs .settings-workspace__body", /min-height:\s*0;/);
78+
expect(css).not.toContain(":has(.card--fill-height)");
79+
});
80+
7381
it("avoids transition-all in the quick settings surface", () => {
7482
expect(css).not.toContain("transition: all");
7583
});

ui/src/styles/layout.mobile.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,33 @@
754754
max-height: 380px;
755755
}
756756

757+
/* Reset the desktop fill-height chain on mobile — single page scroll is
758+
more usable on small viewports than a tiny pinned log panel.
759+
Selectors are intentionally double-classed so they outrank the
760+
desktop rules in components.css / config-quick.css, which are
761+
imported AFTER this file (see ui/src/styles.css). */
762+
.card--fill-height.card--fill-height .log-stream {
763+
flex: none;
764+
min-height: 200px;
765+
max-height: 380px;
766+
}
767+
768+
.content.content--logs {
769+
display: block;
770+
overflow-y: auto;
771+
}
772+
773+
.content.content--logs .settings-workspace {
774+
display: block;
775+
height: auto;
776+
flex: initial;
777+
}
778+
779+
.content.content--logs .settings-workspace__body {
780+
display: block;
781+
flex: initial;
782+
}
783+
757784
.log-row {
758785
grid-template-columns: 1fr;
759786
gap: 4px;

ui/src/styles/layout.mobile.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,21 @@ describe("chat header responsive mobile styles", () => {
8787
}
8888
}
8989
});
90+
91+
it("restores single-page logs scrolling on mobile", () => {
92+
const mobileCss = readMobileCss();
93+
94+
expect(mobileCss).toContain(".content.content--logs {");
95+
expect(mobileCss).toMatch(
96+
/\.content\.content--logs \{[\s\S]*display: block;[\s\S]*overflow-y: auto;/,
97+
);
98+
expect(mobileCss).toMatch(
99+
/\.content\.content--logs \.settings-workspace \{[\s\S]*display: block;/,
100+
);
101+
expect(mobileCss).toMatch(
102+
/\.card--fill-height\.card--fill-height \.log-stream \{[\s\S]*max-height: 380px;/,
103+
);
104+
});
90105
});
91106

92107
describe("sidebar menu trigger styles", () => {

ui/src/ui/app-render.assistant-avatar.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,16 @@ describe("renderApp assistant avatar routing", () => {
243243
expect(shell?.style.getPropertyValue("--chat-message-max-width")).toBe("min(1280px, 82%)");
244244
});
245245

246+
it("marks the logs route so the page can hand scroll ownership to the log stream", () => {
247+
const container = document.createElement("div");
248+
249+
render(renderApp(createState({ tab: "logs" })), container);
250+
251+
const content = container.querySelector<HTMLElement>("main.content");
252+
expect(content?.classList.contains("content--logs")).toBe(true);
253+
expect(content?.classList.contains("content--chat")).toBe(false);
254+
});
255+
246256
it("passes security quick setting fields to Quick Settings", () => {
247257
const state = createState({
248258
configForm: {

ui/src/ui/app-render.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1842,7 +1842,11 @@ export function renderApp(state: AppViewState) {
18421842
</div>
18431843
</aside>
18441844
</div>
1845-
<main class="content ${isChat ? "content--chat" : ""}">
1845+
<main
1846+
class="content ${isChat ? "content--chat" : ""} ${state.tab === "logs"
1847+
? "content--logs"
1848+
: ""}"
1849+
>
18461850
${state.updateStatusBanner
18471851
? html`<div class="callout ${state.updateStatusBanner.tone}" role="alert">
18481852
${state.updateStatusBanner.text}

ui/src/ui/views/logs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function renderLogs(props: LogsProps) {
5555
const exportLabel = needle || levelFiltered ? "filtered" : "visible";
5656

5757
return html`
58-
<section class="card">
58+
<section class="card card--fill-height">
5959
<div class="row" style="justify-content: space-between;">
6060
<div>
6161
<div class="card-title">Logs</div>

0 commit comments

Comments
 (0)