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>
250 lines
6.1 KiB
CSS
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; }
|
|
}
|