Commit Graph

39 Commits

Author SHA1 Message Date
bvandeusen cf18d51394 feat(web): add AlphabeticalGrid wrapper with letter dividers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:21:34 -04:00
bvandeusen 0586483a42 feat(web): add CompactTrackCard for Most played rows
Small 140px clickable card showing album cover, title, and artist name.
Clicking calls playQueue(sectionTracks, index) to play through the full
section list. Includes Vitest tests for click and render behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:19:16 -04:00
bvandeusen c7e7dd269b feat(web): add ArtistCard (circular) with play-shuffle overlay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:18:05 -04:00
bvandeusen b5741e59b7 feat(web): polish AlbumCard with FabledSword tokens + play overlay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:16:43 -04:00
bvandeusen 8f5eb21094 feat(web): add HorizontalScrollRow component
Reusable horizontally-scrolling row with left/right arrow buttons,
scroll-boundary disabling, snap scrolling, and hover-reveal arrows.
Used by M6a home page (Task 17).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:15:10 -04:00
bvandeusen b1f2227d62 feat(web): extend ArtistRef/AlbumRef + add HomePayload + new query keys
Adds sort_name/cover_url to ArtistRef, sort_title to AlbumRef (matching
backend Task 5 wire shape), HomePayload type, and qk.home/albumsAlpha/
artistTracks query keys. Updates existing test fixtures to satisfy the
new required fields.

