From edebe0c0194a6a1a2a2cd874140cd8960edf53a8 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 5 Sep 2026 14:18:44 +0200 Subject: [PATCH] fix(epg): keep loading shimmer visible in both themes --- .../src/epg-timeline-interaction.e2e.ts | 27 +++++++++++ .../src/theme-contrast.ts | 45 +++++++++++++++++++ docs/architecture/iptvnator-ui-guidelines.md | 4 +- .../epg-list-view.component.scss | 6 +-- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/apps/electron-backend-e2e/src/epg-timeline-interaction.e2e.ts b/apps/electron-backend-e2e/src/epg-timeline-interaction.e2e.ts index 0f7cb36ba..e89f5e484 100644 --- a/apps/electron-backend-e2e/src/epg-timeline-interaction.e2e.ts +++ b/apps/electron-backend-e2e/src/epg-timeline-interaction.e2e.ts @@ -24,6 +24,7 @@ import { applyTheme, expectTextContrast, expectThemeSurface, + expectSkeletonContrast, } from './theme-contrast'; const epgCredentials = { @@ -182,6 +183,32 @@ test('@epg @xtream @electron opens the programme dialog from a timeline block an guide.locator('[data-when="now"] .desc').first() ); } + // Keep a fresh channel's EPG IPC pending so the real list loading + // template stays mounted through both theme changes. + await app.electronApp.evaluate(({ ipcMain }) => { + ipcMain.removeHandler('XTREAM_REQUEST'); + ipcMain.handle( + 'XTREAM_REQUEST', + () => + new Promise(() => { + // Released when this isolated Electron test app closes. + }) + ); + }); + await app.mainWindow + .locator('[data-test-id="channel-item"]') + .nth(1) + .click(); + const skeleton = guide.locator('.sk-time').first(); + await expect(skeleton).toBeVisible(); + for (const theme of ['light', 'dark'] as const) { + await applyTheme(app.mainWindow, theme); + await expectSkeletonContrast(skeleton, guide); + await expectSkeletonContrast( + guide.locator('.sk-title').first(), + guide + ); + } } finally { await closeElectronApp(app); } diff --git a/apps/electron-backend-e2e/src/theme-contrast.ts b/apps/electron-backend-e2e/src/theme-contrast.ts index 2c06b0ebd..ab8ac2cf0 100644 --- a/apps/electron-backend-e2e/src/theme-contrast.ts +++ b/apps/electron-backend-e2e/src/theme-contrast.ts @@ -153,3 +153,48 @@ export async function expectOverlayContrastOnWhite( ); } } + +/** Skeletons are decorative, but their faintest gradient stop must remain + * distinguishable from the guide's background in either theme. */ +export async function expectSkeletonContrast( + skeleton: Locator, + guide: Locator +) { + const background = await guide.evaluate((element) => + getComputedStyle(element) + .backgroundColor.match(/[\d.]+/g)! + .slice(0, 3) + .map(Number) + ); + const { data, info } = await sharp( + await skeleton.screenshot({ animations: 'disabled' }) + ) + .removeAlpha() + .raw() + .toBuffer({ resolveWithObject: true }); + const luminance = (rgb: number[]) => + rgb + .map((value) => { + const s = value / 255; + return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; + }) + .reduce( + (sum, value, index) => + sum + value * [0.2126, 0.7152, 0.0722][index], + 0 + ); + const bg = luminance(background); + // Middle row avoids the rounded transparent corners. + for (let x = 8; x < info.width - 8; x++) { + const offset = + (Math.floor(info.height / 2) * info.width + x) * info.channels; + const fg = luminance([ + data[offset], + data[offset + 1], + data[offset + 2], + ]); + expect( + (Math.max(fg, bg) + 0.05) / (Math.min(fg, bg) + 0.05) + ).toBeGreaterThanOrEqual(1.3); + } +} diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index 0a82eedd5..d2db53be1 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -95,7 +95,9 @@ EPG timeline, list, empty states and programme details use the library-local selection and live accents. Text pairs with the actual surface in both themes; current/playing titles must not force white onto a light selection tint. Past programme text remains readable without reducing opacity on the whole -card. Theme changes resolve through CSS on the mounted components immediately. +card. List loading shimmer uses translucent primary text stops so placeholders +remain visible on either theme’s content surface. Theme changes resolve through +CSS on the mounted components immediately. Electron E2E measures app-panel foreground/background contrast (including translucency, ancestor opacity and the timeline’s sibling progress fill), diff --git a/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.scss b/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.scss index a47253d34..a925d4c47 100644 --- a/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.scss +++ b/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.scss @@ -402,9 +402,9 @@ border-radius: 6px; background: linear-gradient( 90deg, - rgba(255, 255, 255, 0.06), - rgba(255, 255, 255, 0.12), - rgba(255, 255, 255, 0.06) + color-mix(in srgb, $text-primary 18%, transparent), + color-mix(in srgb, $text-primary 28%, transparent), + color-mix(in srgb, $text-primary 18%, transparent) ); background-size: 200% 100%; animation: epg-list-shimmer 1.4s linear infinite;