From 1ad25e3abdf36afb0a3e6a63d96fe4bd45ff27b4 Mon Sep 17 00:00:00 2001 From: Salem <40477317+SalemOurabi@users.noreply.github.com> Date: Tue, 14 Jul 2026 04:47:24 +0200 Subject: [PATCH 1/3] fix(settings): restore compact save action chip --- .../src/settings-layout.e2e.ts | 46 +++++++++++++++++++ apps/web/src/_settings-theme.scss | 18 ++------ .../src/app/settings/settings.component.scss | 21 +++------ 3 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 apps/electron-backend-e2e/src/settings-layout.e2e.ts diff --git a/apps/electron-backend-e2e/src/settings-layout.e2e.ts b/apps/electron-backend-e2e/src/settings-layout.e2e.ts new file mode 100644 index 000000000..1f918f471 --- /dev/null +++ b/apps/electron-backend-e2e/src/settings-layout.e2e.ts @@ -0,0 +1,46 @@ +import { + closeElectronApp, + expect, + launchElectronApp, + openSettings, + test, +} from './electron-test-fixtures'; + +test.describe('Electron Settings Layout', () => { + test('@settings @electron keeps the save action in a compact floating chip', async ({ + dataDir, + }) => { + const app = await launchElectronApp(dataDir); + + try { + await openSettings(app.mainWindow); + + const layout = app.mainWindow.locator('.settings-layout'); + const actionBar = app.mainWindow.getByTestId('settings-action-bar'); + await expect(layout).toBeVisible(); + await expect(actionBar).toBeVisible(); + + const [layoutBox, actionBarBox] = await Promise.all([ + layout.evaluate((element) => { + const { width, x } = element.getBoundingClientRect(); + return { width, x }; + }), + actionBar.evaluate((element) => { + const { width, x } = element.getBoundingClientRect(); + return { width, x }; + }), + ]); + + expect(actionBarBox.width).toBeLessThan(layoutBox.width / 2); + expect( + Math.abs( + layoutBox.x + + layoutBox.width - + (actionBarBox.x + actionBarBox.width) + ) + ).toBeLessThanOrEqual(2); + } finally { + await closeElectronApp(app); + } + }); +}); diff --git a/apps/web/src/_settings-theme.scss b/apps/web/src/_settings-theme.scss index 8a843b60e..c9e37a6d8 100644 --- a/apps/web/src/_settings-theme.scss +++ b/apps/web/src/_settings-theme.scss @@ -1,19 +1,9 @@ .settings-container { --settings-safe-bottom: env(safe-area-inset-bottom, 0px); - // Action bar is a full-width sticky footer, so it sits flush against - // the bottom edge of the scroll container — no hover offset. The - // previous 24px gap was tuned for the old floating-chip layout where - // the bar visually hovered above the bottom. - --settings-action-bar-offset: 0px; - // 0 by default — the action bar is now the last child of the form - // and acts as the natural bottom edge, so no padding-bottom is - // needed below it. The previous 112px/80px reservation existed to - // push content above the OLD floating-chip layout, but in the new - // sticky-footer model that just added dead scrollable space below - // the last setting row. The bar's own padding (12px) and the - // section title bottom-borders already provide visual breathing - // room when scrolled to the end. - --settings-action-bar-space: 0px; + // Keep the floating action chip clear of the window edge and reserve + // enough scroll space so it never covers the final settings row. + --settings-action-bar-offset: 24px; + --settings-action-bar-space: 112px; --settings-page-header-bg: color-mix( in srgb, var(--mat-sys-surface-container-low) 94%, diff --git a/apps/web/src/app/settings/settings.component.scss b/apps/web/src/app/settings/settings.component.scss index f3b7d22df..29317cee4 100644 --- a/apps/web/src/app/settings/settings.component.scss +++ b/apps/web/src/app/settings/settings.component.scss @@ -738,31 +738,23 @@ ); } -// Sticky-footer Save bar — full content width so it never overlaps form -// rows. The previous chip-style bar (border-radius:18px, margin-left:auto) -// floated in the bottom-right corner and clipped fields underneath; the -// UX audit called this out as the worst of three available patterns. -// Now the bar acts as a proper footer: spans the column, separator line -// on top, sits flush with the bottom edge while still sticking on scroll. +// Compact sticky action chip. The settings layout reserves space below the +// content so this remains visible without covering the final form rows. .action-bar { position: sticky; bottom: calc( var(--settings-action-bar-offset) + var(--settings-safe-bottom) ); - margin-left: 0; - margin-right: 0; + margin-left: auto; z-index: 100; display: flex; justify-content: flex-end; align-items: center; gap: 12px; flex-wrap: wrap; - padding: 12px 18px; - border-top: 1px solid var(--settings-action-bar-border); - border-left: 0; - border-right: 0; - border-bottom: 0; - border-radius: 0; + padding: 10px 14px; + border: 1px solid var(--settings-action-bar-border); + border-radius: 18px; background: var(--settings-action-bar-bg); backdrop-filter: blur(12px); box-shadow: var(--settings-action-bar-shadow); @@ -775,7 +767,6 @@ box-shadow: none; border-radius: 16px; padding: 12px; - border-top: 1px solid var(--settings-action-bar-border); justify-content: space-between; } From 10f548a009286fb2e3fe3fa268a43c8701a98cbe Mon Sep 17 00:00:00 2001 From: Salem <40477317+SalemOurabi@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:41:51 +0200 Subject: [PATCH 2/3] fix(settings): cover sticky action layouts --- .../src/settings-layout.e2e.ts | 62 +++++++++++++++++++ .../src/app/settings/settings.component.scss | 2 + 2 files changed, 64 insertions(+) diff --git a/apps/electron-backend-e2e/src/settings-layout.e2e.ts b/apps/electron-backend-e2e/src/settings-layout.e2e.ts index 1f918f471..66985576f 100644 --- a/apps/electron-backend-e2e/src/settings-layout.e2e.ts +++ b/apps/electron-backend-e2e/src/settings-layout.e2e.ts @@ -39,6 +39,68 @@ test.describe('Electron Settings Layout', () => { (actionBarBox.x + actionBarBox.width) ) ).toBeLessThanOrEqual(2); + + const actionBarStyles = await actionBar.evaluate((element) => { + const styles = getComputedStyle(element); + return { + bottom: Number.parseFloat(styles.bottom), + position: styles.position, + }; + }); + expect(actionBarStyles.position).toBe('sticky'); + expect(actionBarStyles.bottom).toBeGreaterThanOrEqual(24); + expect( + await layout.evaluate((element) => + Number.parseFloat(getComputedStyle(element).paddingBottom) + ) + ).toBeGreaterThanOrEqual(112); + + const scrollViewport = app.mainWindow.locator('.workspace-content'); + const finalSection = app.mainWindow.locator( + 'app-settings-about-section .settings-group' + ); + await scrollViewport.evaluate((element) => { + element.scrollTop = element.scrollHeight / 2; + }); + await expect + .poll(() => + scrollViewport.evaluate((element) => element.scrollTop) + ) + .toBeGreaterThan(0); + + const [viewportBox, stickyActionBarBox] = await Promise.all([ + scrollViewport.boundingBox(), + actionBar.boundingBox(), + ]); + expect(viewportBox).not.toBeNull(); + expect(stickyActionBarBox).not.toBeNull(); + const viewport = viewportBox as NonNullable; + const stickyActionBar = stickyActionBarBox as NonNullable< + typeof stickyActionBarBox + >; + expect(stickyActionBar.y).toBeGreaterThanOrEqual(viewport.y); + expect( + stickyActionBar.y + stickyActionBar.height + ).toBeLessThanOrEqual(viewport.y + viewport.height); + + await scrollViewport.evaluate((element) => { + element.scrollTop = element.scrollHeight; + }); + const [finalSectionBox, finalActionBarBox] = await Promise.all([ + finalSection.boundingBox(), + actionBar.boundingBox(), + ]); + expect(finalSectionBox).not.toBeNull(); + expect(finalActionBarBox).not.toBeNull(); + const finalSectionBounds = finalSectionBox as NonNullable< + typeof finalSectionBox + >; + const finalActionBarBounds = finalActionBarBox as NonNullable< + typeof finalActionBarBox + >; + expect( + finalSectionBounds.y + finalSectionBounds.height + ).toBeLessThanOrEqual(finalActionBarBounds.y); } finally { await closeElectronApp(app); } diff --git a/apps/web/src/app/settings/settings.component.scss b/apps/web/src/app/settings/settings.component.scss index 29317cee4..83520ae98 100644 --- a/apps/web/src/app/settings/settings.component.scss +++ b/apps/web/src/app/settings/settings.component.scss @@ -762,6 +762,8 @@ .settings-container.dialog-mode .action-bar { position: static; + align-self: stretch; + margin-left: 0; margin-top: 12px; margin-bottom: 2px; box-shadow: none; From 92dab541dc93831bc4f2c30be4cf3d59f79230dd Mon Sep 17 00:00:00 2001 From: Salem <40477317+SalemOurabi@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:08:34 +0200 Subject: [PATCH 3/3] fix(settings): preserve sticky action spacing --- apps/electron-backend-e2e/src/settings-layout.e2e.ts | 3 +++ apps/web/src/app/settings/settings.component.scss | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/electron-backend-e2e/src/settings-layout.e2e.ts b/apps/electron-backend-e2e/src/settings-layout.e2e.ts index 66985576f..744c88723 100644 --- a/apps/electron-backend-e2e/src/settings-layout.e2e.ts +++ b/apps/electron-backend-e2e/src/settings-layout.e2e.ts @@ -13,6 +13,9 @@ test.describe('Electron Settings Layout', () => { const app = await launchElectronApp(dataDir); try { + await app.electronApp.evaluate(({ BrowserWindow }) => { + BrowserWindow.getAllWindows()[0]?.setSize(1000, 760); + }); await openSettings(app.mainWindow); const layout = app.mainWindow.locator('.settings-layout'); diff --git a/apps/web/src/app/settings/settings.component.scss b/apps/web/src/app/settings/settings.component.scss index 83520ae98..7ab4b992c 100644 --- a/apps/web/src/app/settings/settings.component.scss +++ b/apps/web/src/app/settings/settings.component.scss @@ -816,7 +816,6 @@ @media (max-width: 1100px) { .settings-layout { max-width: 100%; - padding: 0; } }