watchparty/client/src/shell/Shell.css
Nicolas afae977b49
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
Make the mobile nav sheet swipe-to-close instead of fighting pull-to-refresh
Reported directly: the only way to close the bottom sheet was tapping
the scrim, and the instinctive swipe-it-down gesture fell straight
through to the browser's own pull-to-refresh instead - nothing was
telling the browser this app's own code owned that gesture.

touch-action: none on the scrim and the (now real, not just visual)
grabber handle stops the browser's default touch handling from
kicking in there at all; overscroll-behavior: contain on the sheet
itself stops a drag that hits its own scroll boundary (the library
grid can run taller than the sheet's 75vh cap) from chaining to the
page underneath. The grabber's small 36x4px pill now sits inside a
much bigger invisible touch target that actually drags the sheet
(pointerdown/move/up, 1:1 tracking, snap back under 90px or close past
it) - scoped to the grabber alone, not the whole sheet, so the
library grid's own taps/scroll keep working untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 17:58:56 +02:00

250 lines
6.1 KiB
CSS

/* ---- App shell: sidebar + routed main content - see Shell.ts. ---- */
.appShell {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 220px;
flex-shrink: 0;
background: var(--bg-header);
padding: 20px 12px;
display: flex;
flex-direction: column;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.sidebarBrand {
font-size: 18px;
font-weight: 700;
padding: 0 12px 20px;
}
.sidebarNav {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
.navSectionLabel {
color: var(--muted);
font-size: 11px;
text-transform: uppercase;
letter-spacing: .05em;
padding: 16px 12px 6px;
}
.navItem {
display: block;
padding: 10px 12px;
border-radius: 8px;
color: var(--text);
text-decoration: none;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.navItem:hover { background: rgba(255, 255, 255, .06); }
.navItem.active { background: var(--accent); color: white; font-weight: 600; }
.sidebarLogout { margin-top: 12px; padding: 8px 12px; }
.mainContent {
flex: 1;
min-width: 0;
padding: 24px clamp(16px, 3vw, 48px) 80px;
}
/* ==== Mobile navigation (<=720px) ====
Replaces the sidebar outright - see docs/architecture.md's "Mobile
navigation" section for why a bottom sheet was chosen over reshaping
the sidebar into a horizontal bar. Every rule below the base
`display: none` only ever applies once the 720px media query turns
it back on, so none of this affects desktop. */
.mobileTopBar,
.mobileNavTrigger,
.mobileNavScrim,
.mobileNavSheet {
display: none;
}
.mobileTopBar {
position: sticky;
top: 0;
z-index: 3;
padding: 14px 16px;
background: var(--bg-header);
}
.mobileBrand { font-size: 16px; font-weight: 700; }
/* position:fixed, not sticky - #musicBar below already relies on a
fixed wrapper to stay docked regardless of how far #mainContent's
own (whole-page, not internally-scrolled) content runs. */
.bottomStack {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 15;
display: flex;
flex-direction: column;
}
.mobileNavTrigger {
align-items: center;
justify-content: center;
gap: 8px;
background: var(--bg-header);
border: none;
border-top: 1px solid rgba(255, 255, 255, .08);
color: var(--text);
font-family: inherit;
font-size: 13px;
font-weight: 600;
padding: 13px;
padding-bottom: calc(13px + env(safe-area-inset-bottom, 0px));
}
.mobileNavScrim {
position: fixed;
inset: 0;
z-index: 20;
background: rgba(0, 0, 0, .5);
opacity: 0;
pointer-events: none;
transition: opacity .2s;
/* A touch starting here has no scrollable content of its own to
"contain" (overscroll-behavior wouldn't apply) - this is what
stops the browser reading a drag over the dimmed backdrop as a
page-level gesture (pull-to-refresh) instead of just dismissing
the sheet via the existing tap-to-close below. */
touch-action: none;
}
.mobileNavScrim.open { opacity: 1; pointer-events: auto; }
.mobileNavSheet {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 21;
max-height: 75vh;
overflow-y: auto;
/* Stops a drag that hits the top of the sheet's OWN scroll (its
library grid can be taller than 75vh) from chaining past this
element to the page underneath and surfacing as pull-to-refresh -
complements touch-action:none on the dedicated grabber handle
below, which instead covers a drag that never scrolls anything. */
overscroll-behavior: contain;
background: var(--bg-header);
border-radius: 16px 16px 0 0;
padding: 8px 16px calc(20px + env(safe-area-inset-bottom, 0px));
transform: translateY(100%);
transition: transform .22s ease;
box-shadow: 0 -8px 24px rgba(0, 0, 0, .3);
}
.mobileNavSheet.open { transform: translateY(0); }
/* Dragging (see Shell.ts) suspends the transition above for 1:1
tracking under the finger, then this class restores it for the
snap-back/close animation - same "instant while dragging, animated
on release" split views/player/Player.css's own seek bar uses. */
.mobileNavSheet.dragging { transition: none; }
/* Bigger drag/touch target than the pill it contains (36x4px is far
below a real touch-target minimum) - see Shell.html/Shell.ts. */
.sheetGrabberHandle {
display: flex;
justify-content: center;
padding: 14px 0 10px;
touch-action: none;
cursor: grab;
}
.sheetGrabber {
width: 36px;
height: 4px;
border-radius: 99px;
background: var(--muted);
}
/* Accueil and the (conditional) Listes de lecture row both use this -
same visual weight, bookending the "Bibliothèques" group between
them, so the one part whose size actually varies (however many
libraries Emby returns) reads as visually distinct from the two
fixed rows around it. */
.sheetPrimary {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
background: var(--panel);
border: none;
border-radius: 10px;
color: var(--text);
font-family: inherit;
font-size: 14px;
font-weight: 600;
padding: 12px;
margin: 4px 0;
text-decoration: none;
}
.sheetPrimary.active { background: var(--accent); color: #fff; }
.sheetGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin: 4px 0 10px;
}
.sheetItem {
display: flex;
align-items: center;
gap: 9px;
background: var(--panel);
border-radius: 10px;
color: var(--text);
font-size: 13px;
padding: 12px 10px;
text-decoration: none;
}
.sheetItem.active { background: var(--accent); color: #fff; font-weight: 600; }
.navIcon { width: 20px; height: 20px; flex-shrink: 0; display: block; }
.navIcon svg { width: 100%; height: 100%; }
.sheetLogout {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
margin-top: 8px;
padding: 12px;
border: 1px solid rgba(255, 255, 255, .15);
border-radius: 10px;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 720px) {
.appShell { flex-direction: column; }
.sidebar { display: none; }
.mobileTopBar { display: flex; align-items: center; }
.mobileNavTrigger { display: flex; }
.mobileNavScrim { display: block; }
.mobileNavSheet { display: block; }
}