trackProgress is musicBarPanel own first child inside a 3-column grid (track/transport/side), with no grid-column of its own - auto-placement dropped it into column 1 of row 1 like any other item, squeezing it to that columns width and wrapping musicBarPanelSide onto a second row underneath instead of spanning the full bar with everything else in one row below, as its own comment already said it should. Fixed with grid-column: 1 / -1, which pushes every sibling after it into row 2 across the full width with no changes needed to any of them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
440 lines
14 KiB
CSS
440 lines
14 KiB
CSS
/* Persistent bottom overlay for a music listen party - see MusicBar.ts's
|
|
header comment for why this is a Shell-level component instead of a
|
|
route (views/player/Player.css's .playerWrap is the video equivalent,
|
|
and stays a full route takeover on purpose - a shared audio queue has
|
|
no image to "theater mode" around, and the whole point here is staying
|
|
able to browse/add tracks while it plays). Sits above .mainContent, at
|
|
a lower z-index than .playerWrap (20) so a video party's takeover still
|
|
correctly covers it - moot in practice since joining a video party
|
|
calls leaveMusicParty() first (see Player.ts), but cheap to get right.
|
|
The fixed-to-viewport positioning itself lives one level up, on
|
|
Shell.css's .bottomStack wrapper - not here - so that on a narrow
|
|
screen the mobile nav trigger right below it in that wrapper always
|
|
sits flush above this bar's own actual rendered height (its content
|
|
can wrap taller on a narrow screen), never a hardcoded guess at it. */
|
|
.musicBar {
|
|
position: relative; /* containing block for .musicBarQueue's own position: absolute - see its comment */
|
|
background: var(--bg-header);
|
|
border-top: 1px solid rgba(255, 255, 255, .08);
|
|
box-shadow: 0 -4px 16px rgba(0, 0, 0, .3);
|
|
}
|
|
|
|
/* A thin strip along the bar's own top edge, not inside .musicBarRow -
|
|
the same "compact player" convention Spotify/YouTube Music use for
|
|
their mini-players. Click/drag to seek (see MusicBar.ts) - the
|
|
generous ::before hit-area below (not just the 3px visual track)
|
|
matters most for drag on a touch screen, same trick
|
|
views/player/Player.css's own .progressBar::before uses.
|
|
`position: relative` is what gives that absolutely-positioned
|
|
::before something to size itself against.
|
|
`grid-column: 1 / -1` is the fix, not decoration: #trackProgress is
|
|
.musicBarPanel's first child, and without an explicit span a 3-column
|
|
grid places it in column 1 alone - squeezing it down to the width of
|
|
.musicBarPanelTrack's own column and wrapping .musicBarPanelSide onto
|
|
a second row underneath it, instead of the intended full-width strip
|
|
with everything else in one row below. Reported directly - never
|
|
looked obviously wrong at a glance since .musicBarPanelTrack's own
|
|
content (a small image + short text) left column 1 wide enough to
|
|
mostly hide the squeeze. Spanning this row is what pushes every
|
|
sibling after it into row 2, across all 3 columns, with no change
|
|
needed to them. */
|
|
.musicBarProgressTrack {
|
|
grid-column: 1 / -1;
|
|
position: relative;
|
|
height: 3px;
|
|
background: rgba(255, 255, 255, .12);
|
|
cursor: pointer;
|
|
touch-action: none; /* this app's own pointer handlers drive the drag, not the browser's default touch scroll/zoom gestures */
|
|
}
|
|
|
|
.musicBarProgressTrack::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -8px;
|
|
bottom: -8px;
|
|
left: 0;
|
|
right: 0;
|
|
}
|
|
|
|
.musicBarProgressFill {
|
|
height: 100%;
|
|
width: 0%;
|
|
background: var(--accent);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ---- Collapsed mobile preview (see MusicBar.html) - display:none by
|
|
default since #musicBarPanel below is already always visible on
|
|
desktop, nothing to collapse into a preview of there. Turned on
|
|
under the 720px breakpoint further down. ---- */
|
|
.mobileMiniBar {
|
|
display: none;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px 12px;
|
|
position: relative;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Passive - not draggable, unlike #trackProgress inside the panel below.
|
|
Glanceable progress while collapsed is enough; dragging is one tap
|
|
away once expanded, same as most real players' own mini-bar. */
|
|
.mobileMiniProgress {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: rgba(255, 255, 255, .12);
|
|
}
|
|
|
|
.mobileMiniProgressFill {
|
|
height: 100%;
|
|
width: 0%;
|
|
background: var(--accent-bright);
|
|
}
|
|
|
|
.mobileMiniArt {
|
|
width: 36px;
|
|
height: 36px;
|
|
object-fit: cover;
|
|
border-radius: 6px;
|
|
background: var(--panel);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.mobileMiniArt.noImage { visibility: hidden; }
|
|
|
|
.mobileMiniMeta {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
|
|
.mobileMiniMeta strong {
|
|
font-size: 13px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ---- The actual controls - always in place on desktop (this IS the
|
|
bar, same as before); a full-screen "now playing" overlay on mobile,
|
|
opened via #mobileMiniBar above and closed via #panelCloseBtn - see
|
|
the 720px breakpoint further down for the fixed-overlay/transform
|
|
part of this, unchanged here. ---- */
|
|
.musicBarPanel {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto 1fr;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 8px 16px;
|
|
}
|
|
|
|
.panelCloseBtn { display: none; }
|
|
|
|
.musicBarPanelTrack {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.musicBarPanelTransport {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.musicBarPanelSide {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
}
|
|
|
|
/* Smaller than the video player's presence row (Player.css/presence.css's
|
|
36px default) - this bar is a thin strip, not an overlay on top of a
|
|
large video frame. */
|
|
.musicBarPresence .avatar {
|
|
width: 26px;
|
|
height: 26px;
|
|
font-size: 10px;
|
|
}
|
|
|
|
.musicBarImage {
|
|
width: 40px;
|
|
height: 40px;
|
|
object-fit: cover;
|
|
border-radius: 6px;
|
|
background: var(--panel);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.musicBarImage.noImage { visibility: hidden; }
|
|
|
|
.musicBarInfo {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
min-width: 0;
|
|
margin-right: auto;
|
|
}
|
|
|
|
.musicBarInfo strong {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.musicBarPosition { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
|
|
/* The one primary transport action, bigger and filled so it clearly
|
|
outranks the plain small .iconBtn (skip-vote/queue) either side of
|
|
it - same "obviously the main button" treatment
|
|
views/player/Player.ts's own controls already get, instead of
|
|
blending into a row of equally-weighted buttons. Shared between
|
|
#playPauseBtn (inside .musicBarPanel) and #miniPlayPauseBtn (the
|
|
collapsed mobile preview) - same look, same size, both wired to the
|
|
exact same toggle in MusicBar.ts. */
|
|
/* .iconBtn.musicBarPlayBig, not just .musicBarPlayBig: both classes are
|
|
always present together on these two buttons (see MusicBar.html), so
|
|
this compound selector's higher specificity is what reliably beats
|
|
.iconBtn's own background/border-radius regardless of which of the
|
|
two files esbuild happens to bundle later - two plain single-class
|
|
selectors at equal specificity would otherwise depend on that bundle
|
|
order, which isn't something to build a visual guarantee on. */
|
|
.iconBtn.musicBarPlayBig {
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: 50%;
|
|
background: var(--text);
|
|
color: var(--bg);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.iconBtn.musicBarPlayBig:hover { background: var(--accent-bright); color: #fff; }
|
|
.musicBarPlayBig svg { width: 14px; height: 14px; display: block; }
|
|
|
|
/* #voteSkipBtn's own vote tally (see MusicBar.ts's updateVoteSkipLabel) -
|
|
a badge instead of the old "Passer (vote) - X/Y" text label, now that
|
|
the button itself is icon-only like every other transport control
|
|
here. Hidden (see MusicBar.html) until there's an actual tally to
|
|
show - matches every other conditionally-shown badge/pill elsewhere
|
|
in this app rather than showing "0/1" before anyone's voted. */
|
|
#voteSkipBtn { position: relative; }
|
|
|
|
.voteBadge {
|
|
position: absolute;
|
|
top: -4px;
|
|
right: -6px;
|
|
background: var(--accent);
|
|
color: #fff;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
border-radius: 99px;
|
|
padding: 1px 4px;
|
|
min-width: 14px;
|
|
text-align: center;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* #castBtn while actively casting - same "clearly-on" treatment as
|
|
views/player/Player.css's own .iconBtn[aria-pressed="true"] (ID-scoped
|
|
here rather than a shared .iconBtn rule since #playPauseBtn's own
|
|
"iconBtn" class in this bar has no matching style at all - see
|
|
MusicBar.html - and never sets aria-pressed, so this couldn't touch it
|
|
either way, but there's no shared iconBtn.css to hang a rule off yet). */
|
|
#castBtn[aria-pressed="true"] {
|
|
color: #ff9800;
|
|
background: rgba(255, 152, 0, .18);
|
|
}
|
|
|
|
/* Pops up ABOVE the bar (same pattern as Player.css's .settingsPanel above
|
|
its gear button - position: absolute against .musicBar's own
|
|
position: relative, bottom: 100% instead of that one's top: 100%/
|
|
right: 0, since this floats up from a bottom bar rather than down from
|
|
a top-corner icon) - collapsed by default, see MusicBar.ts's
|
|
toggleQueueBtn handler. `position: absolute` (not left in normal flow)
|
|
is what stops opening this from silently growing .bottomStack's own
|
|
fixed-to-viewport height and shoving the actual controls upward
|
|
instead of floating over them - reported directly as the desktop bar
|
|
looking broken the moment the queue was opened. Spans the full bar
|
|
width (no max-width) rather than a fixed popup box - a party's queue
|
|
can run long, and a narrow column wastes the width a desktop viewport
|
|
already has going spare. display:flex/flex-direction:column here
|
|
(rather than on .pageHeader/#queueList individually) is what lets
|
|
#queueList be the only child that scrolls below - see #queueList's own
|
|
comment. */
|
|
.musicBarQueue {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 100%;
|
|
max-height: 50vh;
|
|
margin: 0 16px;
|
|
padding: 14px 14px 0;
|
|
background: var(--bg-header);
|
|
border: 1px solid rgba(255, 255, 255, .08);
|
|
border-bottom: none;
|
|
border-radius: 10px 10px 0 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.musicBarQueue .pageHeader { flex-shrink: 0; }
|
|
|
|
/* Always reachable regardless of z-index/positioning below (see
|
|
MusicBar.ts's own click-outside handler for the other way to close
|
|
this on desktop) - reported directly that, in mobile's full-screen
|
|
layout, the *only* previous way to close this (tap the same
|
|
#toggleQueueBtn again) sat visually underneath the now-open queue
|
|
panel itself once it covered the screen, making it unreachable. */
|
|
.queueCloseBtn {
|
|
background: none;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
padding: 2px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.queueCloseBtn:hover { background: none; color: var(--text); }
|
|
|
|
/* The only scrolling region inside .musicBarQueue - "File d'attente"
|
|
above it stays put instead of scrolling away with the tracks. */
|
|
#queueList {
|
|
overflow-y: auto;
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
.mobileMiniBar { display: flex; }
|
|
|
|
/* Same fixed-overlay/slide-up-from-bottom pattern as Shell.css's
|
|
.mobileNavSheet - see docs/architecture.md's "Mobile navigation".
|
|
display stays flex unconditionally (not toggled) so the transform
|
|
transition below actually animates instead of jumping. */
|
|
.musicBarPanel {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 25;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 20px 20px calc(20px + env(safe-area-inset-bottom, 0px));
|
|
background: var(--bg);
|
|
transform: translateY(100%);
|
|
transition: transform .25s ease;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.musicBarPanel.open { transform: translateY(0); }
|
|
|
|
.panelCloseBtn {
|
|
display: flex;
|
|
color: var(--muted);
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.musicBarPanelTrack {
|
|
flex-direction: column;
|
|
text-align: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.musicBarPanelTrack .musicBarImage {
|
|
width: min(220px, 60vw);
|
|
height: min(220px, 60vw);
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.musicBarInfo { margin-right: 0; align-items: center; }
|
|
.musicBarInfo strong { font-size: 19px; white-space: normal; }
|
|
.musicBarPosition { margin-top: 6px; }
|
|
|
|
.musicBarPanelTransport { gap: 28px; margin-bottom: 28px; }
|
|
.musicBarPanelTransport .musicBarPlayBig { width: 56px; height: 56px; }
|
|
.musicBarPanelTransport .musicBarPlayBig svg { width: 22px; height: 22px; }
|
|
|
|
.musicBarPanelSide { margin-top: auto; justify-content: center; gap: 20px; }
|
|
|
|
/* A full-screen overlay of its own here, not desktop's floating popup
|
|
(there's no "above the bar" to float over once .musicBarPanel is
|
|
itself a full-screen overlay) - z-index: 26 (above .musicBarPanel's
|
|
25) is still what puts it on top, but `position: relative` used to
|
|
leave it in .musicBar's own normal flow, sized by `max-height: 50vh`
|
|
inherited from the base rule above and positioned whichever the
|
|
surrounding layout happened to allow - overlapping the very
|
|
#toggleQueueBtn used to open it, with no other way to close a
|
|
`position: relative` sibling that isn't just a small popup here.
|
|
`inset: 0` + `max-height: none` makes it genuinely full-screen like
|
|
.musicBarPanel itself, so its own #queueCloseBtn is always reachable
|
|
regardless of queue length. */
|
|
.musicBarQueue {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 26;
|
|
max-height: none;
|
|
margin: 0;
|
|
border-radius: 0;
|
|
padding: 20px 20px calc(20px + env(safe-area-inset-bottom, 0px));
|
|
}
|
|
}
|
|
|
|
/* One queue entry - a plain flex row (image, then a shrinking/truncating
|
|
info column, then the duration), NOT views/album's shared .trackRow
|
|
grid (styles/trackList.css) - that one's 4 fixed columns assume
|
|
Album.ts's own index/name/duration/button shape, which crushed this
|
|
list's track names into its 28px index column when reused here. See
|
|
MusicBar.ts's own comment. */
|
|
.queueRow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
background: var(--panel);
|
|
border-radius: 8px;
|
|
padding: 8px 10px;
|
|
}
|
|
|
|
.queueRowArt {
|
|
width: 38px;
|
|
height: 38px;
|
|
object-fit: cover;
|
|
border-radius: 6px;
|
|
background: var(--bg-header);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.queueRowArt.noImage { visibility: hidden; }
|
|
|
|
.queueRowInfo {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
|
|
.queueRowName {
|
|
font-size: 13px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Artist/album/"Ajouté par X" condensed onto one secondary line (see
|
|
MusicBar.ts) rather than each getting their own column - there's no
|
|
room for 3+ columns once a name needs to read in full, unlike
|
|
Album.ts's simpler index/name/duration/button row. */
|
|
.queueRowMeta {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|