diff --git a/apps/electron-backend-e2e/src/playlist-switcher.e2e.ts b/apps/electron-backend-e2e/src/playlist-switcher.e2e.ts index 5579a4390..2bcaf00f3 100644 --- a/apps/electron-backend-e2e/src/playlist-switcher.e2e.ts +++ b/apps/electron-backend-e2e/src/playlist-switcher.e2e.ts @@ -530,6 +530,41 @@ test.describe('Electron Playlist Switcher', () => { await expect(panel).toHaveCount(0); await expect(trigger).toBeFocused(); + // In a short window only the list shrinks and scrolls; the + // search field and the type filter keep their height. + const viewport = page.viewportSize(); + await page.setViewportSize({ + width: viewport?.width ?? 1280, + height: 320, + }); + await page.keyboard.press('ControlOrMeta+p'); + await expect(search).toBeFocused(); + const chrome = await panel.evaluate((host) => { + const height = (element: Element | null) => + element?.getBoundingClientRect().height ?? 0; + const list = host.querySelector('.switcher__list'); + return { + search: height(host.querySelector('.switcher__search')), + toggles: Array.from( + host.querySelectorAll('.mat-button-toggle'), + height + ), + listScrolls: + !!list && list.scrollHeight > list.clientHeight, + }; + }); + expect(chrome.search, JSON.stringify(chrome)).toBeCloseTo(36, 0); + expect(chrome.toggles.length, JSON.stringify(chrome)).toBe(3); + for (const toggle of chrome.toggles) { + expect(toggle, JSON.stringify(chrome)).toBeGreaterThanOrEqual( + 30 + ); + } + expect(chrome.listScrolls, JSON.stringify(chrome)).toBe(true); + await page.keyboard.press('Escape'); + await expect(panel).toHaveCount(0); + if (viewport) await page.setViewportSize(viewport); + await openSources(page); await expect( page.locator('app-workspace-sources app-source-row') diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index 5c40c1df3..fe2ed27c8 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -1386,7 +1386,11 @@ in `@iptvnator/playlist/shared/util` (`source-display`, `source-format`, the highlighted row's menu, Escape closes and returns focus to the trigger. The compact row's ⋯ is pointer-only (`aria-hidden`, not focusable), since an option may not contain a control. "Recent" lists the - three most recently opened sources once there are six or more. + three most recently opened sources once there are six or more. The search + field is the shared 36px search field (`--app-search-*`). The popover is a + height-capped column in which only the list shrinks and scrolls: every + other part is `flex: none`. Otherwise a long list (hundreds of sources) + squeezes them in proportion to its size, and the type filter collapses. ### Network source indicators (Electron) diff --git a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-panel.component.scss b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-panel.component.scss index 4f0aa030d..5cb9e1482 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-panel.component.scss +++ b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-panel.component.scss @@ -18,6 +18,19 @@ color: var(--app-heading-color); } +// Only the list shrinks and scrolls. A column flex container shrinks its +// items in proportion to their size, so with a long list (hundreds of +// sources) the fixed parts lost nearly all their height too: the type +// filter, which clips its overflow, collapsed to 2px and the search field +// to its text line. +.switcher__header, +.switcher__search, +.switcher__types, +.switcher__empty, +.switcher__footer { + flex: none; +} + .switcher__header { display: flex; align-items: center; @@ -46,32 +59,36 @@ @include controls.ghost-icon-button(32px, 18px); } -// The search field is one step darker than the popover. Its focus shows on -// the field (token border and halo), not as an outline on the input. +// The same search field as the header and the side panels: 36px, the +// shared search tokens, focus on the field (token border and halo) rather +// than as an outline on the input. .switcher__search { display: flex; align-items: center; - gap: 9px; + gap: 8px; height: 36px; margin: 0 14px 10px; padding: 0 10px; box-sizing: border-box; - border: 1px solid var(--app-line-strong); - border-radius: 9px; - background: var(--app-content-bg); + border: 1px solid var(--app-search-border, var(--app-line-strong)); + border-radius: 10px; + background: var(--app-search-bg, var(--app-content-bg)); color: var(--app-muted-color); + transition: + border-color 0.15s ease, + box-shadow 0.15s ease; &:focus-within { border-color: var(--app-focus-ring); box-shadow: 0 0 0 3px - color-mix(in srgb, var(--app-focus-ring) 18%, transparent); + color-mix(in srgb, var(--app-focus-ring) 12%, transparent); } .mat-icon { flex: none; - width: 15px; - height: 15px; - font-size: 15px; + width: 18px; + height: 18px; + font-size: 18px; } input { @@ -82,7 +99,7 @@ background: none; color: var(--app-heading-color); font: inherit; - font-size: 13px; + font-size: 0.84rem; &::placeholder { color: var(--app-muted-color);