From 07d3f48bc29e7527f69e37c22cf4c5064d47ee7d Mon Sep 17 00:00:00 2001 From: 4gray Date: Wed, 30 Sep 2026 20:06:43 +0200 Subject: [PATCH] feat(playback): give the fullscreen side panel the settings panel's glass look The fullscreen channel/episode panel was a full-height graphite strip while the controls' settings panel is an inset, rounded glass card. The side panel now wears the same surface: 16px inset (8px under 560px), 20px corners, the controls' glass fill, hairline border, blur and shadow, the same open motion and a 32px square close button. The edge hint uses the same glass, and the episode list marks the playing row with the settings panel's selected cyan. Co-Authored-By: Claude Opus 5.5 --- .changes/playback-side-panel-glass.md | 8 ++ docs/architecture/player-controls-contract.md | 11 +- .../fullscreen-channel-panel.component.scss | 101 +++++++++++------- .../fullscreen-episode-panel.component.scss | 13 ++- 4 files changed, 92 insertions(+), 41 deletions(-) create mode 100644 .changes/playback-side-panel-glass.md diff --git a/.changes/playback-side-panel-glass.md b/.changes/playback-side-panel-glass.md new file mode 100644 index 000000000..5917d85c1 --- /dev/null +++ b/.changes/playback-side-panel-glass.md @@ -0,0 +1,8 @@ +--- +type: feature +area: playback +--- + +The fullscreen channel and episode list now floats over the video as a rounded +glass card, matching the player's settings panel, and the playing episode is +highlighted the same way as a selected setting. diff --git a/docs/architecture/player-controls-contract.md b/docs/architecture/player-controls-contract.md index 353555151..7686eee6c 100644 --- a/docs/architecture/player-controls-contract.md +++ b/docs/architecture/player-controls-contract.md @@ -767,9 +767,16 @@ ends. The panel carries the `dark-theme` context class, so app and Material tokens inside it resolve to the dark palette whatever the app theme is — and because the app's global `.dark-theme { background: … !important }` rule (`apps/web/src/styles.scss`) claims the background of every element wearing -that class, the panel's translucent gradient is declared on the compound +that class, the panel's glass fill is declared on the compound `.fullscreen-channel-panel.dark-theme` selector with `!important`; without -that the panel painted no background at all. +that the panel painted no background at all. The panel wears the same surface +as the controls' settings panel — a card inset 16px from the player's top, +bottom and left edges (8px in a player narrower than 560px), 20px corners, +the `--pc-glass-bg` fill, hairline `--pc-glass-border`, the same blur and +shadow, and a 32px square close button — with the values written literally, +because the panel is a sibling of the controls host, outside the scope that +declares the `--pc-*` palette. Its episode list marks the playing row with the +settings panel's selected-option cyan tint. Keyboard: `C` is ignored while any editable element has focus and while the player sits inside an `inert` region; the search field is an ordinary input, so the controls' Space/K/F/M shortcuts stay out of it. That is also why `C` is diff --git a/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.scss b/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.scss index ee9e52de2..f97d532cf 100644 --- a/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.scss +++ b/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.scss @@ -34,12 +34,12 @@ left: 0; width: 22px; height: 96px; - border: 1px solid rgba(255, 255, 255, 0.16); + border: 1px solid rgba(255, 255, 255, 0.09); border-left: 0; border-radius: 0 12px 12px 0; - background: rgba(13, 15, 18, 0.72); + background: rgba(12, 16, 23, 0.72); box-shadow: 4px 0 18px rgba(0, 0, 0, 0.35); - backdrop-filter: blur(12px); + backdrop-filter: blur(18px) saturate(1.3); opacity: 0; transform: translate(-100%, -50%); pointer-events: none; @@ -68,8 +68,8 @@ } .fullscreen-channel-panel__edge-hint--armed { - border-color: var(--app-selection-color, #78adff); - background: rgba(13, 15, 18, 0.9); + border-color: rgba(255, 255, 255, 0.28); + background: rgba(12, 16, 23, 0.88); } .fullscreen-channel-panel__edge-hint--armed::after { @@ -78,6 +78,12 @@ // ─── Open state ────────────────────────────────────────────────────────────── +// A floating glass card inset from the player edges — the same surface as the +// player's settings panel (`player-settings-panel.component.scss`): 20px +// corners, the controls' glass fill, hairline border and blur. The values are +// literal because the panel sits beside the controls host, outside the scope +// that declares the `--pc-*` palette. + .fullscreen-channel-panel__scrim { position: absolute; inset: 0; @@ -86,43 +92,46 @@ .fullscreen-channel-panel { position: absolute; - top: 0; - bottom: 0; - left: 0; + top: 16px; + bottom: 16px; + left: 16px; z-index: 8; display: flex; flex-direction: column; - width: min(400px, calc(100% - 56px)); - color: var(--app-heading-color, #d8dce8); - border-right: 1px solid rgba(255, 255, 255, 0.12); - box-shadow: 18px 0 48px rgba(0, 0, 0, 0.55); - backdrop-filter: blur(24px) saturate(140%); - transform: translateX(-100%); + box-sizing: border-box; + width: min(400px, calc(100% - 72px)); + color: #e7ecf3; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 20px; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45); + backdrop-filter: blur(18px) saturate(1.3); + overflow: hidden; + opacity: 0; + // Past its own inset, so no rounded corner or shadow peeks in while shut. + transform: translateX(calc(-100% - 24px)); visibility: hidden; transition: - transform 240ms cubic-bezier(0.2, 0.7, 0.2, 1), - visibility 0s linear 240ms; + transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), + opacity 220ms ease-out, + visibility 0s linear 220ms; } -// Translucent graphite over the video, matching the controls' popovers. The -// panel carries `dark-theme` for its tokens, and the app's global +// The controls' glass fill over the video. The panel carries `dark-theme` for its tokens, and the app's global // `.dark-theme { background: var(--mat-sys-surface) !important }` // (apps/web/src/styles.scss) would replace this gradient with the flat // surface — or with no background at all where that token is not defined — // so the compound selector and `!important` are required to out-rank it. .fullscreen-channel-panel.dark-theme { - background: linear-gradient( - to right, - rgba(13, 15, 18, 0.94), - rgba(13, 15, 18, 0.88) - ) !important; + background: rgba(12, 16, 23, 0.72) !important; } .fullscreen-channel-panel--open { + opacity: 1; transform: translateX(0); visibility: visible; transition: - transform 240ms cubic-bezier(0.2, 0.7, 0.2, 1), + transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), + opacity 220ms ease-out, visibility 0s; } @@ -131,9 +140,9 @@ .fullscreen-channel-panel__header { display: flex; align-items: center; - gap: 4px; + gap: 8px; flex-shrink: 0; - padding: 10px 6px 10px 12px; + padding: 14px 12px 12px 16px; } // A host without a search field (`panelSearchEnabled` false) shows its title in @@ -144,9 +153,9 @@ margin: 0; padding: 0 4px; line-height: 36px; - font-size: 0.95rem; + font-size: 16px; font-weight: 600; - color: #ffffff; + color: #e7ecf3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -166,9 +175,9 @@ min-width: 0; height: 36px; padding: 0 6px 0 10px; - border: 1px solid rgba(255, 255, 255, 0.1); + border: 1px solid transparent; border-radius: 10px; - background: rgba(255, 255, 255, 0.07); + background: rgba(255, 255, 255, 0.06); cursor: text; transition: border-color 150ms ease-out, @@ -176,7 +185,7 @@ } .fullscreen-channel-panel__search:focus-within { - border-color: var(--app-selection-color, #78adff); + border-color: rgba(255, 255, 255, 0.28); background: rgba(255, 255, 255, 0.1); } @@ -217,7 +226,7 @@ height: 26px; padding: 0; border: 0; - border-radius: 50%; + border-radius: 8px; color: rgba(255, 255, 255, 0.75); background: transparent; cursor: pointer; @@ -226,7 +235,7 @@ .fullscreen-channel-panel__search-clear:hover, .fullscreen-channel-panel__search-clear:focus-visible { color: #ffffff; - background: rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.1); } .fullscreen-channel-panel__search-clear mat-icon { @@ -235,15 +244,31 @@ font-size: 18px; } +// The settings panel's close button: a 32px, 10px-cornered square with a flat +// hover fill instead of Material's round state layer. .fullscreen-channel-panel__close { flex-shrink: 0; - color: rgba(255, 255, 255, 0.8); + width: 32px; + height: 32px; + padding: 4px; + border-radius: 10px; + color: #e7ecf3; --mdc-icon-button-icon-color: currentColor; --mat-icon-button-icon-color: currentColor; + --mat-icon-button-state-layer-size: 32px; + --mat-icon-button-container-shape: 10px; + --mat-icon-button-icon-size: 20px; + --mat-icon-button-hover-state-layer-opacity: 0; +} + +.fullscreen-channel-panel__close mat-icon { + width: 20px; + height: 20px; + font-size: 20px; } .fullscreen-channel-panel__close:hover { - color: #ffffff; + background: rgba(255, 255, 255, 0.1); } // ─── Body ──────────────────────────────────────────────────────────────────── @@ -278,7 +303,11 @@ @container (max-width: 560px) { .fullscreen-channel-panel { - width: calc(100% - 44px); + top: 8px; + bottom: 8px; + left: 8px; + width: calc(100% - 52px); + border-radius: 16px; } } diff --git a/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss b/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss index dc1e5e8f1..d6fc0c771 100644 --- a/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss +++ b/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss @@ -121,6 +121,11 @@ background: rgba(255, 255, 255, 0.1); } + &:focus-visible { + outline: 2px solid #e7ecf3; + outline-offset: -2px; + } + mat-icon { width: 16px; height: 16px; @@ -152,13 +157,15 @@ } &:focus-visible { - outline: 2px solid var(--app-selection-color, #78adff); + outline: 2px solid #e7ecf3; outline-offset: -2px; } } +// The playing row wears the settings panel's selected-option tint: cyan means +// "this is on" across the player chrome. .episode-panel__item--playing { - background: rgba(255, 255, 255, 0.14); + background: rgba(92, 214, 255, 0.14); cursor: default; } @@ -266,7 +273,7 @@ font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; - color: #7dd87d; + color: #5cd6ff; } .episode-panel__title {