* fix(ui): square shape across badges, pills and non-player buttons
zunoz on Discord flagged inconsistent shapes for play buttons, badges
and pills across the app. Unify to var(--radius-sm) for non-player
surfaces so the same visual indicator looks identical wherever it
appears. Player Bar, Fullscreen Player and Mini Player keep their
circular shape as part of the player family; toggle switches, sliders,
search input, pagination dots and theme overrides are left alone.
Covers: hero play + nav arrows, album-card details button, album
header icon buttons, playlist suggestion play, album-row nav arrows;
.badge (incl. New / album-detail), genre pill, np chip/tag/badge,
np-dash toolbar badge, radio filter chip, radio card chip, mp album
plays pill, settings search-result badge, alphabet filter buttons,
download hint, mobile search chip, artist release-group count, artist
external link, all Orbit session pills, device-sync count badge;
ServersTab "Aktiv" badge, PlaylistCard loading badge, AlbumRow /
ArtistRow "more" buttons.
* fix(composers): collapse empty space between virtual rows
The composer grid uses text-only tiles (~78 px intrinsic) but
estimateRowHeightPx scaled with cell width like the image variants,
clamped to a 200 px maximum. On normal viewports every virtual row
reserved ~200 px while the actual card was ~78 px, leaving ~120 px of
empty space below each row.
Pin the composer variant to min === max so the rowHeight is a fixed
88 px regardless of cell width.
* fix(ui): unify secondary action buttons on btn-surface
Action rows on the Artist, Album, Tracks, Favorites and Most Played
pages mixed btn-ghost (borderless), btn-surface (bordered) and bare
.btn (no variant, picked up bordered look in light themes only).
Result was per-page and per-theme inconsistency — secondary buttons
sometimes had borders, sometimes not.
Unify on btn-surface for all secondary actions so the same affordance
looks identical across pages and the difference between themes is just
border tone, not border presence.
- Tracks hero: Enqueue and Reroll buttons
- AlbumHeader: Shuffle, Enqueue, Star, Share, Bio, Download and the
Offline-cache states
- MostPlayed: sort toggle and compilations filter (now matches the
Albums page header)
* fix(hero): make pagination dots visible on light backdrops
The pagination dots used `rgba(255, 255, 255, 0.35)` which disappears
against white-dominant cover art and on light themes, even under the
hero gradient overlay. zunoz on Discord reported the inactive dots as
effectively invisible.
Bump inactive dots to 85 % white with a dark outline + drop shadow so
they read against any backdrop, and switch the active dot to the
accent color so the highlight reads as colour, not just width.
Also opaque-fill `.badge` (`var(--accent)` + `--ctp-crust` text) so the
hero pills do not disappear against light cover art either — base
class previously used `--accent-dim` which is too transparent for
badges per the existing badge rule.
* fix(tracks): align Browse-all-tracks header with rows
The Tracks page header sat outside the scroll container while rows
sat inside it, so the scrollbar gutter shrank only the rows and the
last header column drifted right of its row data. With OpenDyslexic
selected the wider glyphs pushed "Duration" past the viewport edge
entirely; zunoz on Discord reported it.
Move SongListHeader inside the scroll container with position:
sticky so header and rows share the same width budget. Sticky
keeps the header visible while scrolling as a side benefit.
* test(cardGridLayout): pin composer variant to fixed row height
Guards the fix that collapsed the empty space between Composers grid
rows. Re-introducing the `cellWidthPx + extra` scaling for composer
would silently bring back ~120 px of dead space per virtual row, so
the test asserts the fixed 88 px output across a wide cellWidth range.
Image variants (artist / album / playlist) get baseline assertions so
the composer case is documented as the deliberate exception, not an
oversight.
* docs(changelog): UI consistency sweep (PR #745)
The search toggle in Settings shifted everything 1-2 px down when
opened because the open input wrap was taller than the closed icon
button, and the header row sized to its children. Lock the header to
a min-height of 40 px so both states fit, give the icon button a
proper 28x28 hit area with hover state, and pin the open input wrap
to 32 px so toggling never reflows the row.
* fix(artist-info): id-gate fetcher tuples, reuse ArtistCard on ArtistDetail
The cache-mismatch bug PR #732 fixed in `NowPlayingInfo.tsx` had the same
shape inside `ArtistCard` on the NowPlaying page: `useNowPlayingFetchers`
returned `artistInfo` for the previously-current artist for one render
after `artistId` changed, and `CachedImage` persisted that mismatched
blob under the new `artistInfo:<new-id>:hero` key in IndexedDB —
sticky "previous artist" image on every subsequent track.
Apply the same `{ id, value }` tuple pattern from PR #732 inside the
hooks themselves so every consumer is safe by construction:
- `useNowPlayingFetchers`: gate `artistInfo`, `songMeta`, `albumData`,
`discography` on id-match at the return. Late-arriving resolves for
a stale id can no longer overwrite the displayed value.
- `useArtistDetailData`: same for `info`. Required because the
`ArtistDetail` bio card now uses `CachedImage` via the shared
`ArtistCard` (previously raw `<img>`, no persistence hazard).
Unify `ArtistDetail`'s inline "About the Artist" block onto the same
shared `ArtistCard` so there is one source of truth for hero / bio /
similar rendering. New optional props: `onNavigate?` (omitted on
`/artist/:id` since the user is already there), `coverFallback`
(coverArt fallback when artistInfo has no hero image),
`hideArtistName` (avoid duplicating the hero name), `hideSimilar`
(ArtistDetail has its own similar-artists section).
Tests cover the gating contract for both hooks (incl. stale-resolve
race) and the `ArtistCard` prop matrix.
* fix(artist-image): square queue info hero, drop artist-avatar glow
- Queue Info bar's artist hero was rendered in a 16:10 wrap with
`object-fit: cover`, so portrait photos lost top/bottom equally
while landscape ones lost the sides — perceived as cropped even
on roughly square sources. Set the wrap to 1:1 so the crop is
symmetric and matches the typical square framing of artist
photography.
- `ArtistDetail` extracted the cover's accent colour on every image
load and rendered a 36px / 8px-spread `boxShadow` ring around the
avatar. Drop the glow, the state, the one-shot reset effect, the
prop-passing through `ArtistDetailHero`, and the now-orphaned
`extractCoverColors` import on this page. `extractCoverColors`
itself stays in place (still used by `useFsDynamicAccent`).
* docs(changelog): artist-info image fix extension + UI tweaks (PR #739)
* feat(playback): stream buffering UI, ranged M4A tail prefetch, demuxer fix
Defer seekbar/progress until HTTP stream is armed for both legacy and
RangedHttpSource; show buffering overlay on cover art. Add MP4 tail
prefetch and Symphonia isomp4 bounded-mdat/moov-at-EOF probing so
moov-at-end M4A can start without reading the full mdat.
* feat(hot-cache): spill large ranged streams to disk for promote
When a ranged HTTP download completes above the 64 MiB RAM promote cap,
write the existing buffer once to app-data stream-spill/ and register it
for hot-cache promote (rename) and replay via fetch_data. Analysis seeds
from the spill file up to the local-file cap (512 MiB).
* fix(ui): stream buffering — grayscale cover and static clock icon
Desaturate player and queue cover art while isPlaybackBuffering; keep a
non-animated clock overlay for visibility without the spinning animation.
* fix(playback): review follow-up — tests, i18n, spill cleanup, changelog
Clippy and test layout fixes; stream spill orphan cleanup on startup;
buffering flag guard in progress handler; bufferingStream in all player
locales; CHANGELOG and contributor credits for stream/M4A work.
* docs: attribute stream buffering and M4A streaming to PR #737
* test(audio): avoid create_engine in stream spill unit test
CI runners have no audio output device; test spill take/consume via
the Mutex slot only, matching install_stream_completed_spill tests.
* feat(hero): prev / next arrows on the Mainstage featured strip
Adds left and right chevron buttons over the featured-album hero so a
single click flips to the previous / next album. Hitting the 8 px dot
indicators was awkward and often opened the underlying album by
mistake; the arrows give a generous 44 px touch target on each edge.
- New `goPrev` / `goNext` callbacks wrap with modulo, restart the
auto-advance timer on click (same pattern the old dot handler used).
- Buttons live in a new `.hero-nav` flex wrapper with `inset: 0` and
`justify-content: space-between`, so they pin to the hero's left and
right edges regardless of theme padding. `pointer-events: none` on
the wrapper + `auto` on the buttons keeps the rest of the hero
click-through (navigate to album).
- Dots are now decorative spans, not buttons — `pointer-events: none`,
no hover state, no `onClick`. Clicking near a dot used to navigate
to the album because the dot was too small to land on.
- i18n: `previousAlbum` / `nextAlbum` keys in all 9 locales.
Reported by zunoz on Discord.
* docs(changelog): note Mainstage hero prev / next arrows (#735)
* fix(album): contain Artist Biography modal scroll within frame
The modal was rendered inside the album page tree, where an ancestor
broke `position: fixed` so the overlay scrolled with the page instead
of pinning to the viewport. Long bios also pushed the modal past the
viewport entirely.
- Render `BioModal` via `createPortal(document.body)` so no ancestor's
transform/filter/backdrop-filter can break fixed positioning.
- New `.modal-content.bio-modal` variant: flex column, `overflow:
hidden`. Title + close button stay pinned, only `.bio-modal-body`
scrolls. The existing `max-height: 80vh` on `.modal-content` now
reliably caps the modal to the visible viewport.
Reported by zunoz on Discord.
* docs(changelog): note Artist Biography modal scroll fix (#734)
* fix(ui): consolidate Orbit / Server / Live header dropdowns
The three header dropdown popovers each had their own container style.
Live used a glass utility class with backdrop-filter that read poorly on
many themes (reported by cucadmuh). Move all three onto the shared
`.nav-library-dropdown-panel` container — same bg, border, shadow and
radius via existing semantic tokens (`--bg-card`, `--border-dropdown`,
`--shadow-dropdown`, `--radius-md`). Item styles per dropdown stay
case-specific.
- NowPlayingDropdown: replace `glass animate-fade-in` with
`nav-library-dropdown-panel animate-fade-in`; drop now-redundant inline
borderRadius / boxShadow / zIndex; keep padding + gap for the
user-list breathing room.
- OrbitStartTrigger: add `nav-library-dropdown-panel` alongside
`orbit-launch-pop`. The component-local class is now reduced to the
menu-specific min-width + tighter item gap; bg / border / shadow /
radius / padding inherit from the shared container.
- Drop the unused `.glass` utility class; no other call sites.
* docs(changelog): header dropdown consistency fix (#725)
* feat(offline): show cached albums from all servers in Offline Library
List every offline album regardless of active server; load cover art per
source server and switch before play/enqueue. Sidebar, mobile nav, and
disconnect auto-nav use any cached content; multi-server cards show a label.
* docs(changelog): link offline library PR #719
* chore(pr-719): address review — CHANGELOG in Added, helper tests
Move release note to ## Added per team changelog policy; cover
offlineAlbumCoverArt and ensureServerForOfflineAlbum in unit tests.
Adds a per-element visibility toggle for the playlist detail page (Add
Songs, Import CSV, Download ZIP, Cache Offline, Suggestions) and reworks
the way uncommon options are surfaced: instead of a per-tab collapsible
group, a global "Advanced" toggle in the Settings header reveals all
`advanced` sub-sections across every tab and marks each one with a small
badge. Sets the pattern up so any future advanced option lives in its
natural tab, gated by the same switch.
- New `advancedSettingsEnabled` boolean on `authStore`
(UiAppearance slice, persisted with the rest of the store).
- `SettingsSubSection` gains an `advanced?: boolean` prop. Hidden when
the toggle is off; renders an "Advanced" pill in the header when on.
- Settings header gets a Toggle-Switch next to the search lupe.
- `PersonalisationTab` flattens — Sidebar + Home stay always visible;
Artist sections, Queue Toolbar, and the new Playlist layout get
`advanced` and disappear by default. `PersonalisationAdvancedGroup`
component + CSS removed.
- New `playlistLayoutStore` (Zustand + persist, items[{id,visible}] +
rehydrate sanitize) following the queueToolbarStore pattern.
- `PlaylistHero` and `PlaylistSuggestions` gate the four toolbar buttons
and the suggestions rail on the store directly.
- One-time migration in MainApp on mount: if the user had opened the
old per-tab Advanced group (`psysonic_personalisation_advanced_open
=== 'true'`) OR already customised any of the three sub-sections,
Advanced Mode auto-enables on first launch. Idempotent via a
localStorage flag; legacy key removed afterwards.
- New i18n keys `settings.advancedMode`, `settings.advancedModeTooltip`,
`settings.advancedBadge`, `settings.playlistLayout*` in all 9 locales.
Reuses kveld9's design from PR #556; not merged because the locale split
landed afterwards. Credited under the existing kveld9 entry in
settingsCredits.ts.
Co-authored-by: Kveld. <kveld912@proton.me>
Global queue paste opens the preview modal before play; overlay scrollbar,
context-menu suppression, changelog/credits for PR #716 and DanielWTE (#551).
Implement share-link detection in search (track, queue, album, artist,
composer): enqueue tracks/queues without interrupting playback; preview
album/artist/composer without switching the active server; queue preview
modal with scrollable track list. Based on community PR #551.
Co-authored-by: Daniel Wagner <daniel.iuser@icloud.com>
* fix(ui): split OpenSubsonic album and track artists in header and player
Album detail header uses albumArtists from album or child songs; player bar,
mobile player, and mini player use structured track artists with per-id links.
Adds deriveAlbumHeaderArtistRefs helper and OpenArtistRefInline.
Fixes#552
* docs: changelog and credits for OpenSubsonic artist links (PR #696)
The components.css split (PR #657) wrote `result-items.css` to disk, but
.gitignore line 57 (`result-*`) silently filtered it out of `git add -A`.
Local builds worked because the file existed on the splitter's machine;
fresh clones fail with `ENOENT: no such file or directory` when vite
resolves the `@import './result-items.css';` in components/index.css.
Force-adding with `git add -f` keeps the broad `result-*` ignore for
test artifacts intact.
components.css (14205 LOC) → 84 per-section files in src/styles/components/ +
an index.css that imports them in original cascade order.
Same approach as the theme.css split: top-level sections are detected by
single-dash or 3+ dash header decoration (/^\/\* ─(?: |─{2,})/), 2-dash
sub-sections stay inside their parent. Concatenating in @import order
reproduces the original byte stream (+1 trailing newline, cosmetic).
Each section is now self-contained — touching Tracklist, Modal, Hero,
Sidebar, etc. only opens one focused file.
Generated via /tmp/split-components-css.mjs.