fix(ui): eliminate double scrollbar on Logs view · openclaw/openclaw@068d88c
potterdigita
·
2026-05-27
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -3637,12 +3637,28 @@ td.data-table-key-col {
|
3637 | 3637 | border: 1px solid var(--border); |
3638 | 3638 | border-radius: var(--radius-md); |
3639 | 3639 | 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); |
3641 | 3642 | min-height: 200px; |
3642 | 3643 | overflow: auto; |
3643 | 3644 | container-type: inline-size; |
3644 | 3645 | } |
3645 | 3646 | |
| 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 | + |
3646 | 3662 | .log-row { |
3647 | 3663 | display: grid; |
3648 | 3664 | grid-template-columns: 90px 70px minmax(140px, 200px) minmax(0, 1fr); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -8,6 +8,33 @@
|
8 | 8 | width: 100%; |
9 | 9 | } |
10 | 10 | |
| 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 | + |
11 | 38 | .settings-section-nav { |
12 | 39 | display: flex; |
13 | 40 | flex-wrap: wrap; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -70,6 +70,14 @@ describe("config-quick styles", () => {
|
70 | 70 | ); |
71 | 71 | }); |
72 | 72 | |
| 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 | + |
73 | 81 | it("avoids transition-all in the quick settings surface", () => { |
74 | 82 | expect(css).not.toContain("transition: all"); |
75 | 83 | }); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -754,6 +754,33 @@
|
754 | 754 | max-height: 380px; |
755 | 755 | } |
756 | 756 | |
| 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 | + |
757 | 784 | .log-row { |
758 | 785 | grid-template-columns: 1fr; |
759 | 786 | gap: 4px; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -87,6 +87,21 @@ describe("chat header responsive mobile styles", () => {
|
87 | 87 | } |
88 | 88 | } |
89 | 89 | }); |
| 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 | +}); |
90 | 105 | }); |
91 | 106 | |
92 | 107 | describe("sidebar menu trigger styles", () => { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -243,6 +243,16 @@ describe("renderApp assistant avatar routing", () => {
|
243 | 243 | expect(shell?.style.getPropertyValue("--chat-message-max-width")).toBe("min(1280px, 82%)"); |
244 | 244 | }); |
245 | 245 | |
| 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 | + |
246 | 256 | it("passes security quick setting fields to Quick Settings", () => { |
247 | 257 | const state = createState({ |
248 | 258 | configForm: { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1842,7 +1842,11 @@ export function renderApp(state: AppViewState) {
|
1842 | 1842 | </div> |
1843 | 1843 | </aside> |
1844 | 1844 | </div> |
1845 | | - <main class="content ${isChat ? "content--chat" : ""}"> |
| 1845 | + <main |
| 1846 | + class="content ${isChat ? "content--chat" : ""} ${state.tab === "logs" |
| 1847 | + ? "content--logs" |
| 1848 | + : ""}" |
| 1849 | + > |
1846 | 1850 | ${state.updateStatusBanner |
1847 | 1851 | ? html`<div class="callout ${state.updateStatusBanner.tone}" role="alert"> |
1848 | 1852 | ${state.updateStatusBanner.text} |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -55,7 +55,7 @@ export function renderLogs(props: LogsProps) {
|
55 | 55 | const exportLabel = needle || levelFiltered ? "filtered" : "visible"; |
56 | 56 | |
57 | 57 | return html` |
58 | | - <section class="card"> |
| 58 | + <section class="card card--fill-height"> |
59 | 59 | <div class="row" style="justify-content: space-between;"> |
60 | 60 | <div> |
61 | 61 | <div class="card-title">Logs</div> |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。