watchparty/client/src/musicBar/MusicBar.css
Nicolas 617a3d0d49
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
Merge remote-tracking branch 'origin/master' into feature/music-ui
# Conflicts:
#	README.md
#	docs/architecture.md
2026-09-16 15:58:43 +02:00

384 lines
12 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 {
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. */
.musicBarProgressTrack {
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) - collapsed by default, see MusicBar.ts's toggleQueueBtn
handler. 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 {
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; }
/* 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; }
/* Positioned + given a real z-index only here: a normal-flow sibling
otherwise loses to .musicBarPanel's fixed overlay above it once
that's open (its own z-index applies regardless of DOM order) -
opening the queue while the now-playing panel is expanded needs
to still show it on top, matching desktop's existing "queue pops
up above the bar" behavior. */
.musicBarQueue { position: relative; z-index: 26; margin: 0; border-radius: 0; }
}
/* 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;
}