Commit Graph

149 Commits

Author SHA1 Message Date
bvandeusen 835580ef1e fix(web/m7-380): convert PlaylistTrack to TrackRef in PlaylistCard play handler 2026-05-04 19:46:43 -04:00
bvandeusen b290b4ff0b feat(web/m7-380): animate pending placeholder + add play overlay to PlaylistCard 2026-05-04 19:42:30 -04:00
bvandeusen 26f4c7a79f fix(web/m7-352): use typographic apostrophe in failed-variant copy 2026-05-04 19:16:25 -04:00
bvandeusen 412a1c00ac fix(web/m7-352): update listPlaylists test URL after kind filter addition 2026-05-04 18:52:40 -04:00
bvandeusen b8bb1c1fa7 feat(web/m7-352): PlaylistPlaceholderCard with 4 state variants 2026-05-04 18:31:42 -04:00
bvandeusen 886903ae27 feat(web/m7-352): kind-aware createPlaylistsQuery + system-playlists-status helper 2026-05-04 18:30:43 -04:00
bvandeusen 56f0998f84 feat(web/m7-352): Playlist type gains kind/system_variant/seed_artist_id fields 2026-05-04 18:14:46 -04:00
bvandeusen db361c8305 feat(web/m7-377): CompactTrackCard gains like + queue + kebab overlays 2026-05-04 17:49:19 -04:00
bvandeusen 0dbe326d8b fix(server,web): forward-fix CI lint + vitest failures
- gofmt -s on system.go, system_cron.go, api.go
- rename unused r → _ in 3 fetcher_test.go HTTP handlers
- TrackRow +queue test uses /add .* to queue/i (track-aware aria-label from #377)
- /library/artists page test mocks likes + tanstack-query (ArtistCard now embeds LikeButton)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:39:06 -04:00
bvandeusen 93f54e00a4 fix(server,web/m7-353): forward-fix CI — Mount call + AlbumRef test fixtures 2026-05-04 17:31:27 -04:00
bvandeusen 28617df5bd feat(web/m7-353): admin cover-refetch UI (per-album + bulk)
Adds cover_art_source to AlbumRef, two admin API helpers (refetchAlbumCover,
refetchMissingCovers), a per-album retry button gated on is_admin in the album
detail page, and a bulk-refetch section in the admin overview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:15:26 -04:00
bvandeusen 8fff7f62d6 refactor(web/m7-377): TrackRow +queue uses Lucide Plus icon + scoped aria-label 2026-05-04 13:08:26 -04:00
bvandeusen d35ed5945a feat(web/m7-377): AlbumCard gains bottom-right kebab 2026-05-04 13:07:06 -04:00
bvandeusen 4e42fc8280 feat(web/m7-377): ArtistCard top-right cluster + bottom-right kebab 2026-05-04 13:05:11 -04:00
bvandeusen 43a3dc47a8 feat(web/m7-377): AlbumMenu component + listAlbumTracks helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 11:59:38 -04:00
bvandeusen ce01b3d992 feat(web/m7-377): ArtistMenu component with like/queue/playlist actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 11:53:41 -04:00
bvandeusen 72496852a5 feat(web/m7-377): LikeButton gains size prop (sm/md/lg) 2026-05-04 11:50:31 -04:00
bvandeusen 9eeb79e99f fix(web/m7-377): update AddToPlaylistMenu tests for tracks-array prop 2026-05-04 11:48:55 -04:00
bvandeusen 7ddf77ace5 refactor(web/m7-377): AddToPlaylistMenu accepts tracks: TrackRef[] 2026-05-04 11:47:53 -04:00
bvandeusen c04b288737 feat(web/m7-365): add History entry to Library nav 2026-05-04 06:59:05 -04:00
bvandeusen da39ff847b feat(web/m7-365): HistoryRow component with click-to-play 2026-05-04 06:19:59 -04:00
bvandeusen 4dee5922d9 feat(web/m7-365): dayBucket + groupByDay utilities 2026-05-04 06:18:18 -04:00
bvandeusen 46066c4d08 feat(web/m7-365): history API helper + tests 2026-05-04 06:16:15 -04:00
bvandeusen d202319c87 fix(web/m7-364): vitest failures — exact-match query, inert prop, -0, user guard
CI test-web was blocking on:

- QueueDrawer.test close-button query: getByLabelText(/close queue/i)
  matched BOTH the backdrop button (aria-label="Close queue backdrop")
  and the close button (aria-label="Close queue"). Switched to exact
  string match.

- QueueDrawer.test inert assertion: Svelte 5 + jsdom uses property
  binding for `inert`, not attribute, so hasAttribute('inert') returns
  false. Check the DOM property (with attribute fallback for
  robustness) instead.

- queue-row-math.ts -0 normalization: Math.round(-0.5) returns -0,
  and Object.is(-0, 0) is false — vitest's .toBe(0) fails. Added
  `|| 0` to normalize -0 to +0 at the function boundary so consumers
  never see -0.

- player/store.svelte.ts user import guard: persistence $effect.root
  reads `user.value?.id` at module-init, but in test files where
  auth/store.svelte.ts is imported transitively (auth/store.test,
  playlists/playlists.test), the player module loads via auth's
  import chain before auth's `user` binding is assigned. `user?.value`
  guards against that init-order race. The proper fix is to invert
  the auth↔player import dep (filed as I2 follow-up under #375).
2026-05-03 22:37:00 -04:00
bvandeusen d43c6b31f4 chore(web/lint): clear svelte-check warnings + fix QueueDrawer test query
CI svelte-check was blocking on:

- 1 ERROR I introduced in the previous cleanup: QueueDrawer.test.ts
  passed { hidden: true } to getByLabelText, but that option only
  exists on getByRole. Fixed by switching to a direct
  document.querySelector for the aria-hidden assertion.

- 13 WARNINGS pre-existing in the codebase from M7 #352/#372/#349
  era, never surfaced because earlier CI runs failed before reaching
  type-check. Now that CI gets that far, they accumulate. Cleared:

  - FlagPopover, RemoveTrackPopover, AddToPlaylistMenu, TrackMenu:
    interactive role divs gain tabindex="-1" + onkeydown that stops
    propagation and closes on Escape (functional, not just warning-
    suppression).

  - FlagPopover state-from-props: $state(untrack(() => prop ?? default))
    for explicit initial-snapshot semantics; const isUpdate switched
    to $derived so it reacts to prop changes.

  - PlaylistTrackRow drag-div: role="listitem" added.

  - playlists/+page.svelte: autofocus replaced with bind:this + $effect.
2026-05-03 22:26:09 -04:00
bvandeusen 22e3f67411 feat(web/m7-364): drawer focus management + grip Enter/Space handling 2026-05-03 22:23:20 -04:00
bvandeusen d83b3eab25 fix(web/m7-364): TrackRef field shape + seek-on-restore + drawer inert
CI svelte-check failed on 5 type errors caused by the slice using
duration_ms/album_name fields that don't exist on TrackRef (the real
shape uses duration_sec + album_id/album_title). Fixed across
QueueDrawer, QueueDrawer.test, QueueTrackRow.test, persisted.test,
and the inline `state.current = null` in PlayerBar.test (TrackRef |
undefined, not | null).

Final whole-slice review concerns also rolled in:

- C1 (Critical): persisted position was restored to UI but never
  seeked into the audio element — first timeupdate snapped _position
  back to 0. Added a one-shot _pendingRestorePosition module ref +
  consumePendingRestorePosition() export; layout's loadedmetadata
  handler now seeks once on restore.

- I1 (Important): throttled-write effect now wraps _queue / _index
  reads in untrack so the dependency tracking matches the design
  intent. Position remains the only tracked dep.

- I4 (Important): drawer gets inert={!queueDrawerOpen} so its inner
  buttons drop out of tab order when closed (aria-hidden alone
  doesn't do that). Removed redundant role="complementary" from
  <aside> (implied by the element). New test asserts inert binding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 22:14:59 -04:00
bvandeusen 82846c9fcd fix(web/m7-364): align queue toggle styling with sibling toggles
- Queue toggle active class: bg-accent-tint text-accent (matches shuffle/repeat)
- ListMusic icon size: 20 → 18 (matches siblings)
- Right-side container width: w-48 → w-60 (accommodates 4 buttons + volume)
2026-05-03 21:52:39 -04:00
bvandeusen 36805914d8 feat(web/m7-364): PlayerBar queue toggle + Up next line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:50:14 -04:00
bvandeusen b99eb5f120 feat(web/m7-364): QueueDrawer side-panel with track list 2026-05-03 21:47:33 -04:00
bvandeusen e164d69492 fix(web/m7-364): keyboard reorder + measured row height + offsetToDelta helper 2026-05-03 21:45:32 -04:00
bvandeusen 57a353a138 feat(web/m7-364): QueueTrackRow with neodrag reorder + remove 2026-05-03 21:41:59 -04:00
bvandeusen b2e3913df8 feat(web/m7-364): clear persisted queue + reset queue on logout 2026-05-03 21:39:57 -04:00
bvandeusen 472b9d9e84 fix(web/m7-364): untrack _position in queue immediate-write effect
Wrap the _position read inside untrack() in the immediate-write $effect
so it no longer registers as a reactive dependency, preventing the effect
from firing on every 4Hz position tick. Also adds three missing
restoreQueue unit tests with vi.mock for the auth store.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:37:55 -04:00
bvandeusen 8cc79ee612 feat(web/m7-364): queue persistence write/restore on player store 2026-05-03 20:44:33 -04:00
bvandeusen bde017dad3 fix(web/m7-364): pause on last-track remove + reset drawer in test setup 2026-05-03 20:37:33 -04:00
bvandeusen 6f9b5d0059 feat(web/m7-364): queue mutations + drawer state on player store
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 20:29:48 -04:00
bvandeusen 8053704afa feat(web/m7-364): localStorage persistence layer for queue 2026-05-03 20:26:07 -04:00
bvandeusen 180d85eec0 chore(web): post-#363 cleanup — DRY tripwires, drop unused description, singular playlist title
M2 — Add TODO(#375) tripwire comments where the dark/light theme-color
hex pair is duplicated outside tokens.json (vite.config.ts and
applyMetaThemeColor.svelte.ts). Refactoring is out of scope for #363;
the comments are stop signs for the next person who edits these.

M5 — The SPA never reads window.__MINSTREL__.description. The OG meta
description is server-rendered and complete on its own. Drop the dead
inline-script field and the corresponding helpers in branding.ts.
Server-side BrandingConfig.Description stays — it's still used for
<meta name="description"> and og:description.

Copy nit — Playlist detail page title becomes "Playlist · Foo"
matching the singular form already used by Artist · / Album ·.
2026-05-03 19:04:59 -04:00
bvandeusen 66f651cf5c fix(web/m7-362): repair Vitest fixtures (matchMedia.fire, listenbrainz mock)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 18:48:20 -04:00
bvandeusen af6f006242 feat(web/m7-363): theme-color meta follows resolved theme 2026-05-03 17:44:01 -04:00
bvandeusen f31dbcc087 refactor(web/m7-363): Shell header reads appName() not literal 2026-05-03 17:32:57 -04:00
bvandeusen 6c23dba51b feat(web/m7-363): branding helper + window.__MINSTREL__ consumer 2026-05-03 17:30:31 -04:00
bvandeusen 7faa7b4e1f fix(web): hide Play next / Add to queue on PlayerBar's track menu
Those actions don't have meaningful semantics for the track you're
already listening to. New TrackMenu prop hideQueueActions gates the
pair; PlayerBar passes it true. Other mount sites (track rows in
lists) keep the full 8-entry menu.
2026-05-03 14:08:29 -04:00
bvandeusen 4b101481fc fix(web): drop-up Add-to-playlist submenu, hide horizontal scroll row scrollbar
- AddToPlaylistMenu: anchor submenu bottom to parent bottom so it
  grows upward (top-0 → bottom-0) — kebab row is usually near the
  bottom of the track menu and the submenu kept extending off-screen.
- HorizontalScrollRow: scrollbar-width: none + ::-webkit-scrollbar
  display:none. Page arrows already drive the scroll; the scrollbar
  was just consuming vertical real estate.
2026-05-03 14:04:07 -04:00
bvandeusen dd50ce09fd test(web/m7-362): theme store unit test (default/persist/matchMedia) 2026-05-03 13:53:03 -04:00
bvandeusen ffed19f736 feat(web/m7-362): theme preference + resolved-theme rune store 2026-05-03 13:43:28 -04:00
bvandeusen f0bfac2555 refactor(web/m7-362): action-button labels use non-flipping text-action-fg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 13:42:42 -04:00
bvandeusen 9b7dd8272e feat(web/m7-362): tokens.json dark/light/flat split + dual-block CSS generator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 13:39:54 -04:00
bvandeusen 0ce78035aa test(web): PlaylistCard cover-img assertion uses DOM query
The <img alt=""> production element renders as role="presentation"
(decorative — playlist name is in sibling text). screen.getByRole('img')
doesn't match presentation-role elements. Switch to
container.querySelector('img') so the test doesn't fight the
correct a11y choice. Production code unchanged.
2026-05-03 12:28:21 -04:00