From a45ebf3281eda5c2900096ed685209c25af86cca Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:55:30 -0500 Subject: [PATCH] fix(ui): reset settings scroll and align details headers (#68150) thanks @BunsDev Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- ui/src/styles/config.css | 25 ++++++----- ui/src/ui/views/config.browser.test.ts | 58 ++++++++++++++++++++++++++ ui/src/ui/views/config.ts | 31 ++++++++++++-- 3 files changed, 101 insertions(+), 13 deletions(-) diff --git a/ui/src/styles/config.css b/ui/src/styles/config.css index b8a2c0d9603..45faa241f47 100644 --- a/ui/src/styles/config.css +++ b/ui/src/styles/config.css @@ -396,7 +396,7 @@ /* Section Hero */ .config-section-hero { display: flex; - align-items: center; + align-items: flex-start; gap: 14px; padding: 16px 22px; border-bottom: 1px solid var(--border); @@ -431,15 +431,14 @@ display: grid; gap: 2px; min-width: 0; + flex: 1; } .config-section-hero__title { font-size: 15px; font-weight: 650; letter-spacing: -0.02em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + line-height: 1.3; } .config-section-hero__desc { @@ -768,7 +767,7 @@ .config-section-card__header { display: flex; - align-items: center; + align-items: flex-start; gap: 14px; padding: 18px 20px; background: var(--bg-accent); @@ -800,6 +799,8 @@ .config-section-card__titles { flex: 1; min-width: 0; + display: grid; + gap: 4px; } .config-section-card__title { @@ -807,9 +808,7 @@ font-size: 14px; font-weight: 650; letter-spacing: -0.015em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + line-height: 1.35; } .config-section-card__desc { @@ -1318,8 +1317,9 @@ .cfg-object__header { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; + gap: 12px; padding: 10px 12px; cursor: pointer; list-style: none; @@ -1339,6 +1339,7 @@ font-size: 13px; font-weight: 600; color: var(--text); + line-height: 1.35; } .cfg-object__title-wrap { @@ -1351,6 +1352,8 @@ width: 18px; height: 18px; color: var(--muted); + flex-shrink: 0; + margin-top: 1px; transition: transform var(--duration-normal) var(--ease-out); } @@ -1385,11 +1388,12 @@ .cfg-array__header { display: flex; - align-items: center; + align-items: flex-start; gap: 14px; padding: 10px 12px; background: var(--bg-accent); border-bottom: 1px solid var(--border); + flex-wrap: wrap; } :root[data-theme-mode="light"] .cfg-array__header { @@ -1415,6 +1419,7 @@ padding: 4px 10px; background: var(--bg-elevated); border-radius: var(--radius-full); + margin-left: auto; } :root[data-theme-mode="light"] .cfg-array__count { diff --git a/ui/src/ui/views/config.browser.test.ts b/ui/src/ui/views/config.browser.test.ts index ba2fb28f77b..faa9472abae 100644 --- a/ui/src/ui/views/config.browser.test.ts +++ b/ui/src/ui/views/config.browser.test.ts @@ -271,6 +271,64 @@ describe("config view", () => { expect(onSectionChange).toHaveBeenCalledWith("gateway"); }); + it("resets config content scroll when switching top-tab sections", async () => { + const { container } = renderConfigView({ + activeSection: "channels", + navRootLabel: "Communication", + includeSections: ["channels", "messages"], + schema: { + type: "object", + properties: { + channels: { + type: "object", + properties: { + telegram: { type: "string" }, + }, + }, + messages: { + type: "object", + properties: { + inbox: { type: "string" }, + }, + }, + }, + }, + formValue: { + channels: { telegram: "on" }, + messages: { inbox: "smart" }, + }, + originalValue: { + channels: { telegram: "on" }, + messages: { inbox: "smart" }, + }, + }); + + const content = container.querySelector(".config-content"); + expect(content).toBeTruthy(); + if (!content) { + return; + } + content.scrollTop = 280; + content.scrollLeft = 24; + content.scrollTo = vi.fn(({ top, left }: { top?: number; left?: number }) => { + content.scrollTop = top ?? content.scrollTop; + content.scrollLeft = left ?? content.scrollLeft; + }) as typeof content.scrollTo; + + const messagesButton = Array.from(container.querySelectorAll("button")).find( + (btn) => btn.textContent?.trim() === "Messages", + ); + expect(messagesButton).toBeTruthy(); + + messagesButton?.click(); + await Promise.resolve(); + + expect(content.scrollTo).toHaveBeenCalledOnce(); + expect(content.scrollTo).toHaveBeenCalledWith({ top: 0, left: 0, behavior: "auto" }); + expect(content.scrollTop).toBe(0); + expect(content.scrollLeft).toBe(0); + }); + it("wires search input to onSearchChange", () => { const container = document.createElement("div"); const onSearchChange = vi.fn(); diff --git a/ui/src/ui/views/config.ts b/ui/src/ui/views/config.ts index 26a89ae1f3f..b1063d287e7 100644 --- a/ui/src/ui/views/config.ts +++ b/ui/src/ui/views/config.ts @@ -767,6 +767,24 @@ export function renderConfig(props: ConfigProps) { const settingsLayout = props.settingsLayout ?? "tabs"; const allCategories = [...visibleCategories, ...(otherCategory ? [otherCategory] : [])]; + const resetContentScroll = (target: EventTarget | null) => { + queueMicrotask(() => { + const origin = target instanceof Element ? target : null; + const content = origin + ?.closest(".config-main") + ?.querySelector(".config-content"); + if (!content) { + return; + } + if (typeof content.scrollTo === "function") { + content.scrollTo({ top: 0, left: 0, behavior: "auto" }); + return; + } + content.scrollTop = 0; + content.scrollLeft = 0; + }); + }; + function renderAccordionNav() { return html`
@@ -795,12 +813,13 @@ export function renderConfig(props: ConfigProps) { cat.sections.some((s) => s.key === props.activeSection) ? "config-accordion-group__header--active" : ""}" - @click=${() => { + @click=${(e: Event) => { const firstKey = cat.sections[0]?.key ?? null; const isCurrentlyInGroup = cat.sections.some( (s) => s.key === props.activeSection, ); props.onSectionChange(isCurrentlyInGroup ? null : firstKey); + resetContentScroll(e.currentTarget); }} > @@ -832,7 +851,10 @@ export function renderConfig(props: ConfigProps) { class="config-accordion-group__item ${props.activeSection === s.key ? "config-accordion-group__item--active" : ""}" - @click=${() => props.onSectionChange(s.key)} + @click=${(e: Event) => { + props.onSectionChange(s.key); + resetContentScroll(e.currentTarget); + }} > ${getSectionIcon(s.key)} @@ -1004,7 +1026,10 @@ export function renderConfig(props: ConfigProps) { : ""}" role="tab" aria-selected=${props.activeSection === tab.key} - @click=${() => props.onSectionChange(tab.key)} + @click=${(e: Event) => { + props.onSectionChange(tab.key); + resetContentScroll(e.currentTarget); + }} title=${tab.label} > ${tab.label}