Verified clean via 'npm run check' (0 errors, only pre-existing warnings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:12:25 -04:00
bvandeusen 2f3326aee6 feat(web): suggestion feed on /discover (search-empty default) 2026-05-01 06:51:41 -04:00
bvandeusen 7f18a04161 feat(web): DiscoverResultCard attribution prop for M5c suggestions 2026-05-01 06:25:42 -04:00
bvandeusen 3bfec944c7 feat(web): /admin/quarantine aggregated queue with resolution actions
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:34 -04:00
bvandeusen 0d7a65cffb feat(web): /library/hidden user-facing quarantine view
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:35:11 -04:00
bvandeusen 88ff997af7 feat(web): mount TrackMenu in TrackRow + PlayerBar 2026-04-30 20:31:56 -04:00
bvandeusen 8da4a3f9c0 feat(web): TrackMenu overflow + FlagPopover for the quarantine flow 2026-04-30 20:29:07 -04:00
bvandeusen 041e63744d fix(web): promote bg-accent-tint to a real Tailwind utility
Replaces the :global scoped-style workaround in AdminSidebar with a proper
accent.tint color tier in tailwind.config.js. Tailwind generates bg-accent-tint
deterministically; the global class no longer leaks from a single component.
Future consumers (e.g. /admin/requests tab counts) get the same utility.
2026-04-29 22:54:30 -04:00
bvandeusen bfd48f5a02 feat(web): add /admin layout with role-gated load + sidebar
Hard route gate in admin/+layout.ts redirects non-admins to / before
the layout (or any child) renders. AdminSidebar reads the active route
from $app/state and applies a 12% accent-tinted bg + 2px forest-teal
left strip on the active item. Overview landing shows pending-request
count and Lidarr connected/unset, each linking to its sub-page. Shell
nav exposes the Admin link only to is_admin users.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:48:26 -04:00
bvandeusen ad904afaf6 feat(web): add /requests user-facing request history
Renders the caller's Lidarr requests as rows with kind pill,
StatusPill, and per-status actions: Cancel on pending (which
calls cancelRequest then invalidates qk.myRequests()), Listen
link on completed (deepest match wins: track > album > artist),
admin notes on rejected. Empty state uses the voice-rule
"Nothing requested yet." copy. Shell nav gains /requests
between /discover and /playlists, visible to all authed users
since the view is per-user.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:24:17 -04:00
bvandeusen 8a12b8c571 feat(web): add /discover route with search + request flow
Local-input + 250ms debounce drives Lidarr search; tab switch refetches
with new kind. Track-kind Request opens a confirm modal explaining the
album that will be added; Confirm fires createRequest, Cancel is a no-op.
Successful requests flip the card to 'requested' optimistically.
Shell nav now exposes /discover between Search and Playlists.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:15:52 -04:00
bvandeusen d27b381250 feat(web): add StatusPill semantic-color status indicator
Single-prop component mapping LidarrRequestStatus to voice-rule labels and
semantic tones (warning/info/success/error). Used by /requests and /admin/requests
in subsequent tasks.
2026-04-29 22:08:09 -04:00
bvandeusen dcb49e9687 fix(web): a11y on DiscoverResultCard + drop test-only inline styles
aria-labels now include the card title on all three button variants so
SR users navigating a grid can tell which card a button belongs to:
  - "Request <title>" / "<title> is already in library" / "<title>
    already requested"
The Kept pill gets role="status" so SR users hear the badge when it
appears (without aria-live's announce-on-mount noise).

The reserved-slot CSS (.badge-row { min-height: 22px } and
.actions { margin-top: auto }) was already in the scoped <style>
block; we drop the duplicate inline style="" attributes that existed
purely to satisfy jsdom's getComputedStyle. Tried stylesheet
introspection (document.styleSheets) as a replacement assertion, but
vitest's @testing-library/svelte renderer doesn't inject the scoped
<style> tag into jsdom (styleSheets.length === 0), so the two CSS
assertions are dropped with an explanatory comment. The layout
discipline is enforced visually at the consumer page rather than as
a "CSS exists in CSS" unit test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 21:05:27 -04:00
bvandeusen c8bd19d6f1 feat(web): add DiscoverResultCard with reserved badge slot + anchored button
T14 of the M5a Lidarr plan. The Discover grid in M5a renders mixed
requestable / kept / requested cards from Lidarr search. Without
layout discipline the cards in a row land their titles at different
Y coordinates whenever the badge presence varies, which reads as
visual noise. DiscoverResultCard enforces:

  - Flex column with `margin-top: auto` on `.actions`, so the action
    button is anchored to the bottom of the card body regardless of
    title/subtitle wrap differences.
  - `.badge-row` always rendered with `min-height: 22px`, so the
    title baseline holds even when no Kept pill is present.

Both load-bearing CSS values use inline style attributes — jsdom's
getComputedStyle does not resolve scoped <style> blocks, so the tests
verify positioning via inline styles directly. The remaining decorative
CSS (kept-pill background = accent at 15%, flex-direction, gap) lives
in a scoped <style> block.

State -> action mapping (sentence case per FabledSword voice):
  requestable -> bg-action-primary "Request" with Plus icon
  kept        -> disabled ghost "In library" + Kept pill
  requested   -> disabled ghost "Requested"

Cover art falls back to a Lucide glyph (Disc3 / Album / Music2) when
imageUrl is absent. Adds lucide-svelte@^1.0.1 dependency.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:27:58 -04:00
bvandeusen 13e12d97ae feat(web): add /settings page with ListenBrainz section
Adds api.put helper, ListenBrainz API module with query/mutation
helpers, a /settings route with token + enable/disable UI, and 5
tests. Adds Settings to the Shell nav.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 15:45:42 -04:00
bvandeusen 95d68e3d3d feat: M3.5 polish — genre splitting + radio button surface
Two carry-overs from M3 verification, bundled with the search-input
fix already on dev (b7a59a9).

1. Genre splitting in BuildSessionVector
   The library has many tracks whose genre tag is a denormalized
   multi-genre string ("Indie Pop; Pop; Alternative Pop"). Reading them
   as one opaque tag means a single-genre "Pop" track and a multi-genre
   track listing Pop both fail to share the Pop key, so the tags axis
   in similarity scoring (weight 0.7!) returned 0 in nearly all cases
   on real libraries. Result: contextual scoring couldn't differentiate.

   Add splitGenres() that splits on ; and , and trims whitespace;
   strings without a delimiter come back as a single-element slice
   (so the existing single-genre cases keep working unchanged).
   Concatenated-without-separator output ("ElectronicComplextroGlitch
   Hop") stays opaque — that needs a genre dictionary, out of scope.

2. Play-radio button on TrackRow
   playRadio() was only wired to the click handler on /search and
   /search/tracks, leaving /library/liked, album pages, and search
   results' inner clicks with no way to start a radio. Adds a small
   radio button to TrackRow (between LikeButton and the +queue button).
   Click → playRadio(track.id), with stopPropagation so the row's own
   activate handler doesn't also fire.

Tests:
- 3 new sessionvector tests: TestSplitGenres, multi-genre semicolon
  split, no-separator stays opaque. Existing single-genre tests pass
  unchanged.
- 1 new TrackRow test: radio button calls playRadio with track id and
  does NOT trigger row play.

Verified locally: go -short -race ./... clean, golangci-lint clean,
svelte-check 0/0, 175 vitest tests (was 174), web build succeeds.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 23:50:00 -04:00
bvandeusen b7a59a9473 fix(web): SearchInput passes keepFocus to goto() to prevent blur on debounce
The actual cause of the search-field focus loss: SvelteKit's `goto()`
defaults to resetting focus on navigation, so each debounced URL update
blurred the input mid-typing. Pass `keepFocus: true` to both `goto()`
calls in `navigate()`.

The earlier `bind:value` change in 6931358 didn't address this — it was
solving a hypothetical DOM-write-side cursor-jump issue rather than the
real focus-reset behavior. Leaving `bind:value` in place since it's
still cleaner Svelte 5.

Tests updated to match the new goto args.
2026-04-27 23:37:21 -04:00
bvandeusen 693135896e fix(web): SearchInput keeps focus across debounced URL updates
The previous one-way `{value}` + manual `oninput` rewrite let Svelte
re-set the input's DOM value whenever navigation completed, which moved
the cursor (and on some browsers, blurred the input) mid-typing. Switch
to Svelte 5's `bind:value` — it skips DOM writes when the JS value
already matches the DOM, so the user's typing isn't disturbed.

The URL-resync `\$effect` is unchanged: it still updates `value` for
back/forward navigation but is a no-op during normal typing because the
debounce already wrote the matching URL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 23:25:30 -04:00
bvandeusen de5320a7b4 feat(web): wire LikeButton into TrackRow, AlbumCard, ArtistRow, PlayerBar
Heart appears on every entity rendering surface. ArtistRow restructured
to a <div> with the link as a positioned overlay so the heart button
can nest without invalid <button>-in-<a> markup. Shell nav grows a
'Liked' entry pointing at /library/liked.
2026-04-26 16:56:07 -04:00
bvandeusen 6ab1fef07e feat(web): add LikeButton component
Heart icon reading from createLikedIdsQuery; click toggles via
likeEntity/unlikeEntity. Click stops propagation so it can nest
inside TrackRow's role=button div without triggering row activation.
2026-04-26 16:47:51 -04:00
bvandeusen ad9c984b62 feat(web): mount SearchInput in Shell header
Header grows a flex-1 column between the brand and the user menu.
Existing Shell tests remain green (the search input has no impact on
nav/menu behavior).
2026-04-25 16:12:19 -04:00
bvandeusen 3be0dc241c feat(web): add SearchSkeleton placeholder for the search page 2026-04-25 16:12:19 -04:00
bvandeusen 251d165e0b feat(web): add SearchInput header component (debounced URL sync)
Pre-fills from page.url.searchParams.get('q'); typing debounces 250ms
then goto('/search?q=…'); first nav from elsewhere pushes, subsequent
typing on /search replaces. Empty input on /search drops the param.
Escape clears the input. URL → value re-sync via \$effect uses
untrack() on the value comparison so it doesn't loop with typing.
2026-04-25 16:01:25 -04:00
bvandeusen a4a5a20308 feat(web): AlbumCard + queue overlay button
Wraps the existing link in a relative container; adds an absolutely
positioned + button. On click stops propagation, fetches /api/albums/:id
once, and feeds tracks into enqueueTracks.
2026-04-25 15:54:18 -04:00
bvandeusen 80b39f1713 feat(web): TrackRow div+role button with onPlay prop and + queue button
Outer <button> becomes <div role="button" tabindex="0"> with keyboard
handlers (Enter/Space) so a real <button> for + queue can nest without
invalid HTML. New optional onPlay prop overrides the default
playQueue(tracks, index); used by the search Tracks section to call
playRadio(track.id) instead. + queue button calls enqueueTrack and
stops propagation so the row click does not fire.
2026-04-25 15:37:11 -04:00
bvandeusen ccef9be2d3 feat(web): add PlayerBar slot to Shell
Grid grows to 3 rows (auto / 1fr / auto); PlayerBar spans both columns
in the last row, rendering only when player.current is defined so the
row collapses before first play.
2026-04-24 21:42:10 -04:00
bvandeusen ed082226a0 feat(web): add PlayerBar component
Bottom-bar UI with left: cover+title+linked artist, center: stacked
seek row (elapsed / slider / duration) + transport (prev/play/next),
right: shuffle/repeat/volume. Loading shows spinner; error replaces
transport with retry card.
2026-04-24 21:41:24 -04:00
bvandeusen 4bff8cc99a feat(web): make TrackRow click-to-play; pass tracks+index from album page
TrackRow becomes a <button> that dispatches playQueue(tracks, index).
Album page supplies the full track list + each index so the rest of
the album enqueues automatically when the user clicks a track.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 21:39:34 -04:00
bvandeusen f94bf26f02 feat(web): add ApiErrorBanner retry-capable error card 2026-04-23 18:42:22 -04:00
bvandeusen 9ed8e261d4 feat(web): add LibrarySkeleton with list/grid/album variants
Shimmer placeholders that mirror the layout of each library page so
the real content slides in without shifting.
2026-04-23 18:41:32 -04:00
bvandeusen 12d1cb739b feat(web): add TrackRow component
Non-interactive album track row with number, title, duration.
Player plan will add click-to-play.
2026-04-23 18:40:40 -04:00
bvandeusen e9b482da0c feat(web): add AlbumCard component
Grid card used on the artist detail page. Cover image, title, year.
Broken covers swap to FALLBACK_COVER via onerror.
2026-04-23 18:39:42 -04:00
bvandeusen a1252c1ef4 feat(web): add ArtistRow component
One row in the artists list: avatar initial, name, album count,
chevron. Entire row is an <a> for click + keyboard activation.
2026-04-23 18:18:52 -04:00
bvandeusen e1504f8e6c feat(web): add Shell component with header, sidebar, and user menu
Persistent chrome used by every authenticated route. Sidebar hides
below md:; mobile nav is a separate concern for a later plan.
2026-04-22 22:20:18 -04:00