feat(search): scoped live search on browse pages (#938)

* feat(artists): scoped live search badge replaces page filter

Move Artists browse text search into the header Live Search with a page
scope badge (Users icon), field-local undo, and double-click/backspace
to clear scope. Block the live-search dropdown while scoped so results
only filter the Artists grid; mobile overlay follows the same rules.

* fix(artists): plain grid for scoped search fixes broken card layout

Route the browse grid through VirtualCardGrid, switch to non-virtual CSS
grid when live search filters the catalog, reset scroll on filter changes,
and skip content-visibility on plain tiles to avoid blank/black cards.

* fix(search): scope badge double-Backspace and single clear control

Require two Backspaces on an empty scoped field after prior text input;
one Backspace still clears the badge when the field was never filled.
Move live-search clear/advanced controls inside the field pill, drop the
extra outer clear button, and use type=text to avoid native search clears.

* fix(search): drop duplicate outer live-search clear button

Keep the native in-field clear on type=search and the original pill layout;
remove only the extra × control outside the search border. Reset dropdown
state when the query is cleared via the native control.

* refactor(search): generic scoped browse query helper, drop dead code

Rename artistsBrowseSearchQuery to scopedBrowseSearchQuery with an
expectedScope argument; wire Artists via useScopedBrowseSearchQuery.
Remove unused liveSearchScoped dropdown helper (scoped mode blocks it).

* feat(search): ghost scope badge and single-click badge remove

After clearing the artists scope on /artists, show a faded ghost chip to
restore page-only search while keeping the global search placeholder.
Active badge removes on one click; tooltips and styles updated.

* feat(search): scoped live search for All Albums and New Releases

Wire albums and newReleases scope badges with debounced album title search
(local index title-only FTS + filtered search3). Plain grid, scroll reset,
and session query restore on album grid browse pages.

* fix(browse): preserve scroll restore after album/artist detail back

Only reset in-page scroll when filter resetKey changes, not when
isScrollRestorePending clears after session restore.

* feat(search): scoped live search for Tracks browse

Wire /tracks to header live search with wide title/artist/album FTS,
hide hero and discovery rails while search is active, and remove the
inline search field from the browse list.

* fix(search): clear header query when leaving scoped browse pages

Prevent global live search from firing on album/detail routes after a
scoped browse query; browse session stashes still restore on back.

* fix(tracks): restore scroll after back from detail during scoped search

Hold stashed song results across fetchSongPage churn, defer leave-stash
teardown past AppShell scroll reset, restore tracks scroll after the list
is ready, and save scroll snapshot when opening artist from song context menu.

* fix(tracks): hide discovery headings during scoped search

Hide the page subtitle and "Browse all tracks" section title when
tracks search is active, matching hero/rails chrome behavior.

* feat(search): scoped live search for Composers browse

Wire /composers to header live search with composers scope badge,
session stash, scroll restore, and plain grid/list during text filter.
Remove the in-page filter input; add i18n and navigation helpers.

* docs: CHANGELOG and credits for scoped browse live search (PR #938)
This commit is contained in:
cucadmuh
2026-06-01 13:04:36 +03:00
committed by GitHub
parent ddf10ee01d
commit 4ac373a65b
59 changed files with 2301 additions and 392 deletions
@@ -85,6 +85,12 @@
contain-intrinsic-size: 0 220px;
}
/* Text search / plain grid: no deferred paint — stale scroll + content-visibility blanks tiles. */
.album-grid-wrap--plain > .artist-card {
content-visibility: visible;
contain-intrinsic-size: auto;
}
@media (min-width: 1024px) {
.album-grid-wrap {
+84 -29
View File
@@ -22,18 +22,26 @@
cursor: text;
}
.live-search[data-collapsed]:not([data-active]) .live-search-icon {
left: 50%;
transform: translateX(-50%);
.live-search[data-collapsed]:not([data-active]) .live-search-field-cluster {
width: 34px;
height: 34px;
flex: 0 0 34px;
justify-content: center;
padding: 0;
border: none !important;
background: transparent !important;
box-shadow: none !important;
opacity: 1;
pointer-events: auto;
}
.live-search[data-collapsed]:not([data-active]) .live-search-field {
width: 0 !important;
min-width: 0 !important;
padding: 0 !important;
border: none !important;
opacity: 0;
pointer-events: none;
.live-search[data-collapsed]:not([data-active]) .live-search-field,
.live-search[data-collapsed]:not([data-active]) .live-search-scope-badge {
display: none;
}
.live-search[data-collapsed]:not([data-active]) .live-search-leading-icon {
margin: 0;
}
.live-search[data-collapsed][data-active] {
@@ -51,13 +59,18 @@
z-index: 25;
}
.live-search[data-collapsed][data-active] .live-search-field-cluster {
width: 100%;
opacity: 1;
pointer-events: auto;
}
.live-search[data-collapsed][data-active] .live-search-field {
width: 100%;
opacity: 1;
pointer-events: auto;
}
.live-search[data-collapsed]:not([data-active]) .live-search-clear,
.live-search[data-collapsed]:not([data-active]) .live-search-adv-btn {
opacity: 0;
pointer-events: none;
@@ -75,32 +88,75 @@
align-items: center;
}
.live-search-icon {
position: absolute;
left: 12px;
color: var(--text-muted);
pointer-events: none;
.live-search-field-cluster {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
background: var(--ctp-base);
border: 1px solid var(--ctp-overlay0);
border-radius: var(--radius-full);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background var(--transition-fast);
}
.live-search-field-cluster:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.live-search-leading-icon {
flex-shrink: 0;
display: flex;
align-items: center;
margin-left: 12px;
color: var(--text-muted);
pointer-events: none;
}
.live-search-scope-badge {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
margin-left: 6px;
border-radius: var(--radius-sm);
color: var(--accent);
background: color-mix(in srgb, var(--accent) 14%, transparent);
border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
cursor: pointer;
user-select: none;
}
.live-search-scope-badge--ghost {
opacity: 0.42;
border-style: dashed;
cursor: pointer;
transition: opacity var(--transition-fast), background var(--transition-fast), border-color var(--transition-fast);
}
.live-search-scope-badge--ghost:hover {
opacity: 0.9;
background: color-mix(in srgb, var(--accent) 12%, transparent);
border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}
.live-search-field {
padding-left: 36px !important;
padding: var(--space-3) var(--space-4) !important;
padding-left: 8px !important;
padding-right: 58px !important;
border: none !important;
background: transparent !important;
box-shadow: none !important;
border-radius: var(--radius-full) !important;
flex: 1;
min-width: 0;
}
.live-search-clear {
position: absolute;
right: 8px;
font-size: 18px;
color: var(--text-muted);
line-height: 1;
transition: color var(--transition-fast);
}
.live-search-clear:hover {
color: var(--text-primary);
.live-search-field:focus {
border-color: transparent !important;
box-shadow: none !important;
}
.live-search-adv-btn {
@@ -249,4 +305,3 @@
color: var(--text-muted);
font-size: 13px;
}
+24
View File
@@ -34,6 +34,30 @@
flex-shrink: 0;
}
.mobile-search-scope-badge {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: var(--radius-sm);
color: var(--accent);
background: color-mix(in srgb, var(--accent) 14%, transparent);
border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
user-select: none;
}
.mobile-search-scope-badge--ghost {
opacity: 0.42;
border-style: dashed;
cursor: pointer;
}
.mobile-search-field--scoped .mobile-search-input {
font-size: 16px;
}
.mobile-search-input {
flex: 1;
background: none;