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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-01 16:17:24 +02:00
1 parent 814161274b
commit 52d47a5a39
19 files changed
+406 -19

No files matched your search

@@ -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 {
@@ -57,3 +57,47 @@
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;
}
}
@@ -68,6 +68,7 @@
<div class="header-actions header-actions--leading">
<button
type="button"
class="shortcuts-trigger"
mat-icon-button
(click)="onShortcutsRequested()"
[matTooltip]="'WORKSPACE.SHORTCUTS.OPEN_TOOLTIP' | translate"
@@ -291,14 +291,40 @@
}
@media (max-width: 640px) {
.workspace-header {
padding-inline: 8px;
gap: 8px;
}
.playlist-switcher {
width: auto;
min-width: 140px;
flex: 1;
width: 140px;
min-width: 0;
flex: 0 0 140px;
}
.search-field {
.search-chip--status {
// Without a floor of its own the three header regions add up to more
// than a phone-width header and the icons overlap.
min-width: 0;
flex: 1 1 auto;
padding-inline: 8px;
.search-chip--status,
.search-chip--scope {
display: none;
}
// Keyboard-only affordances on a screen that usually has no keyboard.
// The palette still opens via the actual shortcut and the rail.
.command-trigger {
display: none;
}
}
.header-actions--leading {
margin-left: 0;
.shortcuts-trigger {
display: none;
}
}
@@ -97,3 +97,34 @@
background: var(--rail-link-active-icon, var(--app-selection-color));
box-shadow: 0 0 10px var(--rail-link-active-glow, var(--app-selection-glow));
}
// The rail turns into a horizontal top bar below this width, so the links
// have to follow. Without the row direction the host keeps stacking its
// links downwards out of the 52px bar and they overlap the header.
@media (max-width: 640px) {
:host {
width: auto;
}
.rail-links {
flex-direction: row;
width: auto;
}
.portal-rail-link {
width: 40px;
height: 40px;
flex: 0 0 auto;
}
.portal-rail-link.is-active::after,
.portal-rail-link.active::after {
right: auto;
top: auto;
bottom: -3px;
left: 50%;
transform: translateX(-50%);
width: 18px;
height: 3px;
}
}
@@ -227,13 +227,39 @@
@media (max-width: 640px) {
.app-rail {
flex-direction: row;
justify-content: space-between;
padding: 6px 10px;
justify-content: flex-start;
gap: 4px;
padding: 6px 8px;
border-right: none;
border-bottom: 1px solid var(--mat-sys-outline-variant);
// A phone cannot fit every portal section at once, so the bar
// scrolls sideways instead of squeezing the icons into overlap.
overflow-x: auto;
overflow-y: hidden;
&.is-macos {
padding-top: 6px;
}
// The brand links to the dashboard, which the first workspace link
// already does. On a bar this narrow that duplicate costs a section.
.brand {
margin-bottom: 0;
display: none;
}
a {
flex: 0 0 auto;
}
a.is-active::after,
a.active::after {
right: auto;
top: auto;
bottom: -2px;
left: 50%;
transform: translateX(-50%);
width: 18px;
height: 3px;
}
}
@@ -245,6 +271,10 @@
.rail-context-region {
width: auto;
// The vertical rail caps this at 48px; in a row that cap would
// stack every portal section on top of itself.
max-width: none;
flex: 0 0 auto;
flex-direction: row;
margin: 0;
padding: 2px 4px;
@@ -261,7 +291,19 @@
}
}
// Settings sits at the end of a bar that scrolls sideways once a portal
// adds its sections, so it is pinned instead of scrolling out of reach.
.rail-footer {
margin-top: 0;
margin-left: auto;
flex: 0 0 auto;
position: sticky;
right: 0;
padding-left: 4px;
background: linear-gradient(
to right,
transparent,
var(--app-rail-bg, var(--mat-sys-surface-container-low)) 25%
);
}
}
@@ -92,6 +92,19 @@ app-workspace-shell-context-sidebar {
.workspace-body {
grid-column: 1;
grid-row: 3;
// Side by side there is no room for both: a 300px context panel on a
// 375px screen leaves the content unusable, so they stack instead.
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr);
&.single-pane {
grid-template-rows: minmax(0, 1fr);
}
}
.workspace-content {
border-radius: 0;
box-shadow: none;
}
.workspace-now-playing {