From 0d0d7d99e00196ca273dfb1621789faf9634918a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schultz=20Madsen?= Date: Sun, 13 Sep 2026 09:05:47 +0200 Subject: [PATCH] fix(theme): new-design follow-ups for time-planning, compliance report and review findings Time-planning planning grid (.mtx-grid.time-dashboard): the plugin boxes every header cell and its status classes force !important borders. The header now has only the tint and the bottom line; body day dividers stay but in the row-separator colour, and none against the frame's edges. Compliance report "Rapport" grid (and every mtx-grid): - box-sizing: border-box, so width 100% plus the 1px frame no longer overflows and an overflow: hidden parent stops clipping the right edge. - No mtx divider after left-pinned columns (time-planning keeps its pale one). - Pinned header cells keep the --bg tint against plugin paints. - Thin themed scrollbar on grid scrollers. Review follow-ups (post-merge review of #8061 and Copilot's comments): - Sub-header and px-3 gutter rules scoped to the content card, so the simple-layout connection-setup page keeps its padding. - Backend-configuration case page: its routed div.p-4 root no longer doubles the 24px gutter. - Hand-written mat-mdc-table rows lose the separator under the last row. - Radius token and :is() grouping; icon-button comment made accurate. eForm Classic is unchanged: every rule is scoped to body.theme-workspace. Playwright (shard g): checks the last-row edge, grid box-sizing, the pinned header tint against an injected plugin paint, the full-bleed content-card exception, time-planning dividers and pinned-left dividers. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HfLyiGF7BQTRixiUfSuXxD --- ...lication-settings.theme-new-design.spec.ts | 97 ++++++++++++++++++- .../components/_workspace-mat-overrides.scss | 80 +++++++++++++-- .../scss/components/_workspace-patterns.scss | 39 +++++--- .../scss/components/_workspace-utilities.scss | 6 +- 4 files changed, 193 insertions(+), 29 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 f157743c38..8d26f8ad1c 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 @@ -51,6 +51,27 @@ test.describe.serial('Theme "eForm new design" (workspace variant)', () => { }); const siteRowFirstCell = () => siteRow().locator('td.mat-mdc-cell').first(); const siteRowActionBtn = () => siteRow().locator('[id^="action-items-"] #actionMenu'); + // The Device Users row of the user created in beforeAll. + const deviceUserRow = () => page.locator('tbody > tr', { hasText: firstName }); + + // Appends stand-in markup to the content card while `assertions` run, then + // removes it, so fixtures never leak into later tests. + const withContentCardFixture = async (html: string, assertions: () => Promise) => { + await page.evaluate((markup) => { + const template = document.createElement('template'); + template.innerHTML = markup; + Array.from(template.content.children).forEach((el) => el.setAttribute('data-e2e-fixture', '')); + document.querySelector('.content-card')?.append(template.content); + }, html); + try { + await assertions(); + } finally { + await page.evaluate(() => document.querySelectorAll('[data-e2e-fixture]').forEach((el) => el.remove())); + } + }; + // A hand-written one-row table (no .mdc-data-table__row), like the legacy grids. + const matTable = (cells: string) => + `${cells}
`; test.beforeAll(async ({ browser }) => { page = await browser.newPage(); @@ -66,7 +87,7 @@ test.describe.serial('Theme "eForm new design" (workspace variant)', () => { await deviceUsersPage.newDeviceUserBtn().waitFor({ state: 'visible', timeout: 40000 }); await deviceUsersPage.createNewDeviceUser(firstName, lastName); deviceUserCreated = true; - await expect(page.locator('tbody > tr', { hasText: firstName })).toBeVisible({ timeout: 40000 }); + await expect(deviceUserRow()).toBeVisible({ timeout: 40000 }); await myEformsPage.Navbar.goToSites(); await siteRow().waitFor({ state: 'visible', timeout: 40000 }); }); @@ -87,8 +108,7 @@ test.describe.serial('Theme "eForm new design" (workspace variant)', () => { await deviceUsersPage.newDeviceUserBtn().waitFor({ state: 'visible', timeout: 40000 }); // Let the list load before looking the user up; if its creation never // completed there is no row, and the lookup below finds nothing to delete. - await page - .locator('tbody > tr', { hasText: firstName }) + await deviceUserRow() .waitFor({ state: 'visible', timeout: 40000 }) .catch(() => undefined); const deviceUser = await deviceUsersPage.getDeviceUserByName(firstName); @@ -132,7 +152,33 @@ test.describe.serial('Theme "eForm new design" (workspace variant)', () => { await expect(td).toHaveCSS('padding-left', '16px'); await expect(td).toHaveCSS('vertical-align', 'middle'); + // The frame's bottom edge is the only line under the last row. + const lastRowFirstCell = grid().locator('tbody > tr').last().locator('td').first(); + await expect(lastRowFirstCell).toHaveCSS('border-bottom-style', 'none'); + await expect(grid()).toHaveCSS('border-top-left-radius', '8px'); + // width: 100% + the 1px frame must not overflow the container. + await expect(grid()).toHaveCSS('box-sizing', 'border-box'); + + // The right-pinned actions header keeps the --bg header tint (#f8fafd), + // even against a plugin-style `:host ::ng-deep … th` paint (0,3,1). + const pinnedActionsHeader = grid().locator('thead tr th').last(); + await expect(pinnedActionsHeader).toHaveClass(/\bmat-table-sticky-right\b/); + await expect(pinnedActionsHeader).toHaveCSS('background-color', 'rgb(248, 250, 253)'); + const pluginPaintSelector = '.mtx-grid .mat-mdc-header-row th.mat-table-sticky-right'; + await page.evaluate((selector) => { + const style = document.createElement('style'); + style.id = 'e2e-plugin-pinned-paint'; + style.textContent = `${selector} { background: rgb(255, 0, 0) !important; }`; + document.head.appendChild(style); + }, pluginPaintSelector); + try { + // Positive control: the injected paint really targets this cell. + expect(await pinnedActionsHeader.evaluate((el, s) => el.matches(s), pluginPaintSelector)).toBe(true); + await expect(pinnedActionsHeader).toHaveCSS('background-color', 'rgb(248, 250, 253)'); + } finally { + await page.evaluate(() => document.getElementById('e2e-plugin-pinned-paint')?.remove()); + } }); test('new design lays out the content card and sub-header', async () => { @@ -142,6 +188,51 @@ test.describe.serial('Theme "eForm new design" (workspace variant)', () => { await expect(page.locator('app-sites .eform-sub-header')).toHaveCSS('padding-left', '0px'); }); + 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. + await withContentCardFixture('
', async () => { + await expect(contentCard).toHaveCSS('padding-left', '0px'); + await expect(contentCard).toHaveCSS('padding-top', '0px'); + }); + await expect(contentCard).toHaveCSS('padding-left', '24px'); + }); + + test('new design keeps time-planning grid dividers off the frame edges', async () => { + // Stand-in for the time-planning week grid: day cells carry the status + // classes that styles.scss gives !important right/bottom borders. + const dayCell = 'day'; + await withContentCardFixture( + `
${matTable(dayCell + dayCell)}
`, + async () => { + const cells = page.locator('#e2e-time-dashboard td'); + // Day divider between cells, in the row-separator colour. + await expect(cells.first()).toHaveCSS('border-right-style', 'solid'); + await expect(cells.first()).toHaveCSS('border-right-color', 'rgb(225, 231, 239)'); + // None against the frame's right and bottom edges. + await expect(cells.last()).toHaveCSS('border-right-style', 'none'); + await expect(cells.last()).toHaveCSS('border-bottom-style', 'none'); + } + ); + }); + + test('new design drops mtx dividers after left-pinned columns except on the time-planning grid', async () => { + // Stand-ins for a grid with left-pinned columns (compliance report, + // working hours) and the time-planning week grid. mtx-grid's global + // styles are loaded by the Sites grid on this page. + const pinnedCells = matTable( + 'ab' + ); + await withContentCardFixture( + `
${pinnedCells}
` + + `
${pinnedCells}
`, + async () => { + await expect(page.locator('#e2e-pinned-left td').first()).toHaveCSS('border-right-style', 'none'); + await expect(page.locator('#e2e-pinned-left-tp td').first()).toHaveCSS('border-right-style', 'solid'); + } + ); + }); + test('new design uses a compact row action button and keeps the row lit while its menu is open', async () => { const actionBtn = siteRowActionBtn(); // The row paints the background (cells are transparent); the pinned diff --git a/eform-client/src/scss/components/_workspace-mat-overrides.scss b/eform-client/src/scss/components/_workspace-mat-overrides.scss index 10dbf34005..117a2e730e 100644 --- a/eform-client/src/scss/components/_workspace-mat-overrides.scss +++ b/eform-client/src/scss/components/_workspace-mat-overrides.scss @@ -938,7 +938,7 @@ body.theme-workspace { // - Body: 12px/16px cells, content-height rows, surface-variant separators, // no separator under the last row // - Row hover --ws-table-row-hover; a row stays lit while its menu is open - // - Row-action icon buttons shrink to 32px inside cells + // - Icon buttons inside body cells shrink to 32px // --table-row-hover equals --md-surface-container in dark (#1e1f20), so a // hover on it would be invisible; dark mode gets a 4% on-surface lift. @@ -961,11 +961,14 @@ body.theme-workspace { // _table.scss and styles.scss draw the host frame from these hooks with // !important; re-point the hooks instead of out-shouting those rules. --mtx-grid-border: 1px solid var(--md-surface-variant); - --mtx-grid-radius: 8px; + --mtx-grid-radius: var(--radius-lg, 8px); // mtx-grid's own [rowHover] rule out-specifies ours; feed it our colour. --mtx-grid-table-row-hover-background-color: var(--ws-table-row-hover); display: flex; flex-direction: column; + // mtx sets the host `width: 100%`; content-box would add the frame's + // 2px on top, and an `overflow: hidden` parent clips the right edge. + box-sizing: border-box; background: var(--md-surface-container); overflow: hidden; @@ -976,6 +979,15 @@ body.theme-workspace { } } + // The grid's scroller gets a thin themed scrollbar. Setting scrollbar-color + // makes Chrome 121+ skip the global black-track ::-webkit-scrollbar + // (styles.scss). scrollbar-color inherits (scrollbar-width doesn't), so + // scroll areas inside cells get the themed colours at default width. + .mtx-grid .mat-table-container { + scrollbar-width: thin; + scrollbar-color: var(--state-scrollbar-thumb) transparent; + } + // Content-height rows. Set on the rows rather than via Material's // --mat-table-*-container-height tokens: mtx-grid also does arithmetic // with the row token (expansion-row padding), which `auto` would break. @@ -997,7 +1009,7 @@ body.theme-workspace { // sticky header still sticks to the real scroller. .mat-mdc-table:not(.mtx-grid .mat-mdc-table) { border: 1px solid var(--md-surface-variant); - border-radius: 8px; + border-radius: var(--radius-lg, 8px); overflow: clip; } @@ -1013,6 +1025,13 @@ body.theme-workspace { border-bottom-color: var(--md-surface-variant) !important; } + // Material drops the last row's separator only on `.mdc-data-table__row`; + // hand-written mat-mdc-table markup (property-areas-edit modal) lacks that + // class. The !important rules above set the colour only, so `none` wins. + .mat-mdc-table tbody > tr:last-child > .mat-mdc-cell { + border-bottom: none; + } + // The divider under the header is the header cells' own bottom border, // so it moves with them while sticky. .mat-mdc-header-row { @@ -1078,6 +1097,34 @@ body.theme-workspace { text-transform: none; } + // Time-planning planning grid. Its ViewEncapsulation.None component styles + // box every header cell (`.time-dashboard table tr th`, 0,1,3) and draw + // --tp-border day dividers; styles.scss and mtx-grid's pinned-column rule + // recolour them. Header: tint + the bottom line only. Body: keep the day + // dividers in the row-separator colour, none against the frame's edges. + // !important where styles.scss's unscoped rules use it: the border colour, + // and the day cells' `.white-/.grey-/.green-/.red-background` classes, which + // force border-right and border-bottom. + .mtx-grid.time-dashboard { + .mat-mdc-header-row .mat-mdc-header-cell { + border-top: none; + border-left: none; + border-right: none; + } + + .mat-mdc-cell { + border-right-color: var(--md-surface-variant) !important; + } + + .mat-mdc-cell:last-child { + border-right: none !important; + } + + tbody > tr:last-child > .mat-mdc-cell { + border-bottom: none !important; + } + } + // time-planning break-policy rules: the table now draws its own frame, so // drop the wrapper's to avoid a double border. !important: the component's // emulated-encapsulation rule (an _ngcontent attribute on both parts, @@ -1091,10 +1138,15 @@ body.theme-workspace { // cells. Header cells take the header tint; body cells inherit the row's // background, so hover, menu-open, selected and status-coloured rows all // carry across without per-state rules. !important beats styles.scss's - // unscoped `.mtx-grid .mat-table-sticky-left` background. - th.mat-table-sticky-left, - th.mat-table-sticky-right, - th.mat-mdc-table-sticky-border-elem-right { + // unscoped `.mtx-grid .mat-table-sticky-left` background. The header + // selector is row-qualified (0,4,2) so a plugin's `:host ::ng-deep … th` + // paint (0,3,1) can't un-tint it; the body rule stays low on purpose — + // plugin row states like `tr.highlighted > td` rely on beating it. + .mat-mdc-table .mat-mdc-header-row > th:is( + .mat-table-sticky-left, + .mat-table-sticky-right, + .mat-mdc-table-sticky-border-elem-right + ) { background-color: var(--bg) !important; } @@ -1104,6 +1156,13 @@ body.theme-workspace { background-color: inherit !important; } + // mtx draws a divider after every left-pinned column; the new design has + // no vertical dividers. The time-planning week grid keeps its pale one + // (recoloured in the .time-dashboard block above). + .mtx-grid:not(.time-dashboard) .mat-table-sticky-left { + border-right: none; + } + // Right-pinned actions column: drop the legacy left border + shadow. th.mat-table-sticky-right, td.mat-table-sticky-right, @@ -1113,9 +1172,10 @@ body.theme-workspace { box-shadow: none !important; } - // Row-action icon buttons inside body cells (mat and native tables): - // compact 32px target with a 20px glyph. Icon buttons elsewhere keep the - // 42px default above, and the no-halo tokens carry over from it. + // Every icon button inside a body cell (mat and native tables), not only + // row actions: compact 32px target with a 20px glyph. Icon buttons + // elsewhere keep the 42px default above, and the no-halo tokens carry over + // from it. // .small-icon-btn (shared-tags) sizes itself to 28px; leave it alone. td .mat-mdc-icon-button:not(.small-icon-btn), .mat-mdc-cell .mat-mdc-icon-button:not(.small-icon-btn) { diff --git a/eform-client/src/scss/components/_workspace-patterns.scss b/eform-client/src/scss/components/_workspace-patterns.scss index 4a90075264..bd84954008 100644 --- a/eform-client/src/scss/components/_workspace-patterns.scss +++ b/eform-client/src/scss/components/_workspace-patterns.scss @@ -479,8 +479,9 @@ body.theme-workspace { // from the card edges. border-box keeps the padding inside the card's // `min-height: calc(100% - 2 * margin)` (full-layout.component.scss); // content-box would make short pages 32px taller than the drawer content - // and scroll it. Simple-layout routes (auth, landing, connection-string) - // render no .content-card and are unaffected. + // and scroll it. Every rule here is scoped to the content card, so + // simple-layout routes (auth, landing, connection-setup), which render + // none, keep their own padding. // --------------------------------------------------------------------------- mat-drawer-container .content-card { @@ -514,7 +515,9 @@ body.theme-workspace { // card. The card now supplies the gutter, so drop the horizontal part — // keeping the 16px top/bottom and the mb-3 — and the title lines up with the // table's left edge. !important is needed to beat the utility's own. - .eform-sub-header { + // The simple-layout connection-setup page (div.p-3 > eform-new-subheader) + // has no card gutter, so it keeps the inset. + mat-drawer-container .content-card .eform-sub-header { padding-left: 0 !important; padding-right: 0 !important; } @@ -526,10 +529,10 @@ body.theme-workspace { // Limited to div/mat-card wrappers so a stray px-3 button keeps its padding. // Hits: navigation-menu-page, admin-settings, items-planning // plannings-header, backend-configuration statistics + task-tracker. - eform-new-subheader ~ :is(div, mat-card).px-3, - eform-new-subheader ~ :not(.mat-mdc-card) > :is(div, mat-card).px-3, - .eform-sub-header ~ :is(div, mat-card).px-3, - .eform-sub-header ~ :not(.mat-mdc-card) > :is(div, mat-card).px-3 { + mat-drawer-container .content-card eform-new-subheader ~ :is(div, mat-card).px-3, + mat-drawer-container .content-card eform-new-subheader ~ :not(.mat-mdc-card) > :is(div, mat-card).px-3, + mat-drawer-container .content-card .eform-sub-header ~ :is(div, mat-card).px-3, + mat-drawer-container .content-card .eform-sub-header ~ :not(.mat-mdc-card) > :is(div, mat-card).px-3 { padding-left: 0 !important; padding-right: 0 !important; } @@ -540,16 +543,26 @@ body.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 - .content-card router-outlet + * > .board-list-container, - .content-card router-outlet + * > .sprint-panel, - .content-card router-outlet + * > .board-settings, - .content-card router-outlet + * > .reports-container, - .content-card router-outlet + * > .statistics-container, - .content-card router-outlet + * > .pay-rule-sets-container { + .content-card router-outlet + * > :is( + .board-list-container, + .sprint-panel, + .board-settings, + .reports-container, + .statistics-container, + .pay-rule-sets-container + ) { 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). + .content-card router-outlet + * > div.p-4 { + padding-left: 0 !important; + padding-right: 0 !important; + } + // --------------------------------------------------------------------------- // 4. Filter panel — right-edge mirror of the drawer // --------------------------------------------------------------------------- diff --git a/eform-client/src/scss/components/_workspace-utilities.scss b/eform-client/src/scss/components/_workspace-utilities.scss index 198a239310..304df0a872 100644 --- a/eform-client/src/scss/components/_workspace-utilities.scss +++ b/eform-client/src/scss/components/_workspace-utilities.scss @@ -630,7 +630,7 @@ body.theme-workspace { // Native data tables mirror the mat-table look in // _workspace-mat-overrides.scss (Tables): the DS hooks, Bootstrap // `.table`, hand-written `table.mat-table` markup (legacy classes the - // .mat-mdc-* rules miss) and plugin `table.data`. Row-action icon + // .mat-mdc-* rules miss) and plugin `table.data`. Body-cell icon // button sizing and the open-menu trigger colour live there too. // Deliberately not matched: mat-datepicker's `.mat-calendar-table` and // key/value layout tables such as `.summary-table`. @@ -647,7 +647,7 @@ body.theme-workspace { max-height: 220px; overflow: auto; border: 1px solid var(--md-surface-variant); - border-radius: 8px; + border-radius: var(--radius-lg, 8px); background: var(--md-surface-container); table { margin: 0; @@ -667,7 +667,7 @@ body.theme-workspace { // already draws one. `clip` keeps the sticky header working. &:not(.mtx-grid table):not(.table-scroll table) { border: 1px solid var(--md-surface-variant); - border-radius: 8px; + border-radius: var(--radius-lg, 8px); overflow: clip; }