diff --git a/README.md b/README.md
index d15c034..e2493f2 100644
--- a/README.md
+++ b/README.md
@@ -354,6 +354,10 @@ Then put it behind your existing reverse proxy like everything else
- [x] Mobile navigation redesigned as a bottom sheet (Accueil /
Bibliothèques / Listes de lecture) instead of a horizontally
scrolling sidebar — see docs/architecture.md
+- [x] Music UI reworked to look like a music app instead of reusing
+ movie-poster tiles — circular artist tiles, square album tiles,
+ a real hero header, and a mobile "now playing" full-screen player
+ — see docs/architecture.md
- [ ] Try a full party end-to-end with two real accounts (still the main gap)
- [ ] Try casting to a real Chromecast (only the SDK's own bootstrap is
confirmed here — no real device/session)
diff --git a/client/src/musicBar/MusicBar.css b/client/src/musicBar/MusicBar.css
index c81c064..c802df8 100644
--- a/client/src/musicBar/MusicBar.css
+++ b/client/src/musicBar/MusicBar.css
@@ -50,12 +50,97 @@
pointer-events: none;
}
-.musicBarRow {
+/* ---- 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;
- padding: 8px 16px;
- flex-wrap: wrap;
+ 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
@@ -95,6 +180,60 @@
.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
@@ -135,9 +274,62 @@
}
@media (max-width: 720px) {
- .musicBarRow { justify-content: center; }
- .musicBarInfo { margin-right: 0; flex-basis: 100%; order: -1; text-align: center; }
- .musicBarQueue { margin: 0; border-radius: 0; }
+ .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
diff --git a/client/src/musicBar/MusicBar.html b/client/src/musicBar/MusicBar.html
index ce0eb8d..9dc8826 100644
--- a/client/src/musicBar/MusicBar.html
+++ b/client/src/musicBar/MusicBar.html
@@ -5,35 +5,70 @@