Two small, unrelated requests handled together. The search bar (tiles.ts's shared .searchBar, used by views/library/ artist/album) scrolled away with the page - asked to stay reachable while scrolling a long grid. position: sticky; top: 0 on the input itself; base.css's own generic input rule already gives it an opaque background, which is what keeps tiles scrolling underneath from showing through once it's stuck - nothing else needed changing. Seeking within the current music track was explicit, deliberate scope this session's own "participative playlist" work had cut - the progress bar was marked read-only specifically because MusicQueueService had no seek concept, just play/pause/advance. The permission model was already decided by that class's own documented reasoning for why *anyone* can pause/resume (reversible, doesn't change what's playing) - a seek is exactly that same shape, continuous instead of binary, so it gets the same treatment, not voteSkip's majority gate. MusicQueueService.seek mirrors setPaused's own elapsed-time math (clamped into [0, durationSeconds]), with one real difference: setPaused's resume branch gets away with calling scheduleAdvance directly only because pausing already cleared the timer first - scheduleAdvance itself never clears a prior one. Seeking while still playing has no such guarantee, so skipping that clear first would leave the OLD timer (counting down against the old position) running alongside the new one, eventually firing playNext at the wrong moment. New musicSeek socket event, no host check (same as musicPlaystate). Client-side, MusicBar's progress track gained the same pointerdown/move/up drag-to-seek handlers the video player's seek bar already has - no host branch to port this time, since a listen party has none - plus a local, optimistic activeDriver.currentTime write before broadcasting, for the same instant-feedback reason the video player's own seekFromPointerEvent does that. Verified: full typecheck/test/build on both projects (128/128 server tests - new coverage for seek's clamping, its paused-vs-playing branches, and a real-timer test proving the old advance timer is cleared rather than left running alongside a re-armed one - the exact bug the naive "just call scheduleAdvance again" version would have shipped). No server needed for the sticky search bar - checked by reading .mainContent's own layout directly (no nested scroll container between it and the document, confirming position: sticky resolves against the page scroll as intended). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
137 lines
4 KiB
CSS
137 lines
4 KiB
CSS
/* ---- Tile grid (library/artist pages) + tiles (shared with the home
|
|
carousels) - see tiles.ts. ---- */
|
|
|
|
/* base.css's global `input {}` rule already gives this the right look
|
|
(padding/border/background) - this just bounds its width and spaces it
|
|
from the grid below, shared by views/library, views/artist, views/album -
|
|
see tiles.ts's wireSearchFilter. Sticky, not just top-of-page: asked
|
|
for directly, so it stays reachable while scrolling a long grid
|
|
instead of scrolling away with the page title above it. Its own
|
|
background (base.css's input rule already gives it var(--panel), not
|
|
transparent) is what keeps tiles scrolling underneath from showing
|
|
through its own bounds once stuck. */
|
|
.searchBar {
|
|
max-width: 320px;
|
|
margin-bottom: 16px;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 2;
|
|
}
|
|
|
|
.searchEmpty { margin-top: 8px; }
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
gap: 20px 16px;
|
|
}
|
|
|
|
.tile {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
/* Without this, a long title (or an image's intrinsic size before it
|
|
loads) can force this flex/grid item wider than its declared size -
|
|
the classic "min-width: auto" flexbox/grid pitfall. Every tile must
|
|
stay exactly the track/flex-basis size regardless of content. */
|
|
min-width: 0;
|
|
}
|
|
|
|
.tile img {
|
|
width: 100%;
|
|
aspect-ratio: 2 / 3;
|
|
object-fit: cover;
|
|
border-radius: 8px;
|
|
background: var(--panel);
|
|
display: block;
|
|
}
|
|
|
|
.tile img.noImage { visibility: hidden; }
|
|
|
|
.tileLink {
|
|
position: absolute;
|
|
inset: 0 0 26px 0;
|
|
z-index: 1;
|
|
}
|
|
|
|
.tileLabel {
|
|
font-size: 13px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Hover-reveal overlay with the two actions, on devices that actually have
|
|
hover (mouse/trackpad). Touch devices get an always-visible bar instead -
|
|
see the (hover: none) block below, since :hover is unreachable there. */
|
|
.tileOverlay {
|
|
position: absolute;
|
|
inset: 0 0 26px 0;
|
|
z-index: 2;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, .55);
|
|
border-radius: 8px;
|
|
opacity: 0;
|
|
transition: opacity .15s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tile:hover .tileOverlay,
|
|
.tile:focus-within .tileOverlay {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
/* Base look is the same solid, high-contrast treatment as every other
|
|
button in the app (see base.css's global `button {}` rule) - but that
|
|
rule is tag-scoped, so it never applied to "Voir les épisodes", the one
|
|
.tileBtn that's an <a>, not a <button> (a plain hash link, so the
|
|
browser's native navigation handles it with no JS needed). Left
|
|
unstyled beyond padding/text-align, it would render as a bare, small,
|
|
default-blue link sitting on the overlay - much less visible than
|
|
"Lancer une watch party" (a real <button>) or "Voir sur Emby" (an <a>,
|
|
but with the explicit .tileBtnLink look below) right next to it.
|
|
Styling .tileBtn itself, tag-agnostic, fixes that - and is the
|
|
semantically right call besides: for a Series tile, "Voir les épisodes"
|
|
IS the primary action (a Series is never itself directly playable, so
|
|
the two never appear on the same tile), not a secondary one. */
|
|
.tileBtn {
|
|
padding: 8px 10px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.tileBtn:hover { background: var(--accent-bright); }
|
|
|
|
/* "Voir sur Emby" only: the one secondary/neutral action among the tile
|
|
buttons - overrides just the two properties that make it read as
|
|
secondary, on top of .tileBtn's shared sizing/weight/radius above. */
|
|
.tileBtnLink {
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
}
|
|
|
|
.tileBtnLink:hover { background: #333; }
|
|
|
|
@media (hover: none) {
|
|
.tileOverlay {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
inset: auto 0 26px 0;
|
|
flex-direction: row;
|
|
background: linear-gradient(to top, rgba(0,0,0,.75), transparent);
|
|
padding: 6px;
|
|
}
|
|
.tileBtn { font-size: 11px; padding: 6px 4px; }
|
|
}
|