From 1bf5164645776defcf599d64b830a39dd4396a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schultz=20Madsen?= Date: Wed, 7 Oct 2026 11:52:48 +0200 Subject: [PATCH 1/2] fix(theme): plugin page roots with their own side padding line up with the burger (#8102) The compliance report root (main.compliance-page, padding 20px) and pages whose routed root is a bare mat-card (backend-configuration task list and calendar task list, core CMS; 16px header/content side padding) started right of the burger glyph in both themes. The page-gutter neutralisers now drop that side padding and flatten the page-root card like the sub-header. The theme spec gets stand-in fixtures for these root shapes in both themes. Co-Authored-By: Claude Opus 5.5 --- ...lication-settings.theme-new-design.spec.ts | 109 ++++++++++++++---- .../src/scss/components/_page-gutter.scss | 22 +++- 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/eform-client/playwright/e2e/Tests/g/application-settings.theme-new-design.spec.ts b/eform-client/playwright/e2e/Tests/g/application-settings.theme-new-design.spec.ts index 3cddce9984..09b5a0828d 100644 --- a/eform-client/playwright/e2e/Tests/g/application-settings.theme-new-design.spec.ts +++ b/eform-client/playwright/e2e/Tests/g/application-settings.theme-new-design.spec.ts @@ -35,48 +35,90 @@ function computedStyle(locator: Locator, property: string): Promise { ); } +// Offsets in px, read fresh on every poll. +function layoutOffsets(page: Page) { + return page.evaluate(() => { + const rect = (el: Element) => el.getBoundingClientRect(); + const topBar = rect(document.querySelector('mat-toolbar.nav-header')!); + return { + topBarBottom: topBar.bottom, + burger: rect(document.querySelector('mat-toolbar.nav-header button mat-icon')!).left - topBar.left, + contentArea: rect(document.querySelector('mat-drawer-container .mat-drawer-content')!).left, + }; + }); +} + // #8102, both themes: no line under the top bar, no separate sub-header bar, // and the page starts right under the top bar, in line with the burger glyph. // The left edge is compared as an offset — content from the content area's // left edge vs the burger icon from the top bar's — which holds with the side // drawer open (content area starts at the drawer edge) and closed, so the // shared page's drawer state is never touched. +// Polled so a layout still settling after the theme switch is waited out +// rather than read once. +async function expectInLineWithBurger(page: Page, content: Locator[]) { + for (const locator of content) { + await expect + .poll(async () => { + const layout = await layoutOffsets(page); + const left = await locator.evaluate((el) => el.getBoundingClientRect().left); + return Math.abs(left - layout.contentArea - layout.burger); + }, { timeout: UI_TIMEOUT, message: 'content left edge vs burger icon (px)' }) + .toBeLessThanOrEqual(1); + } +} + async function expectPageFlushUnderTopBar(page: Page, subHeader: Locator, content: Locator[]) { const topBar = page.locator('mat-toolbar.nav-header'); await expect(topBar).toHaveCSS('border-bottom-style', 'none'); await expect(subHeader).toHaveCSS('border-top-style', 'none'); await expect(subHeader).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); - // Offsets in px; polled so a layout still settling after the theme switch - // is waited out rather than read once. - const offsets = () => - page.evaluate(() => { - const rect = (el: Element) => el.getBoundingClientRect(); - const topBar = rect(document.querySelector('mat-toolbar.nav-header')!); - return { - topBarBottom: topBar.bottom, - burger: rect(document.querySelector('mat-toolbar.nav-header button mat-icon')!).left - topBar.left, - contentArea: rect(document.querySelector('mat-drawer-container .mat-drawer-content')!).left, - }; - }); await expect .poll(async () => { - const layout = await offsets(); + const layout = await layoutOffsets(page); const top = await subHeader.evaluate((el) => el.getBoundingClientRect().top); return Math.abs(top - layout.topBarBottom); }, { timeout: UI_TIMEOUT, message: 'sub-header top vs top bar bottom (px)' }) .toBeLessThanOrEqual(1); - for (const locator of content) { - await expect - .poll(async () => { - const layout = await offsets(); - const left = await locator.evaluate((el) => el.getBoundingClientRect().left); - return Math.abs(left - layout.contentArea - layout.burger); - }, { timeout: UI_TIMEOUT, message: 'content left edge vs burger icon (px)' }) - .toBeLessThanOrEqual(1); - } + await expectInLineWithBurger(page, content); } +// Stand-ins for plugin page roots that bring their own side padding, each +// mounted where a routed page sits (the element after a ). +// The + +
+
+

Compliance

+
+
+ +
+ + +

Tasks and actions

+
+
+
+ +
+ + + CMS + + +
Settings
+
+
+
`; + test.describe.serial('Theme "eForm new design" (workspace variant)', () => { test.describe.configure({ timeout: 240_000 }); @@ -113,6 +155,20 @@ test.describe.serial('Theme "eForm new design" (workspace variant)', () => { await page.evaluate(() => document.querySelectorAll('[data-e2e-fixture]').forEach((el) => el.remove())); } }; + // Plugin page roots with their own side padding still start in line with + // the burger; the bare page-root mat-card is flat, like the sub-header. + const expectPluginPageRootsInLineWithBurger = () => + withContentCardFixture(pluginPageRootsFixture, async () => { + const card = page.locator('#e2eTaskListCard'); + await expect(card).toHaveCSS('border-top-style', 'none'); + await expect(card).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + await expectInLineWithBurger(page, [ + page.locator('#e2eComplianceTitle'), + page.locator('#e2eTaskListTitle'), + page.locator('#e2eCmsTitle'), + page.locator('#e2eCmsContent'), + ]); + }); // A hand-written one-row table (no .mdc-data-table__row), like the legacy grids. const matTable = (cells: string) => `${cells}
`; @@ -181,6 +237,11 @@ test.describe.serial('Theme "eForm new design" (workspace variant)', () => { await expectPageFlushUnderTopBar(page, subHeader, [subHeader.locator('h2'), grid()]); }); + test('eForm Classic theme lines plugin page roots up with the burger', async () => { + await expect(page.locator('body')).toHaveClass(/\btheme-eform\b/); + await expectPluginPageRootsInLineWithBurger(); + }); + test('eForm Classic theme keeps the content card flush around full-bleed views', async () => { const contentCard = page.locator('.content-card').first(); // Stand-in for the backend-configuration calendar's root. @@ -260,6 +321,10 @@ test.describe.serial('Theme "eForm new design" (workspace variant)', () => { await expectPageFlushUnderTopBar(page, subHeader, [subHeader.locator('h2'), grid()]); }); + test('new design lines plugin page roots up with the burger', async () => { + await expectPluginPageRootsInLineWithBurger(); + }); + test('new design keeps the content card flush around full-bleed views', async () => { const contentCard = page.locator('.content-card').first(); // Stand-in for the backend-configuration calendar's root. diff --git a/eform-client/src/scss/components/_page-gutter.scss b/eform-client/src/scss/components/_page-gutter.scss index 332748a6f9..59cc97dcbe 100644 --- a/eform-client/src/scss/components/_page-gutter.scss +++ b/eform-client/src/scss/components/_page-gutter.scss @@ -89,18 +89,38 @@ body:is(.theme-eform, .theme-workspace) { // top/bottom spacing is part of those pages' own layout. // kanban: board list, sprint panel, board settings, reports, statistics // time-planning: pay rule sets + // backend-configuration: compliance report (main.compliance-page) .content-card router-outlet + * > :is( .board-list-container, .sprint-panel, .board-settings, .reports-container, .statistics-container, - .pay-rule-sets-container + .pay-rule-sets-container, + .compliance-page ) { padding-left: 0; padding-right: 0; } + // Pages whose routed root is one bare mat-card wrapping the whole page + // (backend-configuration task list and calendar task list, core CMS). The + // 16px side padding of its mat-card-header and mat-card-content would push + // the page right of the burger, and a framed card around the page is the + // separate "bar" #8102 removed. Flattened like the sub-header, with that + // side padding dropped. A sub-header card (.eform-sub-header) is handled + // above. + .content-card router-outlet + * > mat-card:not(.eform-sub-header) { + border: none !important; + border-radius: 0 !important; + background: transparent !important; + + > :is(.mat-mdc-card-header, .mat-mdc-card-content) { + padding-left: 0; + padding-right: 0; + } + } + // backend-configuration case page: its root is `div.d-grid.p-4`, the only // routed page root using the p-4 utility. !important beats the utility's // own (padding: 1.5rem !important). From 59f08ec77fe109e5f72069ec0a094e028a4e4a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schultz=20Madsen?= Date: Wed, 7 Oct 2026 11:57:43 +0200 Subject: [PATCH 2/2] fix(theme): flatten only a page-root mat-card that is the routed page's sole element (#8102) The bare-card rule also matched real cards that sit next to a sub-header or another card (folders, entity edit/create, visual editor) and stripped their border, radius and background. :only-child limits it to the task list, calendar task list and CMS root shape; the theme spec gains a sub-header + card host that must keep its surface. Co-Authored-By: Claude Opus 5.5 --- .../g/application-settings.theme-new-design.spec.ts | 10 +++++++++- eform-client/src/scss/components/_page-gutter.scss | 6 ++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/eform-client/playwright/e2e/Tests/g/application-settings.theme-new-design.spec.ts b/eform-client/playwright/e2e/Tests/g/application-settings.theme-new-design.spec.ts index 09b5a0828d..879c13281d 100644 --- a/eform-client/playwright/e2e/Tests/g/application-settings.theme-new-design.spec.ts +++ b/eform-client/playwright/e2e/Tests/g/application-settings.theme-new-design.spec.ts @@ -91,6 +91,7 @@ async function expectPageFlushUnderTopBar(page: Page, subHeader: Locator, conten // compliance report: main.compliance-page { padding: var(--spacing-2xl) } // task list / calendar task list: bare root // core CMS: the same bare root with a above the content +// folders page: a card next to a sub-header is a real card and stays framed const pluginPageRootsFixture = ` @@ -117,6 +118,11 @@ const pluginPageRootsFixture = `
Settings
+ + +
+ +
`; test.describe.serial('Theme "eForm new design" (workspace variant)', () => { @@ -156,12 +162,14 @@ test.describe.serial('Theme "eForm new design" (workspace variant)', () => { } }; // Plugin page roots with their own side padding still start in line with - // the burger; the bare page-root mat-card is flat, like the sub-header. + // the burger; the bare page-root mat-card is flat, like the sub-header, + // while a card next to a sub-header keeps its surface. const expectPluginPageRootsInLineWithBurger = () => withContentCardFixture(pluginPageRootsFixture, async () => { const card = page.locator('#e2eTaskListCard'); await expect(card).toHaveCSS('border-top-style', 'none'); await expect(card).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + await expect(page.locator('#e2eFramedCard')).not.toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); await expectInLineWithBurger(page, [ page.locator('#e2eComplianceTitle'), page.locator('#e2eTaskListTitle'), diff --git a/eform-client/src/scss/components/_page-gutter.scss b/eform-client/src/scss/components/_page-gutter.scss index 59cc97dcbe..1bf08b5750 100644 --- a/eform-client/src/scss/components/_page-gutter.scss +++ b/eform-client/src/scss/components/_page-gutter.scss @@ -104,13 +104,15 @@ body:is(.theme-eform, .theme-workspace) { } // Pages whose routed root is one bare mat-card wrapping the whole page - // (backend-configuration task list and calendar task list, core CMS). The + // (backend-configuration task list and calendar task list, core CMS); + // :only-child keeps the real cards that sit next to a sub-header or another + // card (folders, entity edit/create, visual editor) framed. The // 16px side padding of its mat-card-header and mat-card-content would push // the page right of the burger, and a framed card around the page is the // separate "bar" #8102 removed. Flattened like the sub-header, with that // side padding dropped. A sub-header card (.eform-sub-header) is handled // above. - .content-card router-outlet + * > mat-card:not(.eform-sub-header) { + .content-card router-outlet + * > mat-card:only-child:not(.eform-sub-header) { border: none !important; border-radius: 0 !important; background: transparent !important;