From 8f861a3a1b2786afb701e7e17c83a43f6ab390b2 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:23:20 +0200 Subject: [PATCH] fix(ui): make the workspace usable on phone-sized screens (#1326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ui): make the workspace usable on phone-sized screens The shell was half-adapted below 640px: the rail flipped to a horizontal bar but the link lists inside it kept stacking downwards, so the navigation was drawn outside the bar and over the header (#1100). Three resizable rails — the shell context panel, the live-layout channel sidebar and the M3U channel drawer — kept their persisted desktop width, which left the content around 50px on a 375px screen. They now span the full width and stack above the content. The inline width written by ResizableDirective is why these rules need `!important`. Found while walking the rest of the UI at 375px and 768px: - The detail hero kept poster and details side by side, squeezing the action row below its own labels until "Play" was clipped to its icon. - The settings section list did not scroll and painted over the footer, which also affected short desktop windows. - Hiding the M3U channel list on a phone was one-way: the restore handle was hidden and only Cmd/Ctrl+B could bring it back. - The live header drew the channel count and the paginator on top of each other up to tablet width, because the paginator does not shrink and the meta collapsed to zero width and overflowed its box. - The search scope checkbox was pushed off the right edge. Live TV states a floor for the player instead of a ceiling for the lists, so the video keeps a usable share of the screen under the categories panel and the channel list. Closes #1100 Co-Authored-By: Claude Fable 5 * fix(ui): address review — keep the palette reachable and the video visible Two findings from the Codex review on #1326. Hiding the command-palette trigger on phones removed the only pointer-driven way to open it: the rail renders route links plus Settings and emits nothing, so `commandPaletteRequested` had exactly one source. The button stays and its keyboard-shortcut label is swapped for an icon instead. Doing that exposed a latent flex trap in the same row — an keeps an intrinsic min-width from its `size`, and `min-width: auto` honours it, so the field refused to shrink and pushed the trigger out onto the buttons beside it. The M3U drawer released the shared player floor, which on a short landscape phone (600-640px wide) left the content container at half the shell body. The inline guide inside it is `flex: 0 0 ` and took its full 180px out of a container that no longer had it, so the video could reach zero height. The floor is restored and now yields on short viewports, the video states its own minimum, and the guide is what gives way. Co-Authored-By: Claude Fable 5 * fix(ui): let the channel list keep its height on a landscape phone Follow-up to the review: the player floor added in the previous commit was measured against the viewport, not against what the shell had left. On a 640x360 landscape phone the stacked categories panel already takes 30vh, so claiming another 50vh here drove the channel sidebar to zero height while it was still marked expanded — no way to pick another channel — and pushed the layout past the viewport. The floor now applies only where the screen can afford it (`min-height: 600px`), the sidebar states a floor of its own so it cannot be squeezed out, and the collapsed rule clears that floor so hiding the list still works. Below that height the two panes simply share what is left. Portrait is unchanged: categories 244px, channel list 220px, player 240px on a 375x812 screen. Co-Authored-By: Claude Fable 5 * fix(ui): address review — settings nav on landscape, poster dead space Two more findings from the Codex review. The stacked settings context panel capped itself at 30vh, which on a 360px screen is 108px — less than the panel's own title and footer, so the seven section rows collapsed to nothing behind an overlapping footer. On short screens the caption gives way (the rail already labels the page), the footer sheds its tall-screen padding, and the settings variant gets a slightly larger cap: unlike the live routes there is no player below competing for height, only a scrollable form. The poster kept a 330px minimum from the skeleton fallback at the bottom of the file — sized for the 220px desktop poster — while the stacked phone hero renders it 140px wide with a ~210px aspect-ratio height. Every loaded detail page carried ~120px of empty space between the poster and the title. The override sits after that rule because it wins on source order, not specificity. Co-Authored-By: Claude Fable 5 * fix(ui): let the playlist switcher yield to the search field on narrow phones Codex review of d6133da9: on a 320px header a route that contributes its shortcut button left the search field less than its own chrome needs (~74px of icon, palette trigger, gaps and padding), so the field's contents spilled onto the buttons beside it. The switcher is the one header region whose content can ellipsize, so it is what shrinks — down to an 88px floor — while the field states its chrome as a minimum. The field's basis moves from auto to zero so the input's intrinsic size stops counting as content: with basis auto the field claimed its intrinsic width even when room was ample and squeezed the switcher to ~115px on a 375px screen that could fit all 140. Co-Authored-By: Claude Fable 5 * fix(ui): fit the switcher's own chrome inside its phone floor Follow-up to the Codex note that the trigger's fixed chrome (type icon, refresh, chevron, gaps, padding) exceeds the 88px floor the shell now allows the switcher to shrink to. The flagged scenario itself cannot occur — the Multi-EPG shortcut needs Electron bridge methods the PWA lacks, and Electron enforces a 900px minimum window width so it never sees the phone breakpoint — but the floor should hold on its own terms rather than by accident of which buttons happen to render. Dropping the decorative type icon on phones brings the fixed chrome under the floor, and the name gets the space instead. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- .changes/ui-mobile-layout.md | 10 +++ .../src/app/settings/settings.component.scss | 3 + docs/architecture/iptvnator-ui-guidelines.md | 52 ++++++++++++++- .../video-player/video-player.component.scss | 18 ++++-- .../playlist-switcher.component.scss | 19 ++++++ .../category-content-view.component.scss | 7 +++ .../search-layout.component.scss | 20 ++++++ .../stalker-itv-all-items.component.scss | 30 ++++++++- .../live-stream-layout.component.scss | 34 +++++++++- .../content-hero/content-hero.component.scss | 31 +++++++++ libs/ui/styles/_panel-header.scss | 3 + libs/ui/styles/_portal-layout.scss | 63 +++++++++++++++++++ ...pace-settings-context-panel.component.scss | 28 ++++++++- ...space-shell-context-sidebar.component.scss | 54 ++++++++++++++++ .../workspace-shell-header.component.html | 10 ++- .../workspace-shell-header.component.scss | 63 ++++++++++++++++++- .../workspace-shell-rail-links.component.scss | 31 +++++++++ .../workspace-shell-rail.component.scss | 48 +++++++++++++- .../workspace-shell.component.scss | 13 ++++ 19 files changed, 518 insertions(+), 19 deletions(-) create mode 100644 .changes/ui-mobile-layout.md diff --git a/.changes/ui-mobile-layout.md b/.changes/ui-mobile-layout.md new file mode 100644 index 000000000..a6ae38203 --- /dev/null +++ b/.changes/ui-mobile-layout.md @@ -0,0 +1,10 @@ +--- +type: fix +area: ui +issues: [1100] +--- + +IPTVnator is usable on a phone again. The navigation bar no longer sits off +screen, category and channel lists take the full width instead of squeezing +the content into a sliver, the video keeps a usable share of the screen, and +the M3U channel list can be reopened after hiding it. diff --git a/apps/web/src/app/settings/settings.component.scss b/apps/web/src/app/settings/settings.component.scss index f3b7d22df..a29be1497 100644 --- a/apps/web/src/app/settings/settings.component.scss +++ b/apps/web/src/app/settings/settings.component.scss @@ -751,6 +751,9 @@ ); margin-left: 0; margin-right: 0; + // Stretched to the full layout width, so its own inline padding would be + // added on top and push the settings page into a horizontal scroll. + box-sizing: border-box; z-index: 100; display: flex; justify-content: flex-end; diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index e0082a268..7e0e4b39d 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -253,9 +253,11 @@ remain local when the meaning is explicit. width is preserved so uncollapsing restores the user's previous resized width. Both rails share the same 180 ms width transition so motion stays in lockstep. -- Below 600 px viewport, the M3U layout's mobile bottom-drawer rule overrides - the desktop collapse to `height: 0` instead of `width: 0`, and the floating - restore handle is hidden. +- At the phone breakpoint the M3U layout's bottom-drawer rule overrides the + desktop collapse to `height: 0` instead of `width: 0`. The floating restore + handle stays visible there: the collapse toggle is reachable by touch, so + hiding the handle left a phone with no way to bring the list back short of + `Cmd/Ctrl+B`. ### EPG Card @@ -331,6 +333,50 @@ Settings use the same system but are flatter than content-heavy views. - Neutral rows can use low-opacity dark overlays - Keep strong blue tint reserved for active sections and selected items +## Phone Layout + +`640px` is the phone breakpoint. Use `@media (max-width: 640px)` rather than +inventing a nearby value: several surfaces cooperate at this width, and a +component that picks `599px` leaves a band where the shell has already stacked +but the component has not. + +### Rails become rows or stacks + +- The workspace shell rail turns into a horizontal top bar. Everything inside + it has to opt into the row direction — a nested list that keeps + `flex-direction: column` stacks its links out of the bar and over the header. + The bar scrolls sideways once a portal contributes its sections, and the + settings link is `position: sticky` so it never scrolls out of reach. +- Side rails stack above the content instead of beside it: the shell context + panel, the live-layout channel sidebar, and the M3U channel drawer. + +### Resizable rails need `!important` + +`ResizableDirective` writes the persisted desktop width as an inline style, so +a phone rule must be `width: 100% !important` to win. Hide `.resize-handle` in +the same rule — dragging is meaningless at full width. Since there is no global +`border-box` reset, a full-width rail with its own padding also needs +`box-sizing: border-box` or it overflows the viewport. + +### State the content's floor, not the list's ceiling + +On routes that stack two lists above the player (live TV shows the categories +panel and the channel list), capping both lists still leaves the video a +sliver. Give the player container a `min-height` instead and let the lists +shrink into what is left. + +### What to drop + +Prefer removing a control over shrinking everything around it: + +- Keyboard-only affordances — the `⌘K` badge, the shortcuts button. +- The `mat-paginator` page-size select, which is the widest part of the + control and the least useful one on a phone. The range and arrows stay. +- Counts and subtitles that a neighbouring control already states. + +Never drop the only way back to a hidden surface. A collapse toggle that is +reachable by touch needs its restore affordance to be reachable too. + ## Theme Guidance ### Light Theme diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss index 246226635..5e9c2fff5 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss @@ -109,18 +109,25 @@ } } -@media only screen and (max-width: 599px) { +// Matches the phone breakpoint in portal-layout's live-layout mixin, which +// this component includes; a narrower one here would leave a band of widths +// where only the mixin's generic stacking applied. +@media only screen and (max-width: 640px) { :host { flex-direction: column-reverse; } .sidebar { - width: 100%; + width: 100% !important; min-width: 100%; max-width: 100%; height: 50vh; + // The mixin caps the list's share; here the explicit height is the + // whole point of the bottom-drawer layout, so release the cap. + max-height: none; border-right: none; border-top: 1px solid var(--mat-sys-outline-variant); + border-bottom: none; // Sidebar collapse uses width=0 on desktop; on mobile the rail flips // to a bottom drawer so collapse it via height instead. @@ -131,11 +138,10 @@ } } + // Keeps the mixin's player floor rather than releasing it: `height: 50vh` + // is a preference, not a reservation, so on a short landscape phone the + // drawer could otherwise squeeze the video away entirely. .content-container { height: 50vh; } - - .sidebar-restore { - display: none; - } } diff --git a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss index fdd6f4df9..3ab758990 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss +++ b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss @@ -90,6 +90,25 @@ line-height: 1.3; letter-spacing: 0.02em; color: var(--mat-sys-on-surface-variant); + // A wrapping subtitle pushes the trigger past the 56px header row. + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +// On a phone the header row has no width to spare, and the shell allows this +// trigger to shrink to 88px there. The decorative type icon goes so that the +// trigger's fixed chrome (refresh, chevron, gaps, padding) actually fits that +// floor with the name ellipsizing; the subtitle goes because the name is the +// only part worth the room. +@media (max-width: 640px) { + .trigger-icon-container { + display: none; + } + + .playlist-info .channels-count { + display: none; } } diff --git a/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss b/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss index a55858215..7326e8f65 100644 --- a/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss +++ b/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss @@ -234,6 +234,13 @@ mat-paginator { mat-paginator { justify-self: start; + + // The page-size select wraps onto a line of its own here, costing a + // row of an already short screen for a control nobody reaches for + // while browsing on a phone. The range and the arrows stay. + ::ng-deep .mat-mdc-paginator-page-size { + display: none; + } } } diff --git a/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.scss b/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.scss index 5bf7163c8..61df3c9ad 100644 --- a/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.scss +++ b/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.scss @@ -169,3 +169,23 @@ @include grid.content-grid; padding-bottom: 1rem; } + +// The header packs the title, the type filters and the scope toggle into one +// row. On a phone that row is wider than the screen, and the scope checkbox +// was the part pushed off the right edge, so the row becomes a column. +@media (max-width: 640px) { + .header .header-top { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .header .header-left { + gap: 12px; + } + + .header .header-right { + justify-content: flex-start; + flex-wrap: wrap; + } +} diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-itv-all-items.component.scss b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-itv-all-items.component.scss index 14fc6ebc1..a5025ae2c 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-itv-all-items.component.scss +++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-itv-all-items.component.scss @@ -12,6 +12,11 @@ .category-content-header { @include panel.standard-panel-header($sticky: true); + + // Same reason as the Xtream live header: the paginator does not shrink, + // so it must be able to drop to its own line rather than crowd the meta + // out of existence. + flex-wrap: wrap; } .category-meta { @@ -49,7 +54,9 @@ // painting its own (mismatched) Material surface color. mat-paginator { background: transparent !important; - flex-shrink: 0; + // Its own container already wraps internally, so letting it shrink turns + // a clipped next-page arrow into a second line of the paginator itself. + min-width: 0; --mat-paginator-container-size: 40px; --mat-paginator-enabled-icon-color: var( --app-body-color, @@ -70,3 +77,24 @@ app-grid-list.all-items-grid { scrollbar-color: color-mix(in srgb, var(--app-muted-color) 55%, transparent) transparent; } + +// Mirrors the Xtream live header: on a phone the page-size select is the +// widest part of the paginator and the least useful one, and the count it +// costs is already stated by the range beside it. +@media (max-width: 640px) { + .category-content-header { + padding-inline: 12px; + } + + .category-subtitle { + display: none; + } + + mat-paginator { + min-width: 0; + + ::ng-deep .mat-mdc-paginator-page-size { + display: none; + } + } +} diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss index 6bbc80b9f..7feced7ec 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss @@ -43,6 +43,11 @@ .category-content-header { @include panel.standard-panel-header($sticky: true); + + // The paginator does not shrink, so on any window narrow enough that it + // and the category meta cannot share a line it took the whole row and the + // meta collapsed underneath it. Let it drop to a line of its own instead. + flex-wrap: wrap; } .category-meta { @@ -59,7 +64,9 @@ mat-paginator { background: transparent !important; - flex-shrink: 0; + // Its own container already wraps internally, so letting it shrink turns + // a clipped next-page arrow into a second line of the paginator itself. + min-width: 0; --mat-paginator-container-size: 40px; --mat-paginator-enabled-icon-color: var( --app-body-color, @@ -116,3 +123,28 @@ app-grid-list.live-all-items-grid { line-height: 1.45; opacity: 0.74; } + +// The header is one flex row of "N channels" plus the paginator. The +// paginator's intrinsic width alone exceeds a phone, which crushed the count +// to zero width and left the two drawn on top of each other. +@media (max-width: 640px) { + .category-content-header { + padding-inline: 12px; + } + + // The paginator already states "1 – 25 of 320" right next to it, and the + // room it costs here is taken straight out of the category title. + .category-subtitle { + display: none; + } + + mat-paginator { + min-width: 0; + + // Dropped for the same reason as the catalog grid: it is the widest + // part of the control and the least useful one on a phone. + ::ng-deep .mat-mdc-paginator-page-size { + display: none; + } + } +} diff --git a/libs/ui/components/src/lib/content-hero/content-hero.component.scss b/libs/ui/components/src/lib/content-hero/content-hero.component.scss index 73ca7f5c8..1126fe6b3 100644 --- a/libs/ui/components/src/lib/content-hero/content-hero.component.scss +++ b/libs/ui/components/src/lib/content-hero/content-hero.component.scss @@ -542,6 +542,26 @@ } } +// A phone cannot carry the poster and the details side by side: the details +// column drops to ~200px, and the action buttons are then squeezed below +// their own labels ("Play" clipped down to its icon). Stack them instead. +@media (max-width: 640px) { + .hero__content { + flex-direction: column; + align-items: stretch; + padding-inline: 16px; + } + + .poster { + width: 140px; + align-self: center; + } + + .details__title { + text-align: center; + } +} + // ============================================================================ // Animations // ============================================================================ @@ -587,3 +607,14 @@ ngx-skeleton-loader { min-height: 330px; // Fallback aspect-ratio: 2 / 3; } + +// After the rule above so it wins the cascade at equal specificity: that +// 330px skeleton fallback is sized for the desktop poster width, while the +// stacked phone hero renders the poster 140px wide — its aspect-ratio height +// is ~210px, and the leftover minimum was pure empty space pushing the title +// and actions ~120px down on every loaded detail page. +@media (max-width: 640px) { + .poster { + min-height: 0; + } +} diff --git a/libs/ui/styles/_panel-header.scss b/libs/ui/styles/_panel-header.scss index 0738bf244..0d1325636 100644 --- a/libs/ui/styles/_panel-header.scss +++ b/libs/ui/styles/_panel-header.scss @@ -29,6 +29,9 @@ align-items: baseline; gap: $gap; flex: 1; + // `flex: 1` lets this shrink to zero width, and the nowrap subtitle then + // painted outside the box and on top of whatever sits beside it. + overflow: hidden; } @mixin standard-panel-title($font-size: 0.95rem) { diff --git a/libs/ui/styles/_portal-layout.scss b/libs/ui/styles/_portal-layout.scss index 5075f3333..153a6ced8 100644 --- a/libs/ui/styles/_portal-layout.scss +++ b/libs/ui/styles/_portal-layout.scss @@ -178,4 +178,67 @@ width: 100% !important; } } + + // ─── Phone layout ──────────────────────────────────────────────────────── + // Side by side, the 400px channel list leaves nothing for the player on a + // phone, so the two stack instead. The width needs `!important` because + // ResizableDirective writes the persisted desktop width inline; dragging + // is meaningless at full width, so the handle goes too. + @media (max-width: 640px) { + :host { + flex-direction: column; + } + + .sidebar { + width: 100% !important; + min-width: 0 !important; + max-width: none; + flex-shrink: 1; + max-height: 42vh; + // Without a floor of its own the player's minimum can take the + // whole body on a short screen and the list vanishes while still + // marked expanded, leaving no way to pick another channel. + min-height: 72px; + border-right: none; + border-bottom: 1px solid var(--app-separator); + + &.sidebar-collapsed { + max-height: 0; + min-height: 0; + border-bottom-color: transparent; + } + + .resize-handle { + display: none; + } + } + + .content-container { + flex: 1 1 auto; + } + + // Inside that container the guide is `flex: 0 0 ` while the + // video is `flex: 1 1 0`, so shrinking is distributed by basis and + // the video — basis zero — absorbed none of it and collapsed. Here + // the video keeps a floor and the guide is what gives way. + .video-player { + min-height: 96px; + } + + .epg.epg--inline { + flex-shrink: 1; + min-height: 0; + } + } + + // The player's floor, not the list's ceiling, is what has to hold — but + // only where the screen can afford it. A landscape phone already spends + // 30vh on the stacked categories panel, so claiming 240px more here left + // nothing for the channel list; there, the two panes just share what is + // left. Keyed on height because that, not width, is what runs out. + @media (max-width: 640px) and (min-height: 600px) { + .content-container { + min-height: 240px; + } + } } diff --git a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-settings-context-panel.component.scss b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-settings-context-panel.component.scss index fb7b41080..c439cb7e2 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-settings-context-panel.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-settings-context-panel.component.scss @@ -26,10 +26,17 @@ flex: 1; min-height: 0; padding-top: 16px; + display: flex; + flex-direction: column; + overflow: hidden; } +// `height: 100%` here does not resolve against a stretched flex item, so the +// list kept its full content height and painted over the footer whenever the +// panel was shorter than its sections (a phone, or a short desktop window). .settings-sections-list { - height: 100%; + flex: 1; + min-height: 0; } .settings-panel-footer { @@ -42,3 +49,22 @@ .settings-back-button:hover:not(.active):not(.selected) { background: var(--app-selection-surface); } + +// On a landscape phone the stacked panel gets ~108-148px, and this panel's +// fixed chrome (title + footer) alone is about that tall — the section list +// was collapsing to nothing behind an overlapping footer. The rail already +// labels the page, so the caption is what gives way, and the footer sheds +// the padding it carries for taller screens. +@media (max-width: 640px) and (max-height: 599px) { + .panel-title { + display: none; + } + + .settings-panel-body { + padding-top: 8px; + } + + .settings-panel-footer { + padding: 6px 8px calc(6px + env(safe-area-inset-bottom, 0px)); + } +} diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component.scss b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component.scss index d71915cd3..a5e05bd9f 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component.scss +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component.scss @@ -57,3 +57,57 @@ min-height: 0; overflow: hidden; } + +.context-panel--settings app-workspace-settings-context-panel { + display: flex; + flex: 1; + min-height: 0; + overflow: hidden; +} + +// Below this width the shell stacks the panel above the content instead of +// beside it. `!important` is required because ResizableDirective writes the +// persisted desktop width as an inline style; a 300px panel on a 375px screen +// leaves ~50px for the actual content. Dragging is meaningless when the panel +// spans the full width, so the handle goes away with it. +@media (max-width: 640px) { + .context-panel { + width: 100% !important; + max-width: none !important; + // There is no global border-box reset, so the panel's own padding + // would push a 100%-wide panel past the viewport. + box-sizing: border-box; + height: auto; + // Roughly a third of the screen: enough to show several entries + // without pushing the route's own content off the bottom. + max-height: 30vh; + border-right: none; + border-bottom: 1px solid var(--mat-sys-outline-variant); + } + + .context-panel--collapsed { + max-height: 0; + padding-top: 0; + padding-bottom: 0; + border-bottom-color: transparent; + } + + .context-panel :is(.resize-handle) { + display: none; + } + + .context-panel--route, + .context-panel--sources { + overflow: hidden; + } +} + +// The settings route has no player competing for height — its content is one +// scrollable form — so on a short landscape phone the panel can take a larger +// share. 30vh of a 360px screen is 108px, which the panel's own footer and +// padding consume before a single section row renders. +@media (max-width: 640px) and (max-height: 599px) { + .context-panel--settings { + max-height: max(30vh, 148px); + } +} diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html index 082312550..153e50838 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html @@ -55,7 +55,14 @@ 'WORKSPACE.SHELL.OPEN_COMMAND_PALETTE' | translate " > - {{ commandShortcutLabel }} + + {{ + commandShortcutLabel + }} + bolt } @@ -68,6 +75,7 @@