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..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 @@ -35,48 +35,96 @@ 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 +161,22 @@ 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, + // 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'), + 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 +245,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 +329,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..1bf08b5750 100644 --- a/eform-client/src/scss/components/_page-gutter.scss +++ b/eform-client/src/scss/components/_page-gutter.scss @@ -89,18 +89,40 @@ 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); + // :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:only-child: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).