From 627b159d23ad3d4f5f873c37d489be13afcc5cce Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:19:17 -0500 Subject: [PATCH 001/142] feat(25-01): add latest and next release dates to GET /watchlist - two LEFT JOIN LATERAL ... LIMIT 1 joins in ListWatchlist and GetWatchlistEntry - upcoming decided in Postgres at the date's own precision against UTC today - Entry.LatestReleaseDate / NextReleaseDate as always-present nullable JSON keys --- internal/db/sqlc/querier.go | 6 ++ internal/db/sqlc/watchlist.sql.go | 106 ++++++++++++++++++-------- internal/httpserver/watchlist_test.go | 62 +++++++++++++++ internal/watchlist/service.go | 56 +++++++------- queries/watchlist.sql | 42 +++++++++- 5 files changed, 214 insertions(+), 58 deletions(-) diff --git a/internal/db/sqlc/querier.go b/internal/db/sqlc/querier.go index 366cc95..efd49c2 100644 --- a/internal/db/sqlc/querier.go +++ b/internal/db/sqlc/querier.go @@ -98,6 +98,9 @@ type Querier interface { GetWatchlistArtistID(ctx context.Context, id int64) (int64, error) // ListWatchlist's select list narrowed to one row (D-26); Service.get only // compiles while the projections stay identical. + // latest/upcoming new_release dates (G1-G3): no retention window; Postgres + // decides "upcoming" at the date's own precision against UTC today. Plain + // columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. GetWatchlistEntry(ctx context.Context, id int64) (GetWatchlistEntryRow, error) // D-14's implicit seed-mode check, scoped per-source per D-15: zero // existing event rows for this artist+source means seed mode. @@ -225,6 +228,9 @@ type Querier interface { // tag_ids/tag_names (D-26): parallel ARRAY(...) subqueries, not json_agg // (sqlc#3438 emits interface{}). They must stay identical but for the column, // or zipTags would mispair ids and names. + // latest/upcoming new_release dates (G1-G3): no retention window; Postgres + // decides "upcoming" at the date's own precision against UTC today. Plain + // columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. ListWatchlist(ctx context.Context) ([]ListWatchlistRow, error) // Merge statement order and why it never inserts: ADR 0004. // Locks both tags in id order so concurrent merges cannot deadlock; it also diff --git a/internal/db/sqlc/watchlist.sql.go b/internal/db/sqlc/watchlist.sql.go index b9506a1..d11781c 100644 --- a/internal/db/sqlc/watchlist.sql.go +++ b/internal/db/sqlc/watchlist.sql.go @@ -90,31 +90,52 @@ SELECT w.id AS id, a.id AS artist_id, a.mbid, a.name, a.deezer_id, ARRAY( SELECT t.name FROM artist_tags link JOIN tags t ON t.id = link.tag_id WHERE link.artist_id = a.id ORDER BY lower(t.name), t.id - )::text[] AS tag_names + )::text[] AS tag_names, + latest.release_date AS latest_release_date, + upcoming.release_date AS next_release_date FROM watchlist w JOIN artists a ON a.id = w.artist_id +LEFT JOIN LATERAL ( + SELECT e.release_date FROM events e + WHERE e.artist_id = a.id AND e.event_type = 'new_release' + AND e.release_date IS NOT NULL + AND e.release_date <= left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) + ORDER BY e.release_date DESC LIMIT 1 +) latest ON true +LEFT JOIN LATERAL ( + SELECT e.release_date FROM events e + WHERE e.artist_id = a.id AND e.event_type = 'new_release' + AND e.release_date IS NOT NULL + AND e.release_date > left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) + ORDER BY e.release_date ASC LIMIT 1 +) upcoming ON true WHERE w.id = $1 ` type GetWatchlistEntryRow struct { - ID int64 `json:"id"` - ArtistID int64 `json:"artist_id"` - Mbid string `json:"mbid"` - Name string `json:"name"` - DeezerID *string `json:"deezer_id"` - Disambiguation *string `json:"disambiguation"` - ImageUrl *string `json:"image_url"` - ReleaseTypes []string `json:"release_types"` - MutedEventTypes []string `json:"muted_event_types"` - Note *string `json:"note"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` - TagIds []int64 `json:"tag_ids"` - TagNames []string `json:"tag_names"` + ID int64 `json:"id"` + ArtistID int64 `json:"artist_id"` + Mbid string `json:"mbid"` + Name string `json:"name"` + DeezerID *string `json:"deezer_id"` + Disambiguation *string `json:"disambiguation"` + ImageUrl *string `json:"image_url"` + ReleaseTypes []string `json:"release_types"` + MutedEventTypes []string `json:"muted_event_types"` + Note *string `json:"note"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + TagIds []int64 `json:"tag_ids"` + TagNames []string `json:"tag_names"` + LatestReleaseDate *string `json:"latest_release_date"` + NextReleaseDate *string `json:"next_release_date"` } // ListWatchlist's select list narrowed to one row (D-26); Service.get only // compiles while the projections stay identical. +// latest/upcoming new_release dates (G1-G3): no retention window; Postgres +// decides "upcoming" at the date's own precision against UTC today. Plain +// columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. func (q *Queries) GetWatchlistEntry(ctx context.Context, id int64) (GetWatchlistEntryRow, error) { row := q.db.QueryRow(ctx, getWatchlistEntry, id) var i GetWatchlistEntryRow @@ -133,6 +154,8 @@ func (q *Queries) GetWatchlistEntry(ctx context.Context, id int64) (GetWatchlist &i.UpdatedAt, &i.TagIds, &i.TagNames, + &i.LatestReleaseDate, + &i.NextReleaseDate, ) return i, err } @@ -148,27 +171,45 @@ SELECT w.id AS id, a.id AS artist_id, a.mbid, a.name, a.deezer_id, ARRAY( SELECT t.name FROM artist_tags link JOIN tags t ON t.id = link.tag_id WHERE link.artist_id = a.id ORDER BY lower(t.name), t.id - )::text[] AS tag_names + )::text[] AS tag_names, + latest.release_date AS latest_release_date, + upcoming.release_date AS next_release_date FROM watchlist w JOIN artists a ON a.id = w.artist_id +LEFT JOIN LATERAL ( + SELECT e.release_date FROM events e + WHERE e.artist_id = a.id AND e.event_type = 'new_release' + AND e.release_date IS NOT NULL + AND e.release_date <= left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) + ORDER BY e.release_date DESC LIMIT 1 +) latest ON true +LEFT JOIN LATERAL ( + SELECT e.release_date FROM events e + WHERE e.artist_id = a.id AND e.event_type = 'new_release' + AND e.release_date IS NOT NULL + AND e.release_date > left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) + ORDER BY e.release_date ASC LIMIT 1 +) upcoming ON true ORDER BY a.name ASC, a.id ASC ` type ListWatchlistRow struct { - ID int64 `json:"id"` - ArtistID int64 `json:"artist_id"` - Mbid string `json:"mbid"` - Name string `json:"name"` - DeezerID *string `json:"deezer_id"` - Disambiguation *string `json:"disambiguation"` - ImageUrl *string `json:"image_url"` - ReleaseTypes []string `json:"release_types"` - MutedEventTypes []string `json:"muted_event_types"` - Note *string `json:"note"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` - TagIds []int64 `json:"tag_ids"` - TagNames []string `json:"tag_names"` + ID int64 `json:"id"` + ArtistID int64 `json:"artist_id"` + Mbid string `json:"mbid"` + Name string `json:"name"` + DeezerID *string `json:"deezer_id"` + Disambiguation *string `json:"disambiguation"` + ImageUrl *string `json:"image_url"` + ReleaseTypes []string `json:"release_types"` + MutedEventTypes []string `json:"muted_event_types"` + Note *string `json:"note"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + TagIds []int64 `json:"tag_ids"` + TagNames []string `json:"tag_names"` + LatestReleaseDate *string `json:"latest_release_date"` + NextReleaseDate *string `json:"next_release_date"` } // Both watchlist and artists have a column named id -- every selected @@ -182,6 +223,9 @@ type ListWatchlistRow struct { // tag_ids/tag_names (D-26): parallel ARRAY(...) subqueries, not json_agg // (sqlc#3438 emits interface{}). They must stay identical but for the column, // or zipTags would mispair ids and names. +// latest/upcoming new_release dates (G1-G3): no retention window; Postgres +// decides "upcoming" at the date's own precision against UTC today. Plain +// columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. func (q *Queries) ListWatchlist(ctx context.Context) ([]ListWatchlistRow, error) { rows, err := q.db.Query(ctx, listWatchlist) if err != nil { @@ -206,6 +250,8 @@ func (q *Queries) ListWatchlist(ctx context.Context) ([]ListWatchlistRow, error) &i.UpdatedAt, &i.TagIds, &i.TagNames, + &i.LatestReleaseDate, + &i.NextReleaseDate, ); err != nil { return nil, err } diff --git a/internal/httpserver/watchlist_test.go b/internal/httpserver/watchlist_test.go index 3e66a9f..c65b752 100644 --- a/internal/httpserver/watchlist_test.go +++ b/internal/httpserver/watchlist_test.go @@ -2018,3 +2018,65 @@ func TestWatchlist_Patch_ReturnsTagsAndNote(t *testing.T) { // -- kept file-local since it is unexported and this is a different // package. func ptr(s string) *string { return &s } + +// TestWatchlist_List_LatestRelease proves GET /watchlist carries the latest +// and next release dates from its single query, with null (not a 500) for an +// artist without releases (WLVW-03, G1-G3). +func TestWatchlist_List_LatestRelease(t *testing.T) { + pool := testutil.NewIsolatedTestPool(t, "watchlist_release_http_test") + ctx := context.Background() + + watchlistSvc := watchlist.NewService(sqlc.New(pool)) + srv := httpserver.New(pool, watchlistSvc, stubEventsStore{}, nil, discardLogger()) + ts := httptest.NewServer(srv.Router()) + defer ts.Close() + + released, err := watchlistSvc.Add(ctx, watchlist.AddParams{MBID: testMBID(t) + "-released", Name: "Released Artist"}) + if err != nil { + t.Fatalf("seed released entry: %v", err) + } + quiet, err := watchlistSvc.Add(ctx, watchlist.AddParams{MBID: testMBID(t) + "-quiet", Name: "Quiet Artist"}) + if err != nil { + t.Fatalf("seed quiet entry: %v", err) + } + insertTestEventWithDate(t, pool, released.ArtistID, testMBID(t)+"-past", datePtr("1999-05-17")) + insertTestEventWithDate(t, pool, released.ArtistID, testMBID(t)+"-future", datePtr("2999")) + + resp, err := http.Get(ts.URL + "/watchlist") + if err != nil { + t.Fatalf("GET /watchlist: %v", err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK) + } + var entries []map[string]any + if err := json.NewDecoder(resp.Body).Decode(&entries); err != nil { + t.Fatalf("decode: %v", err) + } + + byID := map[int64]map[string]any{} + for _, e := range entries { + byID[int64(e["id"].(float64))] = e + } + rel, ok := byID[released.ID] + if !ok { + t.Fatalf("released entry %d missing from list", released.ID) + } + if got := rel["latest_release_date"]; got != "1999-05-17" { + t.Errorf("latest_release_date = %v, want 1999-05-17", got) + } + if got := rel["next_release_date"]; got != "2999" { + t.Errorf("next_release_date = %v, want 2999", got) + } + q, ok := byID[quiet.ID] + if !ok { + t.Fatalf("quiet entry %d missing from list", quiet.ID) + } + for _, key := range []string{"latest_release_date", "next_release_date"} { + v, present := q[key] + if !present || v != nil { + t.Errorf("quiet %s = %v (present=%v), want present null", key, v, present) + } + } +} diff --git a/internal/watchlist/service.go b/internal/watchlist/service.go index 6c85ecc..10bb662 100644 --- a/internal/watchlist/service.go +++ b/internal/watchlist/service.go @@ -83,19 +83,21 @@ type TagRef struct { // Entry is the API-facing joined artist + watchlist row. type Entry struct { - ID int64 `json:"id"` - ArtistID int64 `json:"artist_id"` - MBID string `json:"mbid"` - Name string `json:"name"` - DeezerID *string `json:"deezer_id"` - Disambiguation *string `json:"disambiguation"` - ImageURL *string `json:"image_url"` - ReleaseTypes []string `json:"release_types"` - MutedEventTypes []string `json:"muted_event_types"` - Tags []TagRef `json:"tags"` - Note *string `json:"note"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int64 `json:"id"` + ArtistID int64 `json:"artist_id"` + MBID string `json:"mbid"` + Name string `json:"name"` + DeezerID *string `json:"deezer_id"` + Disambiguation *string `json:"disambiguation"` + ImageURL *string `json:"image_url"` + ReleaseTypes []string `json:"release_types"` + MutedEventTypes []string `json:"muted_event_types"` + Tags []TagRef `json:"tags"` + Note *string `json:"note"` + LatestReleaseDate *string `json:"latest_release_date"` + NextReleaseDate *string `json:"next_release_date"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // AddParams carries the fields needed to add a new artist to the @@ -346,19 +348,21 @@ func entryFromRow(row sqlc.ListWatchlistRow) (Entry, error) { return Entry{}, err } return Entry{ - ID: row.ID, - ArtistID: row.ArtistID, - MBID: row.Mbid, - Name: row.Name, - DeezerID: row.DeezerID, - Disambiguation: row.Disambiguation, - ImageURL: row.ImageUrl, - ReleaseTypes: row.ReleaseTypes, - MutedEventTypes: row.MutedEventTypes, - Tags: tags, - Note: row.Note, - CreatedAt: row.CreatedAt.Time, - UpdatedAt: row.UpdatedAt.Time, + ID: row.ID, + ArtistID: row.ArtistID, + MBID: row.Mbid, + Name: row.Name, + DeezerID: row.DeezerID, + Disambiguation: row.Disambiguation, + ImageURL: row.ImageUrl, + ReleaseTypes: row.ReleaseTypes, + MutedEventTypes: row.MutedEventTypes, + Tags: tags, + Note: row.Note, + LatestReleaseDate: row.LatestReleaseDate, + NextReleaseDate: row.NextReleaseDate, + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, }, nil } diff --git a/queries/watchlist.sql b/queries/watchlist.sql index f8487f8..627cd21 100644 --- a/queries/watchlist.sql +++ b/queries/watchlist.sql @@ -27,9 +27,28 @@ SELECT w.id AS id, a.id AS artist_id, a.mbid, a.name, a.deezer_id, ARRAY( SELECT t.name FROM artist_tags link JOIN tags t ON t.id = link.tag_id WHERE link.artist_id = a.id ORDER BY lower(t.name), t.id - )::text[] AS tag_names + )::text[] AS tag_names, + latest.release_date AS latest_release_date, + upcoming.release_date AS next_release_date FROM watchlist w JOIN artists a ON a.id = w.artist_id +-- latest/upcoming new_release dates (G1-G3): no retention window; Postgres +-- decides "upcoming" at the date's own precision against UTC today. Plain +-- columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. +LEFT JOIN LATERAL ( + SELECT e.release_date FROM events e + WHERE e.artist_id = a.id AND e.event_type = 'new_release' + AND e.release_date IS NOT NULL + AND e.release_date <= left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) + ORDER BY e.release_date DESC LIMIT 1 +) latest ON true +LEFT JOIN LATERAL ( + SELECT e.release_date FROM events e + WHERE e.artist_id = a.id AND e.event_type = 'new_release' + AND e.release_date IS NOT NULL + AND e.release_date > left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) + ORDER BY e.release_date ASC LIMIT 1 +) upcoming ON true ORDER BY a.name ASC, a.id ASC; -- name: GetWatchlistEntry :one @@ -45,9 +64,28 @@ SELECT w.id AS id, a.id AS artist_id, a.mbid, a.name, a.deezer_id, ARRAY( SELECT t.name FROM artist_tags link JOIN tags t ON t.id = link.tag_id WHERE link.artist_id = a.id ORDER BY lower(t.name), t.id - )::text[] AS tag_names + )::text[] AS tag_names, + latest.release_date AS latest_release_date, + upcoming.release_date AS next_release_date FROM watchlist w JOIN artists a ON a.id = w.artist_id +-- latest/upcoming new_release dates (G1-G3): no retention window; Postgres +-- decides "upcoming" at the date's own precision against UTC today. Plain +-- columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. +LEFT JOIN LATERAL ( + SELECT e.release_date FROM events e + WHERE e.artist_id = a.id AND e.event_type = 'new_release' + AND e.release_date IS NOT NULL + AND e.release_date <= left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) + ORDER BY e.release_date DESC LIMIT 1 +) latest ON true +LEFT JOIN LATERAL ( + SELECT e.release_date FROM events e + WHERE e.artist_id = a.id AND e.event_type = 'new_release' + AND e.release_date IS NOT NULL + AND e.release_date > left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) + ORDER BY e.release_date ASC LIMIT 1 +) upcoming ON true WHERE w.id = $1; -- name: UpdateWatchlistNote :execrows From a5779706caf186f711ffa604cfd44a4e1c80c490 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:20:29 -0500 Subject: [PATCH 002/142] test(25-01): pin latest/next release semantics and shared projection - table of subtests for precision, event types, age, null dates and UTC-year boundary - ProjectionParity now seeds releases and asserts both fields across Add/Update/UpdateNote --- internal/watchlist/service_test.go | 111 +++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/internal/watchlist/service_test.go b/internal/watchlist/service_test.go index cc516c6..3f19add 100644 --- a/internal/watchlist/service_test.go +++ b/internal/watchlist/service_test.go @@ -16,6 +16,7 @@ import ( "log/slog" "reflect" "slices" + "strconv" "strings" "testing" "time" @@ -2346,10 +2347,27 @@ func TestService_ProjectionParity(t *testing.T) { svc := watchlist.NewService(sqlc.New(pool)) mbid := testMBID(t) + // Pre-seed the artist and its releases so Add's returned entry already + // carries both date fields (Add upserts the existing artist). + var artistID int64 + if err := pool.QueryRow(ctx, + `INSERT INTO artists (mbid, name) VALUES ($1, 'Projection Parity Test') RETURNING id`, mbid, + ).Scan(&artistID); err != nil { + t.Fatalf("seed artist: %v", err) + } + insertReleaseEvent(t, pool, artistID, mbid+"-past", "new_release", ptr("1999-05-17"), time.Now()) + insertReleaseEvent(t, pool, artistID, mbid+"-next", "new_release", ptr("2999"), time.Now()) + added, err := svc.Add(ctx, watchlist.AddParams{MBID: mbid, Name: "Projection Parity Test", Note: ptr("first")}) if err != nil { t.Fatalf("Add: %v", err) } + if added.LatestReleaseDate == nil || *added.LatestReleaseDate != "1999-05-17" { + t.Fatalf("Add LatestReleaseDate = %v, want 1999-05-17", added.LatestReleaseDate) + } + if added.NextReleaseDate == nil || *added.NextReleaseDate != "2999" { + t.Fatalf("Add NextReleaseDate = %v, want 2999", added.NextReleaseDate) + } listAfterAdd, err := svc.List(ctx) if err != nil { t.Fatalf("List after Add: %v", err) @@ -2383,6 +2401,99 @@ func TestService_ProjectionParity(t *testing.T) { } } +// insertReleaseEvent seeds one events row; externalID must be unique per +// (event_type, source, external_id). +func insertReleaseEvent(t *testing.T, pool *pgxpool.Pool, artistID int64, externalID, eventType string, releaseDate *string, createdAt time.Time) { + t.Helper() + _, err := pool.Exec(context.Background(), ` + INSERT INTO events (artist_id, source, event_type, external_id, title, artist_name, release_date, created_at) + VALUES ($1, 'musicbrainz', $2, $3, 'Title', 'Artist', $4, $5)`, + artistID, eventType, externalID, releaseDate, createdAt) + if err != nil { + t.Fatalf("insert %s event %q: %v", eventType, externalID, err) + } +} + +type releaseSeed struct { + eventType string + date *string + age time.Duration +} + +// TestService_List_LatestRelease pins G1-G3: latest/next come from new_release +// events only, at any age, with "upcoming" decided at the date's own precision. +// Only the year-boundary case reads the clock; fixed far dates cover the rest. +func TestService_List_LatestRelease(t *testing.T) { + pool := testutil.NewIsolatedTestPool(t, "watchlist_release_test") + ctx := context.Background() + svc := watchlist.NewService(sqlc.New(pool)) + + year := time.Now().UTC().Year() + thisYear := strconv.Itoa(year) + nextYear := strconv.Itoa(year + 1) + nr := "new_release" + + tests := []struct { + name string + seeds []releaseSeed + wantLatest *string + wantNext *string + }{ + {"only past", []releaseSeed{{nr, ptr("1999"), 0}}, ptr("1999"), nil}, + {"only upcoming", []releaseSeed{{nr, ptr("2999-05"), 0}}, nil, ptr("2999-05")}, + {"past mixed precision", []releaseSeed{{nr, ptr("1999"), 0}, {nr, ptr("1999-05-17"), 0}, {nr, ptr("1999-06"), 0}}, ptr("1999-06"), nil}, + {"upcoming mixed precision", []releaseSeed{{nr, ptr("3000-01"), 0}, {nr, ptr("2999-12-25"), 0}, {nr, ptr("2999"), 0}}, nil, ptr("2999")}, + {"past and upcoming together", []releaseSeed{{nr, ptr("1999-05-17"), 0}, {nr, ptr("2999"), 0}}, ptr("1999-05-17"), ptr("2999")}, + {"guest_feature and deluxe_change ignored", []releaseSeed{{"guest_feature", ptr("1999-12-31"), 0}, {"deluxe_change", ptr("1999-12-30"), 0}}, nil, nil}, + {"null-dated new_release ignored", []releaseSeed{{nr, nil, 0}}, nil, nil}, + {"old event still counts (no retention)", []releaseSeed{{nr, ptr("1999-01-01"), 400 * 24 * time.Hour}}, ptr("1999-01-01"), nil}, + {"current UTC year is latest, next year is next", []releaseSeed{{nr, ptr(thisYear), 0}, {nr, ptr(nextYear), 0}}, ptr(thisYear), ptr(nextYear)}, + {"zero events", nil, nil, nil}, + } + + for i, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mbid := testMBID(t) + entry, err := svc.Add(ctx, watchlist.AddParams{MBID: mbid, Name: "Release Case " + strconv.Itoa(i)}) + if err != nil { + t.Fatalf("Add: %v", err) + } + for j, sd := range tc.seeds { + insertReleaseEvent(t, pool, entry.ArtistID, mbid+"-"+strconv.Itoa(j), sd.eventType, sd.date, time.Now().Add(-sd.age)) + } + + list, err := svc.List(ctx) + if err != nil { + t.Fatalf("List: %v", err) + } + got := findEntry(list, entry.ID) + if got == nil { + t.Fatalf("entry %d missing from List", entry.ID) + } + assertDate(t, "LatestReleaseDate", got.LatestReleaseDate, tc.wantLatest) + assertDate(t, "NextReleaseDate", got.NextReleaseDate, tc.wantNext) + }) + } +} + +func assertDate(t *testing.T, field string, got, want *string) { + t.Helper() + switch { + case got == nil && want == nil: + case got == nil || want == nil: + t.Errorf("%s = %v, want %v", field, deref(got), deref(want)) + case *got != *want: + t.Errorf("%s = %q, want %q", field, *got, *want) + } +} + +func deref(s *string) string { + if s == nil { + return "" + } + return *s +} + // findEntry returns a pointer to the entry with the given id in entries, or // nil if absent. func findEntry(entries []watchlist.Entry, id int64) *watchlist.Entry { From a20968dd9c4fca15a77dc061c4f41d8c3c05b0a3 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:22:18 -0500 Subject: [PATCH 003/142] docs(25-01): add plan summary --- .../25-01-SUMMARY.md | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 .planning/phases/25-find-filter-watchlist-and-history/25-01-SUMMARY.md diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-01-SUMMARY.md b/.planning/phases/25-find-filter-watchlist-and-history/25-01-SUMMARY.md new file mode 100644 index 0000000..f031502 --- /dev/null +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-01-SUMMARY.md @@ -0,0 +1,133 @@ +--- +phase: 25-find-filter-watchlist-and-history +plan: 01 +subsystem: api +tags: [postgres, sqlc, lateral-join, watchlist, go] + +requires: + - phase: 24-artist-tags-and-notes + provides: "shared ListWatchlist/GetWatchlistEntry projection (D-26) and Entry shape" +provides: + - "latest_release_date and next_release_date on every GET /watchlist entry, from the single ListWatchlist statement" + - "Same two values on Add/UpdatePreferences/UpdateNote responses via the shared projection" +affects: [25-03 card rendering, 25-05 latest-release sort] + +actuals: + tokens: 5900 + tasks: 2 + commits: 2 +plan_head_before: 72f26dac1e2d0086b90bb9e5230fc6a4a23abfa3 + +tech-stack: + added: [] + patterns: + - "Two LEFT JOIN LATERAL ... LIMIT 1 plain-column joins (not aggregates) so sqlc emits *string and NULL scans cleanly" + - "Upcoming decided in SQL by comparing against UTC today truncated to the stored value's own length" + +key-files: + created: [] + modified: + - queries/watchlist.sql + - internal/db/sqlc/watchlist.sql.go + - internal/db/sqlc/querier.go + - internal/watchlist/service.go + - internal/watchlist/service_test.go + - internal/httpserver/watchlist_test.go + +key-decisions: + - "No retention parameter: ListWatchlist keeps its zero-arg signature and NewService is unchanged (G1)" + - "Date fields have no omitempty so a missing release serializes as a present null key" + +requirements-completed: [WLVW-03] + +coverage: + - id: D1 + description: "GET /watchlist returns latest_release_date and next_release_date per entry from one query, null (with 200) for artists without releases" + requirement: WLVW-03 + verification: + - kind: integration + ref: "internal/httpserver/watchlist_test.go#TestWatchlist_List_LatestRelease" + status: pass + human_judgment: false + - id: D2 + description: "Precision, mixed-precision ordering, event-type filtering, no-retention age and UTC-year boundary semantics" + requirement: WLVW-03 + verification: + - kind: integration + ref: "internal/watchlist/service_test.go#TestService_List_LatestRelease" + status: pass + human_judgment: false + - id: D3 + description: "Add, UpdatePreferences and UpdateNote return the same two dates as the matching List element" + requirement: WLVW-03 + verification: + - kind: integration + ref: "internal/watchlist/service_test.go#TestService_ProjectionParity" + status: pass + human_judgment: false + +duration: 25min +completed: 2026-10-10 +status: complete +--- + +# Phase 25 Plan 01: Latest and Next Release Dates Summary + +**GET /watchlist now carries `latest_release_date` and `next_release_date` per entry, computed by two `LEFT JOIN LATERAL ... LIMIT 1` joins inside the single ListWatchlist statement, with Postgres deciding "upcoming" at each date's own precision against UTC today.** + +## Performance + +- **Duration:** about 25 min +- **Completed:** 2026-10-10T16:22Z +- **Tasks:** 2 +- **Files modified:** 6 + +## Accomplishments +- Both ListWatchlist and GetWatchlistEntry gained identical `latest`/`upcoming` lateral joins; sqlc regenerated `LatestReleaseDate`/`NextReleaseDate` as `*string` on both row types, so the `sqlc.ListWatchlistRow(row)` conversion still compiles. +- `watchlist.Entry` exposes the two fields as always-present nullable JSON keys; no retention setting was added (G1). +- Service-level tests pin past/upcoming split, mixed precision (`1999-06` over `1999-05-17` over `1999`; `2999` over `2999-12-25` over `3000-01`), non-release event types and NULL dates being ignored, 400-day-old events still counting, and the current UTC year / next year boundary. +- `TestService_ProjectionParity` now seeds releases and asserts the added entry and every later write path equal the List element. + +## Task Commits + +1. **Task 1: GET /watchlist carries latest and next release dates (tracer)** - `627b159` (feat) +2. **Task 2: Pin latest/upcoming semantics and shared projection** - `a577970` (test) + +**Plan metadata:** committed separately after this summary (docs). + +## Files Created/Modified +- `queries/watchlist.sql` - two lateral joins and two selected columns on ListWatchlist and GetWatchlistEntry +- `internal/db/sqlc/watchlist.sql.go`, `internal/db/sqlc/querier.go` - regenerated by sqlc +- `internal/watchlist/service.go` - `Entry.LatestReleaseDate` / `NextReleaseDate`, mapped in `entryFromRow` +- `internal/httpserver/watchlist_test.go` - `TestWatchlist_List_LatestRelease` +- `internal/watchlist/service_test.go` - `insertReleaseEvent`, `TestService_List_LatestRelease`, extended `TestService_ProjectionParity` + +## Decisions Made +None beyond the plan (G1-G3 followed as specified). + +## Deviations from Plan + +### Process notes + +**1. [Process] Task 2 tests passed on first run (no RED phase)** +- The tracer in Task 1 already implemented the full SQL semantics, so the Task 2 pinning tests exercised existing behavior and were green immediately. They are regression pins, not new behavior; there is no separate failing-test commit. + +**2. [Environment] Race detector unavailable** +- `make test` runs `go test -race`, but this box has no cgo (`-race requires cgo`). The full suite was run without `-race` using the same coverage flags: all 25 packages passed, backend coverage 90.18% (gate 80%). + +**Total deviations:** 0 code deviations, 2 environment/process notes. **Impact:** none on correctness. + +## Issues Encountered +None. + +## Known Stubs +None. + +## Threat Flags +None. The new fields are read-only values from the existing events table behind the existing auth gate (T-25-01..04 handled as planned: LIMIT 1 laterals in one statement, plain-column laterals so NULL scans, no new parameters). + +## Self-Check: PASSED + +- Acceptance criteria: 4 `LEFT JOIN LATERAL` lines; `LatestReleaseDate *string` x2, `NextReleaseDate *string` x2; `ListWatchlist(ctx context.Context) (` signature unchanged; each JSON tag present once; 10 subtest PASS lines in `TestService_List_LatestRelease`. +- Plan verification: `go build ./...`, `go vet ./...`, `golangci-lint run` (0 issues), sqlc regenerate + `git diff --exit-code -- internal/db/sqlc/` clean, full test suite green, coverage 90.18%. +- Commits `627b159` and `a577970` exist on `gsd/phase-25-find-filter-watchlist-and-history`. From f6687a68fcafd1397bb02e8c2c130c9cacf80d69 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:22:32 -0500 Subject: [PATCH 004/142] docs(25-01): update state and roadmap for completed plan --- .planning/ROADMAP.md | 6 +++--- .planning/STATE.md | 28 +++++++++++++++------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 4b4f1c3..144896f 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -193,12 +193,12 @@ Plans: 4. The user can filter History by a tag alongside the existing filters. It lists events for artists carrying that tag, including artists since removed from the watchlist, and it never shows an event older than the retention window, either on the first page or via "load older". 5. Opening the Watchlist makes one request regardless of watchlist size. Searching, sorting, and filtering then happen instantly with no further requests. -**Plans:** 10 plans +**Plans:** 1/10 plans executed Plans: **Wave 1** -- [ ] 25-01-PLAN.md — GET /watchlist gains `latest_release_date` and `next_release_date` from two lateral joins in its single query (G1–G3), with semantics and projection-parity tests +- [x] 25-01-PLAN.md — GET /watchlist gains `latest_release_date` and `next_release_date` from two lateral joins in its single query (G1–G3), with semantics and projection-parity tests - [ ] 25-03-PLAN.md — Cards show `Latest release: {date}` / `No releases yet` and `Upcoming: {date}`, `formatReleaseDate`, and the `makeEntry` fixture builder **Wave 2** *(blocked on Wave 1 completion)* @@ -327,7 +327,7 @@ Phases execute in numeric order: 24 → 25 → 26 → 27 → 28 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 24. Artist Tags & Notes | 14/14 | Complete | 2026-10-06 | -| 25. Find & Filter — Watchlist and History | 0/? | Not started | - | +| 25. Find & Filter — Watchlist and History | 1/10 | In Progress| | | 26. Bulk Edit & Remove | 0/? | Not started | - | | 27. Paste-a-List Bulk Add | 0/? | Not started | - | | 28. Tags on Discord Notifications | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 0e16633..fd77945 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,17 +4,17 @@ milestone: v1.6 milestone_name: Watchlist Organization current_phase: 25 current_phase_name: Find & Filter — Watchlist and History -status: planning -stopped_at: Phase 24 complete, ready to plan Phase 25 -last_updated: "2026-10-06T21:40:10.711Z" -last_activity: 2026-10-06 -last_activity_desc: Phase 24 complete, transitioned to Phase 25 -state_head: 43f00c104424775675eecba8545aeb3045255c0f +status: executing +stopped_at: Completed 25-01-PLAN.md +last_updated: "2026-10-10T16:22:28.176Z" +last_activity: 2026-10-10 +last_activity_desc: Phase 25 execution started +state_head: a20968dd9c4fca15a77dc061c4f41d8c3c05b0a3 progress: total_phases: 5 completed_phases: 1 total_plans: 24 - completed_plans: 14 + completed_plans: 15 percent: 20 --- @@ -29,10 +29,10 @@ See: .planning/PROJECT.md (updated 2026-10-06) ## Current Position -Phase: 25 — Find & Filter — Watchlist and History -Plan: Not started -Status: Ready to plan -Last activity: 2026-10-07 - Completed quick task 261007-lc4: Phase 24 comment cleanup +Phase: 25 (Find & Filter — Watchlist and History) — EXECUTING +Plan: 2 of 10 +Status: Ready to execute +Last activity: 2026-10-10 — Phase 25 execution started ## Performance Metrics @@ -162,6 +162,7 @@ Last activity: 2026-10-07 - Completed quick task 261007-lc4: Phase 24 comment cl | Phase 24 P12 | 6 min | 3 tasks | 4 files | | Phase 24 P13 | 7 min | 2 tasks | 4 files | | Phase 24 P14 | 20 min | 3 tasks | 5 files | +| Phase 25 P01 | 25 min | 2 tasks | 6 files | ## Accumulated Context @@ -389,6 +390,7 @@ Recent decisions affecting current work: - [Phase 24]: 24-12: ManageTagsDialog guards load() with a generation ref; invalidateLoad() on delete/rename/merge success bumps it and refetches only when a load was in flight (no skeleton strand) - [Phase 24]: 24-13: wrap-anywhere on ConfirmDialog title/description (not break-word) so unspaced 32-char tag names wrap in the centered header grid (G-24-5) - [Phase 24]: 24-14: overflow latch via dependency-plus-early-return in ArtistNote; Manage tags hand-off consumed only by a successful load (G-24-9..11) +- [Phase 25]: 25-01: ListWatchlist keeps zero-arg signature; latest/next release dates need no retention setting (G1) ### Pending Todos @@ -482,8 +484,8 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-06T21:45:00Z -Stopped at: Phase 24 complete, ready to plan Phase 25 +Last session: 2026-10-10T16:22:28.103Z +Stopped at: Completed 25-01-PLAN.md Resume file: None ## Operator Next Steps From aa50c00b70ebfb49cad8737863daa0c820e707ac Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:24:27 -0500 Subject: [PATCH 005/142] feat(25-03): show latest and upcoming release on Watchlist cards - add latest_release_date/next_release_date to WatchlistEntry - add formatReleaseDate (string-only, no Date construction) - render latest-release line and conditional Upcoming segment --- .../history/HistoryFilters.test.tsx | 4 +++ .../components/watchlist/ArtistNote.test.tsx | 2 ++ .../watchlist/PreferenceToggles.test.tsx | 2 ++ .../watchlist/SearchResultsColumns.test.tsx | 2 ++ .../components/watchlist/TagChips.test.tsx | 2 ++ .../components/watchlist/TagCombobox.test.tsx | 2 ++ web/app/components/watchlist/WatchlistRow.tsx | 16 ++++++++++ web/app/lib/api.test.ts | 2 ++ web/app/lib/api.ts | 4 +++ web/app/lib/format.ts | 30 +++++++++++++++++++ web/app/routes/watchlist.test.tsx | 28 +++++++++++++++++ 11 files changed, 94 insertions(+) diff --git a/web/app/components/history/HistoryFilters.test.tsx b/web/app/components/history/HistoryFilters.test.tsx index 50fc5d3..edd3169 100644 --- a/web/app/components/history/HistoryFilters.test.tsx +++ b/web/app/components/history/HistoryFilters.test.tsx @@ -26,6 +26,8 @@ const artists: WatchlistEntry[] = [ updated_at: "2026-01-01T00:00:00Z", tags: [], note: null, + latest_release_date: null, + next_release_date: null, }, { id: 2, @@ -41,6 +43,8 @@ const artists: WatchlistEntry[] = [ updated_at: "2026-01-02T00:00:00Z", tags: [], note: null, + latest_release_date: null, + next_release_date: null, }, ] diff --git a/web/app/components/watchlist/ArtistNote.test.tsx b/web/app/components/watchlist/ArtistNote.test.tsx index 99160cd..9d589ee 100644 --- a/web/app/components/watchlist/ArtistNote.test.tsx +++ b/web/app/components/watchlist/ArtistNote.test.tsx @@ -32,6 +32,8 @@ const entry: WatchlistEntry = { updated_at: "2026-01-01T00:00:00Z", tags: [], note: null, + latest_release_date: null, + next_release_date: null, } describe("ArtistNote", () => { diff --git a/web/app/components/watchlist/PreferenceToggles.test.tsx b/web/app/components/watchlist/PreferenceToggles.test.tsx index b273afc..daa6f33 100644 --- a/web/app/components/watchlist/PreferenceToggles.test.tsx +++ b/web/app/components/watchlist/PreferenceToggles.test.tsx @@ -29,6 +29,8 @@ const entry: WatchlistEntry = { updated_at: "2026-01-01T00:00:00Z", tags: [], note: null, + latest_release_date: null, + next_release_date: null, } describe("PreferenceToggles", () => { diff --git a/web/app/components/watchlist/SearchResultsColumns.test.tsx b/web/app/components/watchlist/SearchResultsColumns.test.tsx index d183e6b..a1809d5 100644 --- a/web/app/components/watchlist/SearchResultsColumns.test.tsx +++ b/web/app/components/watchlist/SearchResultsColumns.test.tsx @@ -74,6 +74,8 @@ const watchlistEntry: WatchlistEntry = { updated_at: "2026-01-01T00:00:00Z", tags: [], note: null, + latest_release_date: null, + next_release_date: null, } describe("SearchResultsColumns", () => { diff --git a/web/app/components/watchlist/TagChips.test.tsx b/web/app/components/watchlist/TagChips.test.tsx index c8459c9..69110fd 100644 --- a/web/app/components/watchlist/TagChips.test.tsx +++ b/web/app/components/watchlist/TagChips.test.tsx @@ -54,6 +54,8 @@ const entry: WatchlistEntry = { { id: 2, name: "" }, ], note: null, + latest_release_date: null, + next_release_date: null, } function renderChips() { diff --git a/web/app/components/watchlist/TagCombobox.test.tsx b/web/app/components/watchlist/TagCombobox.test.tsx index d6b094a..03004bd 100644 --- a/web/app/components/watchlist/TagCombobox.test.tsx +++ b/web/app/components/watchlist/TagCombobox.test.tsx @@ -20,6 +20,8 @@ const entry: WatchlistEntry = { updated_at: "2026-01-01T00:00:00Z", tags: [], note: null, + latest_release_date: null, + next_release_date: null, } function renderCombobox( diff --git a/web/app/components/watchlist/WatchlistRow.tsx b/web/app/components/watchlist/WatchlistRow.tsx index e8e4a6f..36c4bf7 100644 --- a/web/app/components/watchlist/WatchlistRow.tsx +++ b/web/app/components/watchlist/WatchlistRow.tsx @@ -3,6 +3,7 @@ import { X } from "lucide-react" import { CoverArt } from "~/components/common/CoverArt" import { Button } from "~/components/ui/button" import type { WatchlistEntry } from "~/lib/api" +import { formatReleaseDate } from "~/lib/format" import { ArtistNote } from "./ArtistNote" import { PreferenceToggles } from "./PreferenceToggles" @@ -49,6 +50,21 @@ export function WatchlistRow({ {entry.disambiguation} )} +

+ + {entry.latest_release_date !== null + ? `Latest release: ${formatReleaseDate(entry.latest_release_date)}` + : "No releases yet"} + + {entry.next_release_date !== null && ( + <> + + + Upcoming: {formatReleaseDate(entry.next_release_date)} + + + )} +

{ updated_at: "2026-01-01T00:00:00Z", tags: [], note: "crate digger", + latest_release_date: null, + next_release_date: null, } fetchMock.mockResolvedValueOnce( new Response(JSON.stringify(updatedEntry), { diff --git a/web/app/lib/api.ts b/web/app/lib/api.ts index cb38c51..184c263 100644 --- a/web/app/lib/api.ts +++ b/web/app/lib/api.ts @@ -90,6 +90,10 @@ export interface WatchlistEntry { // when unset (D-28). tags: TagRef[] note: string | null + // Server-computed (G2/G3): the client only formats these, never compares + // them to today. + latest_release_date: string | null + next_release_date: string | null } // SearchArtist mirrors internal/httpserver/search.go's SearchArtist. diff --git a/web/app/lib/format.ts b/web/app/lib/format.ts index 1735483..7af1414 100644 --- a/web/app/lib/format.ts +++ b/web/app/lib/format.ts @@ -144,6 +144,36 @@ export function formatDuration(ms: number | null): string { return `${minutes}m ${String(seconds).padStart(2, "0")}s` } +const MONTHS = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +] + +const YEAR_RE = /^(\d{4})$/ +const YEAR_MONTH_RE = /^(\d{4})-(0[1-9]|1[0-2])$/ +const YEAR_MONTH_DAY_RE = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/ + +// formatReleaseDate renders a stored release date at its own precision (D-15). +// String-only on purpose: no Date, so no timezone shift and no ICU "Sept". +export function formatReleaseDate(raw: string): string { + if (YEAR_RE.test(raw)) return raw + const ym = YEAR_MONTH_RE.exec(raw) + if (ym) return `${MONTHS[Number(ym[2]) - 1]} ${ym[1]}` + const ymd = YEAR_MONTH_DAY_RE.exec(raw) + if (ymd) return `${Number(ymd[3])} ${MONTHS[Number(ymd[2]) - 1]} ${ymd[1]}` + return raw +} + // formatPollInterval turns poll_interval_seconds (SYS-02, D-06) into human // text for the About block's "Every {…}" row and the first-run copy. Returns // a bare phrase with no leading preposition -- both call sites supply their diff --git a/web/app/routes/watchlist.test.tsx b/web/app/routes/watchlist.test.tsx index 5fce7d2..8d9a56b 100644 --- a/web/app/routes/watchlist.test.tsx +++ b/web/app/routes/watchlist.test.tsx @@ -4,6 +4,7 @@ import { screen, waitFor, waitForElementToBeRemoved, + within, } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { createRoutesStub } from "react-router" @@ -60,6 +61,8 @@ const entry: WatchlistEntry = { updated_at: "2026-01-01T00:00:00Z", tags: [], note: null, + latest_release_date: null, + next_release_date: null, } // Lets a test hold a mocked request open and settle it at a chosen moment. @@ -925,4 +928,29 @@ describe("Watchlist route under the passphrase gate (GATE-05 / UI-SPEC E6)", () name: "Enter the instance passphrase", }) }) + + it("renders each card's latest release and a conditional Upcoming segment (D-03)", async () => { + mockListWatchlist.mockResolvedValue([ + { + ...entry, + id: 1, + name: "Drake", + latest_release_date: "2024-05-17", + next_release_date: "2027", + }, + { ...entry, id: 2, name: "Kendrick" }, + ]) + + renderRoute(Watchlist, "/") + + await screen.findByText("Drake") + const [drake, kendrick] = screen.getAllByRole("listitem") + + expect( + within(drake).getByText("Latest release: 17 May 2024") + ).toBeInTheDocument() + expect(within(drake).getByText("Upcoming: 2027")).toBeInTheDocument() + expect(within(kendrick).getByText("No releases yet")).toBeInTheDocument() + expect(within(kendrick).queryByText(/Upcoming:/)).not.toBeInTheDocument() + }) }) From 07e47eec7ed30ceb9800870e52fa1a1b7a3fee3c Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:25:43 -0500 Subject: [PATCH 006/142] test(25-03): pin release-date table, add makeEntry fixture, refresh route comment - formatReleaseDate table over year, year-month, full date, malformed - makeEntry builder for later Phase 25 plans - upcoming-only and literal-markup route tests - replace outdated Phase 06 module comment --- web/app/lib/format.test.ts | 21 +++++++++++++++++++++ web/app/lib/test/fixtures.ts | 23 +++++++++++++++++++++++ web/app/routes/watchlist.test.tsx | 29 +++++++++++++++++++++++++++++ web/app/routes/watchlist.tsx | 10 +++------- 4 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 web/app/lib/test/fixtures.ts diff --git a/web/app/lib/format.test.ts b/web/app/lib/format.test.ts index 8a3ad7c..0b2df90 100644 --- a/web/app/lib/format.test.ts +++ b/web/app/lib/format.test.ts @@ -7,6 +7,7 @@ import { formatIsoTitle, formatPollInterval, formatRelativeTime, + formatReleaseDate, } from "~/lib/format" const EM_DASH = "—" @@ -116,3 +117,23 @@ describe("formatPollInterval", () => { expect(formatPollInterval(seconds)).toBe(expected) }) }) + +describe("formatReleaseDate", () => { + it.each([ + ["2024", "2024"], + ["2024-01", "Jan 2024"], + ["2024-09", "Sep 2024"], // fixed table, never ICU "Sept" + ["2024-12", "Dec 2024"], + ["2024-05-17", "17 May 2024"], + ["2024-05-01", "1 May 2024"], // no leading zero on the day + ])("renders %s as %s", (raw, expected) => { + expect(formatReleaseDate(raw)).toBe(expected) + }) + + it.each(["2024-13", "2024-5", "", "May 2024", "2024-05-17T00:00:00Z"])( + "returns malformed input %j unchanged", + (raw) => { + expect(formatReleaseDate(raw)).toBe(raw) + } + ) +}) diff --git a/web/app/lib/test/fixtures.ts b/web/app/lib/test/fixtures.ts new file mode 100644 index 0000000..a54d27e --- /dev/null +++ b/web/app/lib/test/fixtures.ts @@ -0,0 +1,23 @@ +import type { WatchlistEntry } from "~/lib/api" + +// makeEntry builds a WatchlistEntry with neutral defaults; overrides win. +export function makeEntry( + overrides: Partial & Pick +): WatchlistEntry { + return { + artist_id: overrides.id + 1000, + mbid: `mbid-${overrides.id}`, + deezer_id: null, + disambiguation: null, + image_url: null, + release_types: ["album", "single", "ep", "deluxe"], + muted_event_types: [], + tags: [], + note: null, + latest_release_date: null, + next_release_date: null, + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + ...overrides, + } +} diff --git a/web/app/routes/watchlist.test.tsx b/web/app/routes/watchlist.test.tsx index 8d9a56b..a7de09e 100644 --- a/web/app/routes/watchlist.test.tsx +++ b/web/app/routes/watchlist.test.tsx @@ -26,6 +26,7 @@ import { type TagSummary, type WatchlistEntry, } from "~/lib/api" +import { makeEntry } from "~/lib/test/fixtures" import { renderRoute } from "~/lib/test/routeStub" import Watchlist from "./watchlist" @@ -953,4 +954,32 @@ describe("Watchlist route under the passphrase gate (GATE-05 / UI-SPEC E6)", () expect(within(kendrick).getByText("No releases yet")).toBeInTheDocument() expect(within(kendrick).queryByText(/Upcoming:/)).not.toBeInTheDocument() }) + + it("shows Upcoming alongside No releases yet when only next_release_date is set", async () => { + mockListWatchlist.mockResolvedValue([ + makeEntry({ id: 1, name: "Drake", next_release_date: "2026-11" }), + ]) + + renderRoute(Watchlist, "/") + + await screen.findByText("Drake") + const card = screen.getByRole("listitem") + expect(within(card).getByText("No releases yet")).toBeInTheDocument() + expect(within(card).getByText("Upcoming: Nov 2026")).toBeInTheDocument() + }) + + it("renders a release date containing markup as literal text (T-25-09)", async () => { + mockListWatchlist.mockResolvedValue([ + makeEntry({ id: 1, name: "Drake", latest_release_date: "2024" }), + ]) + + renderRoute(Watchlist, "/") + + await screen.findByText("Drake") + const card = screen.getByRole("listitem") + expect( + within(card).getByText("Latest release: 2024") + ).toBeInTheDocument() + expect(card.querySelector("b")).toBeNull() + }) }) diff --git a/web/app/routes/watchlist.tsx b/web/app/routes/watchlist.tsx index b52dd95..b60e3f6 100644 --- a/web/app/routes/watchlist.tsx +++ b/web/app/routes/watchlist.tsx @@ -26,13 +26,9 @@ import { type TagChange, } from "~/lib/useTagVocabulary" -// Watchlist renders the UI-02 management surface: fetches listWatchlist() -// on mount and renders the entries in exactly the order the server -// returned them -- ListWatchlist already orders by artist name then artist -// id, a deterministic total order even for two artists sharing a name, so -// this route never re-sorts client-side. Per D-03/D-04 this tab is a pure -// management list with no cross-reference to History-tab state; every -// release-activity signal lives on the History tab only. +// Watchlist renders the UI-02 management surface: loads GET /watchlist once +// on mount. Phase 25 D-02/D-03 add client-side find/sort over that list and +// a latest-release line on each card. // // `refresh` re-issues the fetch without resetting `entries` to null first, // so a background re-sync (task 2's post-mutation refresh, plan 06-04's From 10e454d1501e74bf47a6c81c17847817ab8ce963 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:26:07 -0500 Subject: [PATCH 007/142] docs(25-03): add plan summary --- .../25-03-SUMMARY.md | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .planning/phases/25-find-filter-watchlist-and-history/25-03-SUMMARY.md diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-03-SUMMARY.md b/.planning/phases/25-find-filter-watchlist-and-history/25-03-SUMMARY.md new file mode 100644 index 0000000..1b42fa0 --- /dev/null +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-03-SUMMARY.md @@ -0,0 +1,127 @@ +--- +phase: 25-find-filter-watchlist-and-history +plan: 03 +subsystem: web +tags: [watchlist, release-dates, formatter, react, vitest] +requires: + - phase: 25-01 + provides: "GET /watchlist latest_release_date / next_release_date fields" +provides: + - "WatchlistEntry.latest_release_date / next_release_date (string | null)" + - "formatReleaseDate(raw): stored-precision, timezone-free date formatter" + - "makeEntry(overrides) test fixture builder for later Phase 25 plans" + - "Latest release / Upcoming lines on every Watchlist card" +affects: [25-04, 25-05, 25-07, 25-08] +tech-stack: + added: [] + patterns: + - "String-only date formatting (fixed month table, anchored regexes, no Date)" + - "Shared test fixture builder under app/lib/test/" +key-files: + created: + - web/app/lib/test/fixtures.ts + modified: + - web/app/lib/api.ts + - web/app/lib/format.ts + - web/app/lib/format.test.ts + - web/app/components/watchlist/WatchlistRow.tsx + - web/app/routes/watchlist.tsx + - web/app/routes/watchlist.test.tsx + - web/app/lib/api.test.ts + - web/app/components/history/HistoryFilters.test.tsx + - web/app/components/watchlist/ArtistNote.test.tsx + - web/app/components/watchlist/PreferenceToggles.test.tsx + - web/app/components/watchlist/SearchResultsColumns.test.tsx + - web/app/components/watchlist/TagChips.test.tsx + - web/app/components/watchlist/TagCombobox.test.tsx +key-decisions: + - "Release dates render via string-only formatReleaseDate; malformed input passes through unchanged as plain JSX text" + - "Client never compares dates to today; server decides what is upcoming (G2/G3)" +requirements-completed: [WLVW-03] +status: complete +commits: 2 +plan_head_before: f6687a68fcafd1397bb02e8c2c130c9cacf80d69 +actuals: + tokens: 9000 + tasks: 2 + commits: 2 +duration: 8 min +completed: 2026-10-10 +coverage: + - deliverable: "Watchlist cards show latest release or 'No releases yet' plus conditional Upcoming" + verification: + - kind: test + ref: "web/app/routes/watchlist.test.tsx#renders each card's latest release and a conditional Upcoming segment (D-03)" + status: pass + human_judgment: false + - deliverable: "formatReleaseDate pins year / year-month / full-date table and passes malformed input through" + verification: + - kind: test + ref: "web/app/lib/format.test.ts#formatReleaseDate" + status: pass + human_judgment: false + - deliverable: "Release dates render as literal text (no markup injection)" + verification: + - kind: test + ref: "web/app/routes/watchlist.test.tsx#renders a release date containing markup as literal text (T-25-09)" + status: pass + human_judgment: false + - deliverable: "makeEntry fixture builder and outdated route comment replaced" + verification: + - kind: command + ref: "corepack pnpm --dir web run typecheck" + status: pass + human_judgment: false +--- + +# Phase 25 Plan 03: Latest and upcoming release on Watchlist cards Summary + +Each Watchlist card now shows `Latest release: {date}` (or `No releases yet`) and, only when the server sends a next date, `Upcoming: {date}`, formatted at stored precision by a string-only `formatReleaseDate` with no Date construction. + +## Accomplishments + +- `WatchlistEntry` gained `latest_release_date` and `next_release_date` (`string | null`), matching plan 25-01's JSON contract; all existing fixtures updated and typecheck is green. +- `formatReleaseDate` renders `2024`, `May 2024`, `17 May 2024` from a fixed English month table and returns anything else unchanged. +- `WatchlistRow` renders a single wrapping line in the name column, with the Upcoming segment conditional on `next_release_date`. +- `makeEntry` fixture builder added in `web/app/lib/test/fixtures.ts` for the rest of the phase. +- The `watchlist.tsx` module comment no longer claims "never re-sorts" or "no release activity"; replaced with a 3-line Phase 25 D-02/D-03 note. + +## Task Commits + +1. Task 1 (tracer): `aa50c00` feat(25-03): show latest and upcoming release on Watchlist cards +2. Task 2 (tdd): `07e47ee` test(25-03): pin release-date table, add makeEntry fixture, refresh route comment + +## Deviations from Plan + +### Notes + +**1. [Plan criterion quirk] Date-construction grep matches the function signature** +- **Found during:** Task 1 acceptance check +- **Issue:** `awk '/export function formatReleaseDate/,/^}/' format.ts | grep -c 'Date('` prints `1`, not `0`, because the signature line `formatReleaseDate(raw: string)` contains the substring `Date(`. No Date object is constructed anywhere in the function body. +- **Fix:** None needed; intent (no Date construction) is met. Verified by reading the function. + +**2. [TDD ordering] Task 2 formatter tests were green on first run** +- **Issue:** `formatReleaseDate` was already implemented in the Task 1 tracer (the tracer's route test needed it), so Task 2's table tests could not be observed RED. The tests pin the table and were confirmed passing. + +**Total deviations:** 0 auto-fixed, 2 notes. **Impact:** none on behavior or scope. + +## Verification + +- `vitest run app/lib/format.test.ts app/routes/watchlist.test.tsx`: 69 passed +- `corepack pnpm run typecheck`: exit 0 +- `prettier --check "**/*.{ts,tsx}"`: clean +- `corepack pnpm test` (full web suite with coverage): passed + +## Known Stubs + +None. + +## Threat Flags + +None. T-25-09 (XSS) and T-25-10 (date integrity) mitigated as planned: plain JSX text nodes, literal-`` route test, no Date or timezone math. + +## Self-Check: PASSED + +- FOUND: web/app/lib/test/fixtures.ts +- FOUND commits aa50c00, 07e47ee +- Task 1 and Task 2 acceptance criteria re-run and passing (see note 1) From 66054460b461e37f24f92bad404ff72ef5523624 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:26:23 -0500 Subject: [PATCH 008/142] docs(25-03): update state and roadmap for completed plan --- .planning/ROADMAP.md | 6 +++--- .planning/STATE.md | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 144896f..3dcb363 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -193,13 +193,13 @@ Plans: 4. The user can filter History by a tag alongside the existing filters. It lists events for artists carrying that tag, including artists since removed from the watchlist, and it never shows an event older than the retention window, either on the first page or via "load older". 5. Opening the Watchlist makes one request regardless of watchlist size. Searching, sorting, and filtering then happen instantly with no further requests. -**Plans:** 1/10 plans executed +**Plans:** 2/10 plans executed Plans: **Wave 1** - [x] 25-01-PLAN.md — GET /watchlist gains `latest_release_date` and `next_release_date` from two lateral joins in its single query (G1–G3), with semantics and projection-parity tests -- [ ] 25-03-PLAN.md — Cards show `Latest release: {date}` / `No releases yet` and `Upcoming: {date}`, `formatReleaseDate`, and the `makeEntry` fixture builder +- [x] 25-03-PLAN.md — Cards show `Latest release: {date}` / `No releases yet` and `Upcoming: {date}`, `formatReleaseDate`, and the `makeEntry` fixture builder **Wave 2** *(blocked on Wave 1 completion)* @@ -327,7 +327,7 @@ Phases execute in numeric order: 24 → 25 → 26 → 27 → 28 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 24. Artist Tags & Notes | 14/14 | Complete | 2026-10-06 | -| 25. Find & Filter — Watchlist and History | 1/10 | In Progress| | +| 25. Find & Filter — Watchlist and History | 2/10 | In Progress| | | 26. Bulk Edit & Remove | 0/? | Not started | - | | 27. Paste-a-List Bulk Add | 0/? | Not started | - | | 28. Tags on Discord Notifications | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index fd77945..ab7c43a 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,16 +5,16 @@ milestone_name: Watchlist Organization current_phase: 25 current_phase_name: Find & Filter — Watchlist and History status: executing -stopped_at: Completed 25-01-PLAN.md -last_updated: "2026-10-10T16:22:28.176Z" +stopped_at: Completed 25-03-PLAN.md +last_updated: "2026-10-10T16:26:14.453Z" last_activity: 2026-10-10 last_activity_desc: Phase 25 execution started -state_head: a20968dd9c4fca15a77dc061c4f41d8c3c05b0a3 +state_head: 10e454d1501e74bf47a6c81c17847817ab8ce963 progress: total_phases: 5 completed_phases: 1 total_plans: 24 - completed_plans: 15 + completed_plans: 16 percent: 20 --- @@ -30,7 +30,7 @@ See: .planning/PROJECT.md (updated 2026-10-06) ## Current Position Phase: 25 (Find & Filter — Watchlist and History) — EXECUTING -Plan: 2 of 10 +Plan: 3 of 10 Status: Ready to execute Last activity: 2026-10-10 — Phase 25 execution started @@ -163,6 +163,7 @@ Last activity: 2026-10-10 — Phase 25 execution started | Phase 24 P13 | 7 min | 2 tasks | 4 files | | Phase 24 P14 | 20 min | 3 tasks | 5 files | | Phase 25 P01 | 25 min | 2 tasks | 6 files | +| Phase 25 P03 | 8 min | 2 tasks | 14 files | ## Accumulated Context @@ -391,6 +392,7 @@ Recent decisions affecting current work: - [Phase 24]: 24-13: wrap-anywhere on ConfirmDialog title/description (not break-word) so unspaced 32-char tag names wrap in the centered header grid (G-24-5) - [Phase 24]: 24-14: overflow latch via dependency-plus-early-return in ArtistNote; Manage tags hand-off consumed only by a successful load (G-24-9..11) - [Phase 25]: 25-01: ListWatchlist keeps zero-arg signature; latest/next release dates need no retention setting (G1) +- [Phase 25]: Release dates render via string-only formatReleaseDate; malformed values pass through as plain text — No Date construction avoids timezone shifts; server decides upcoming (G2/G3) ### Pending Todos @@ -484,8 +486,8 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-10T16:22:28.103Z -Stopped at: Completed 25-01-PLAN.md +Last session: 2026-10-10T16:26:14.370Z +Stopped at: Completed 25-03-PLAN.md Resume file: None ## Operator Next Steps From c421197351a2fd692c09c4ed50f095fe8621f0d5 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:28:15 -0500 Subject: [PATCH 009/142] feat(25-02): filter GET /events by tag_id inside retention-aware queries - EXISTS over artist_tags in ListEvents and HasOlderEvents - events.ListParams.TagID passed to both queries - tag_id parsing with fixed invalid tag_id 400 - tracer test over two artists with no watchlist rows --- internal/db/sqlc/events.sql.go | 31 +++++++--- internal/events/service.go | 3 + internal/httpserver/events.go | 11 +++- internal/httpserver/events_test.go | 98 ++++++++++++++++++++++++++++++ queries/events.sql | 3 + 5 files changed, 134 insertions(+), 12 deletions(-) diff --git a/internal/db/sqlc/events.sql.go b/internal/db/sqlc/events.sql.go index 49569fa..776e479 100644 --- a/internal/db/sqlc/events.sql.go +++ b/internal/db/sqlc/events.sql.go @@ -102,13 +102,15 @@ SELECT EXISTS( SELECT 1 FROM events WHERE ($1::bigint IS NULL OR artist_id = $1::bigint) AND ($2::text IS NULL OR event_type = $2::text) - AND created_at < $3::timestamptz + AND ($3::bigint IS NULL OR EXISTS (SELECT 1 FROM artist_tags link WHERE link.artist_id = events.artist_id AND link.tag_id = $3::bigint)) + AND created_at < $4::timestamptz ) AS has_older ` type HasOlderEventsParams struct { ArtistID *int64 `json:"artist_id"` EventType *string `json:"event_type"` + TagID *int64 `json:"tag_id"` Cutoff pgtype.Timestamptz `json:"cutoff"` } @@ -128,7 +130,12 @@ type HasOlderEventsParams struct { // row exactly at the boundary is never double-counted as both visible and // older (D-04 stays consistent across both queries). func (q *Queries) HasOlderEvents(ctx context.Context, arg HasOlderEventsParams) (bool, error) { - row := q.db.QueryRow(ctx, hasOlderEvents, arg.ArtistID, arg.EventType, arg.Cutoff) + row := q.db.QueryRow(ctx, hasOlderEvents, + arg.ArtistID, + arg.EventType, + arg.TagID, + arg.Cutoff, + ) var has_older bool err := row.Scan(&has_older) return has_older, err @@ -206,26 +213,29 @@ SELECT id, artist_id, source, event_type, external_id, release_group_mbid, FROM events WHERE ($1::bigint IS NULL OR artist_id = $1::bigint) AND ($2::text IS NULL OR event_type = $2::text) + -- HIST-02: tag filter lives in this retention-aware query, never a separate one (PITFALLS #9). + AND ($3::bigint IS NULL OR EXISTS (SELECT 1 FROM artist_tags link WHERE link.artist_id = events.artist_id AND link.tag_id = $3::bigint)) AND ( - $3::bigint IS NULL + $4::bigint IS NULL OR CASE - WHEN $4::text IS NULL + WHEN $5::text IS NULL THEN release_date IS NULL - AND id < $3::bigint + AND id < $4::bigint ELSE release_date IS NULL - OR release_date < $4::text - OR (release_date = $4::text - AND id < $3::bigint) + OR release_date < $5::text + OR (release_date = $5::text + AND id < $4::bigint) END ) - AND created_at >= $5::timestamptz + AND created_at >= $6::timestamptz ORDER BY release_date DESC NULLS LAST, id DESC -LIMIT $6 +LIMIT $7 ` type ListEventsParams struct { ArtistID *int64 `json:"artist_id"` EventType *string `json:"event_type"` + TagID *int64 `json:"tag_id"` CursorID *int64 `json:"cursor_id"` CursorReleaseDate *string `json:"cursor_release_date"` Cutoff pgtype.Timestamptz `json:"cutoff"` @@ -304,6 +314,7 @@ func (q *Queries) ListEvents(ctx context.Context, arg ListEventsParams) ([]ListE rows, err := q.db.Query(ctx, listEvents, arg.ArtistID, arg.EventType, + arg.TagID, arg.CursorID, arg.CursorReleaseDate, arg.Cutoff, diff --git a/internal/events/service.go b/internal/events/service.go index 1d99f02..0391476 100644 --- a/internal/events/service.go +++ b/internal/events/service.go @@ -57,6 +57,7 @@ type Event struct { type ListParams struct { ArtistID *int64 EventType *string + TagID *int64 // nil means no tag filter Cursor *Cursor PageSize int32 } @@ -143,6 +144,7 @@ func (s *Service) List(ctx context.Context, p ListParams) (Page, error) { rows, err := s.q.ListEvents(ctx, sqlc.ListEventsParams{ ArtistID: p.ArtistID, EventType: p.EventType, + TagID: p.TagID, CursorID: cursorID, CursorReleaseDate: cursorReleaseDate, Cutoff: cutoffParam, @@ -160,6 +162,7 @@ func (s *Service) List(ctx context.Context, p ListParams) (Page, error) { hasOlderEvents, err := s.q.HasOlderEvents(ctx, sqlc.HasOlderEventsParams{ ArtistID: p.ArtistID, EventType: p.EventType, + TagID: p.TagID, Cutoff: cutoffParam, }) if err != nil { diff --git a/internal/httpserver/events.go b/internal/httpserver/events.go index 4d95b95..1edd0dc 100644 --- a/internal/httpserver/events.go +++ b/internal/httpserver/events.go @@ -69,8 +69,8 @@ func parseOptionalPageSize(r *http.Request) (int32, bool) { return int32(v), true //nolint:gosec // int32 wraparound from a huge v is harmless here: every reachable output is either <=0 or >MaxPageSize (caught by events.Service.List's clamp, see comment above) or lands directly in the already-valid [1,MaxPageSize] range } -// handleListEvents implements GET /events (HIST-01, UI-03): reads four -// optional query params -- artist_id, event_type, cursor, limit -- and +// handleListEvents implements GET /events (HIST-01, UI-03): reads five +// optional query params -- artist_id, event_type, tag_id, cursor, limit -- and // builds a populated events.ListParams, rejecting any malformed value with a // 400 before ever calling the store (T-06-06 through T-06-10). On a store // error, this mirrors every other handler exactly: log the raw error to the @@ -84,6 +84,12 @@ func (s *Server) handleListEvents(w http.ResponseWriter, r *http.Request) { return } + tagID, ok := parseOptionalPositiveInt64(r, "tag_id") + if !ok { + writeError(w, http.StatusBadRequest, "invalid tag_id") + return + } + var eventType *string if raw := strings.TrimSpace(r.URL.Query().Get("event_type")); raw != "" { if !slices.Contains(watchlist.EventTypes, raw) { @@ -118,6 +124,7 @@ func (s *Server) handleListEvents(w http.ResponseWriter, r *http.Request) { page, err := s.events.List(r.Context(), events.ListParams{ ArtistID: artistID, EventType: eventType, + TagID: tagID, Cursor: cursor, PageSize: pageSize, }) diff --git a/internal/httpserver/events_test.go b/internal/httpserver/events_test.go index 955528a..19ea91b 100644 --- a/internal/httpserver/events_test.go +++ b/internal/httpserver/events_test.go @@ -1328,3 +1328,101 @@ func TestListEvents_HasOlderEventsRespectsFilters(t *testing.T) { t.Fatal("HasOlderEvents = true, want false for artist_id=B, who has only in-window events -- the query must apply its own artist_id filter, not answer a table-wide question") } } + +func insertTestTag(t *testing.T, pool *pgxpool.Pool, name string) int64 { + t.Helper() + var id int64 + row := pool.QueryRow(context.Background(), `INSERT INTO tags (name) VALUES ($1) RETURNING id`, name) + if err := row.Scan(&id); err != nil { + t.Fatalf("insert test tag: %v", err) + } + return id +} + +func linkTestTag(t *testing.T, pool *pgxpool.Pool, artistID, tagID int64) { + t.Helper() + if _, err := pool.Exec(context.Background(), + `INSERT INTO artist_tags (artist_id, tag_id) VALUES ($1, $2)`, artistID, tagID); err != nil { + t.Fatalf("link test tag: %v", err) + } +} + +// getEventsBody GETs rawURL and decodes the events envelope, failing on non-200. +func getEventsBody(t *testing.T, rawURL string) eventsResponseBody { + t.Helper() + resp, err := http.Get(rawURL) + if err != nil { + t.Fatalf("GET %s: %v", rawURL, err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + t.Fatalf("GET %s: status = %d, want %d", rawURL, resp.StatusCode, http.StatusOK) + } + var body eventsResponseBody + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + t.Fatalf("decode response body: %v", err) + } + return body +} + +func eventIDs(evs []events.Event) []int64 { + ids := make([]int64, 0, len(evs)) + for _, e := range evs { + ids = append(ids, e.ID) + } + return ids +} + +func sameIDs(got, want []int64) bool { + if len(got) != len(want) { + return false + } + for i := range got { + if got[i] != want[i] { + return false + } + } + return true +} + +// TestListEvents_TagFilter is the HIST-02 tracer: tag_id narrows the feed to +// the tag's artists, and neither artist has a watchlist row (removed-artist shape). +func TestListEvents_TagFilter(t *testing.T) { + pool := testutil.NewTestPool(t) + base := testMBID(t) + mbidTagged, mbidOther := base+"-tagged", base+"-other" + + tagged := insertTestArtist(t, pool, mbidTagged) + other := insertTestArtist(t, pool, mbidOther) + tagA := insertTestTag(t, pool, base+"-tag-a") + tagB := insertTestTag(t, pool, base+"-tag-b") + t.Cleanup(func() { + ctx := context.Background() + if _, err := pool.Exec(ctx, "DELETE FROM artists WHERE mbid = ANY($1)", []string{mbidTagged, mbidOther}); err != nil { + t.Fatalf("cleanup: delete artists: %v", err) + } + if _, err := pool.Exec(ctx, "DELETE FROM tags WHERE id = ANY($1)", []int64{tagA, tagB}); err != nil { + t.Fatalf("cleanup: delete tags: %v", err) + } + }) + linkTestTag(t, pool, tagged, tagA) + linkTestTag(t, pool, other, tagB) + + first := insertTestEvent(t, pool, tagged, mbidTagged+"-1") + second := insertTestEvent(t, pool, tagged, mbidTagged+"-2") + otherEv := insertTestEvent(t, pool, other, mbidOther+"-1") + + svc := events.NewService(sqlc.New(pool), 90) + srv := httpserver.New(pool, stubStore{}, svc, nil, discardLogger()) + ts := httptest.NewServer(srv.Router()) + defer ts.Close() + + got := getEventsBody(t, fmt.Sprintf("%s/events?tag_id=%d", ts.URL, tagA)) + if want := []int64{second, first}; !sameIDs(eventIDs(got.Events), want) { + t.Fatalf("tag A ids = %v, want %v", eventIDs(got.Events), want) + } + got = getEventsBody(t, fmt.Sprintf("%s/events?tag_id=%d", ts.URL, tagB)) + if want := []int64{otherEv}; !sameIDs(eventIDs(got.Events), want) { + t.Fatalf("tag B ids = %v, want %v", eventIDs(got.Events), want) + } +} diff --git a/queries/events.sql b/queries/events.sql index 42df08c..b0b8886 100644 --- a/queries/events.sql +++ b/queries/events.sql @@ -147,6 +147,8 @@ SELECT id, artist_id, source, event_type, external_id, release_group_mbid, FROM events WHERE (sqlc.narg('artist_id')::bigint IS NULL OR artist_id = sqlc.narg('artist_id')::bigint) AND (sqlc.narg('event_type')::text IS NULL OR event_type = sqlc.narg('event_type')::text) + -- HIST-02: tag filter lives in this retention-aware query, never a separate one (PITFALLS #9). + AND (sqlc.narg('tag_id')::bigint IS NULL OR EXISTS (SELECT 1 FROM artist_tags link WHERE link.artist_id = events.artist_id AND link.tag_id = sqlc.narg('tag_id')::bigint)) AND ( sqlc.narg('cursor_id')::bigint IS NULL OR CASE @@ -183,5 +185,6 @@ SELECT EXISTS( SELECT 1 FROM events WHERE (sqlc.narg('artist_id')::bigint IS NULL OR artist_id = sqlc.narg('artist_id')::bigint) AND (sqlc.narg('event_type')::text IS NULL OR event_type = sqlc.narg('event_type')::text) + AND (sqlc.narg('tag_id')::bigint IS NULL OR EXISTS (SELECT 1 FROM artist_tags link WHERE link.artist_id = events.artist_id AND link.tag_id = sqlc.narg('tag_id')::bigint)) AND created_at < sqlc.arg('cutoff')::timestamptz ) AS has_older; From 00e0ada558885d2ff83a7c713a5cafdbde100b56 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:37:01 -0500 Subject: [PATCH 010/142] test(25-02): pin tag filter retention, paging, composition and validation - retention holds on first page, every cursor page, and has_older_events - tag_id composes with artist_id and event_type; unknown id yields empty page - tag_id validation cases and TagID reaching both queries --- internal/httpserver/events_test.go | 211 ++++++++++++++++++++++++++++- 1 file changed, 208 insertions(+), 3 deletions(-) diff --git a/internal/httpserver/events_test.go b/internal/httpserver/events_test.go index 19ea91b..a7d3c59 100644 --- a/internal/httpserver/events_test.go +++ b/internal/httpserver/events_test.go @@ -194,7 +194,8 @@ func TestHandleListEvents_StoreErrorReturns500WithFixedMessage(t *testing.T) { // cannot observe that clamp. type recordingQuerier struct { sqlc.Querier - gotParams sqlc.ListEventsParams + gotParams sqlc.ListEventsParams + gotOlderParams sqlc.HasOlderEventsParams } func (q *recordingQuerier) ListEvents(_ context.Context, arg sqlc.ListEventsParams) ([]sqlc.ListEventsRow, error) { @@ -208,7 +209,8 @@ func (q *recordingQuerier) ListEvents(_ context.Context, arg sqlc.ListEventsPara // nil-panic at runtime the moment it's called through -- go build cannot // catch this, only a test run can (see this file's header comment and // 10-02-PLAN.md's corrections-carried-forward note). -func (q *recordingQuerier) HasOlderEvents(_ context.Context, _ sqlc.HasOlderEventsParams) (bool, error) { +func (q *recordingQuerier) HasOlderEvents(_ context.Context, arg sqlc.HasOlderEventsParams) (bool, error) { + q.gotOlderParams = arg return false, nil } @@ -228,6 +230,9 @@ func TestHandleListEvents_Validation(t *testing.T) { {"artist_id non-numeric", "artist_id=abc"}, {"artist_id zero", "artist_id=0"}, {"artist_id negative", "artist_id=-3"}, + {"tag_id non-numeric", "tag_id=abc"}, + {"tag_id zero", "tag_id=0"}, + {"tag_id negative", "tag_id=-1"}, {"cursor non-numeric", "cursor=abc"}, {"cursor zero", "cursor=0"}, {"event_type not allow-listed", "event_type=bogus"}, @@ -302,6 +307,70 @@ func TestHandleListEvents_Validation(t *testing.T) { } }) + t.Run("malformed tag_id gets the fixed message", func(t *testing.T) { + srv := httpserver.New(noopPinger{}, stubStore{}, stubEventsStore{}, nil, discardLogger()) + ts := httptest.NewServer(srv.Router()) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/events?tag_id=abc") + if err != nil { + t.Fatalf("GET /events?tag_id=abc: %v", err) + } + defer func() { _ = resp.Body.Close() }() + var eb errorBody + if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil { + t.Fatalf("decode response body: %v", err) + } + if resp.StatusCode != http.StatusBadRequest || eb.Error != "invalid tag_id" { + t.Fatalf("got %d %q, want 400 %q", resp.StatusCode, eb.Error, "invalid tag_id") + } + }) + + t.Run("empty tag_id is treated as absent, not malformed", func(t *testing.T) { + called := false + stub := stubEventsStore{listFunc: func(_ context.Context, p events.ListParams) (events.Page, error) { + called = true + if p.TagID != nil { + t.Fatalf("TagID = %v, want nil for an empty tag_id param", *p.TagID) + } + return events.Page{}, nil + }} + srv := httpserver.New(noopPinger{}, stubStore{}, stub, nil, discardLogger()) + ts := httptest.NewServer(srv.Router()) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/events?tag_id=") + if err != nil { + t.Fatalf("GET /events?tag_id=: %v", err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK || !called { + t.Fatalf("status = %d, called = %v, want 200 and the store called", resp.StatusCode, called) + } + }) + + t.Run("tag_id reaches both ListEvents and HasOlderEvents", func(t *testing.T) { + rq := &recordingQuerier{} + srv := httpserver.New(noopPinger{}, stubStore{}, events.NewService(rq, 90), nil, discardLogger()) + ts := httptest.NewServer(srv.Router()) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/events?tag_id=7") + if err != nil { + t.Fatalf("GET /events?tag_id=7: %v", err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK) + } + if rq.gotParams.TagID == nil || *rq.gotParams.TagID != 7 { + t.Fatalf("ListEventsParams.TagID = %v, want 7", rq.gotParams.TagID) + } + if rq.gotOlderParams.TagID == nil || *rq.gotOlderParams.TagID != 7 { + t.Fatalf("HasOlderEventsParams.TagID = %v, want 7", rq.gotOlderParams.TagID) + } + }) + t.Run("artist_id and event_type filters both populate ListParams", func(t *testing.T) { var got events.ListParams stub := stubEventsStore{listFunc: func(_ context.Context, p events.ListParams) (events.Page, error) { @@ -312,7 +381,7 @@ func TestHandleListEvents_Validation(t *testing.T) { ts := httptest.NewServer(srv.Router()) defer ts.Close() - resp, err := http.Get(ts.URL + "/events?artist_id=7&event_type=new_release") + resp, err := http.Get(ts.URL + "/events?artist_id=7&event_type=new_release&tag_id=5") if err != nil { t.Fatalf("GET /events: %v", err) } @@ -326,6 +395,9 @@ func TestHandleListEvents_Validation(t *testing.T) { if got.EventType == nil || *got.EventType != "new_release" { t.Fatalf("EventType = %v, want new_release", got.EventType) } + if got.TagID == nil || *got.TagID != 5 { + t.Fatalf("TagID = %v, want 5", got.TagID) + } }) t.Run("limit above the maximum is clamped, not rejected", func(t *testing.T) { @@ -1426,3 +1498,136 @@ func TestListEvents_TagFilter(t *testing.T) { t.Fatalf("tag B ids = %v, want %v", eventIDs(got.Events), want) } } + +// TestRetention_TagFilterComposesWithCutoff pins SC4: under a tag filter no +// aged-out event appears on the first page, any cursor page, or has_older_events. +func TestRetention_TagFilterComposesWithCutoff(t *testing.T) { + pool := testutil.NewTestPool(t) + base := testMBID(t) + mbidT, mbidT2, mbidU := base+"-t", base+"-t2", base+"-u" + + artistT := insertTestArtist(t, pool, mbidT) + artistT2 := insertTestArtist(t, pool, mbidT2) + artistU := insertTestArtist(t, pool, mbidU) + tagT := insertTestTag(t, pool, base+"-tag-t") + tagT2 := insertTestTag(t, pool, base+"-tag-t2") + t.Cleanup(func() { + ctx := context.Background() + if _, err := pool.Exec(ctx, "DELETE FROM artists WHERE mbid = ANY($1)", []string{mbidT, mbidT2, mbidU}); err != nil { + t.Fatalf("cleanup: delete artists: %v", err) + } + if _, err := pool.Exec(ctx, "DELETE FROM tags WHERE id = ANY($1)", []int64{tagT, tagT2}); err != nil { + t.Fatalf("cleanup: delete tags: %v", err) + } + }) + linkTestTag(t, pool, artistT, tagT) + linkTestTag(t, pool, artistT2, tagT2) + + now := time.Now() + agedT := insertTestEventAt(t, pool, artistT, mbidT+"-aged", now.AddDate(0, 0, -120)) + newer := insertTestEventAt(t, pool, artistT, mbidT+"-1d", now.AddDate(0, 0, -1)) + older := insertTestEventAt(t, pool, artistT, mbidT+"-2d", now.AddDate(0, 0, -2)) + insertTestEventAt(t, pool, artistU, mbidU+"-aged", now.AddDate(0, 0, -120)) + insertTestEventAt(t, pool, artistT2, mbidT2+"-1d", now.AddDate(0, 0, -1)) + + svc := events.NewService(sqlc.New(pool), 90) + srv := httpserver.New(pool, stubStore{}, svc, nil, discardLogger()) + ts := httptest.NewServer(srv.Router()) + defer ts.Close() + + first := getEventsBody(t, fmt.Sprintf("%s/events?tag_id=%d", ts.URL, tagT)) + // Undated rows order by id DESC, so the later-inserted 2-day-old row leads. + if want := []int64{older, newer}; !sameIDs(eventIDs(first.Events), want) { + t.Fatalf("first page ids = %v, want %v", eventIDs(first.Events), want) + } + if !first.HasOlderEvents { + t.Fatal("has_older_events = false, want true (tag T's artist has an aged-out event)") + } + + var collected []int64 + cursor := "" + done := false + for page := 0; page < 5 && !done; page++ { + u := fmt.Sprintf("%s/events?tag_id=%d&limit=1", ts.URL, tagT) + if cursor != "" { + u += "&cursor=" + url.QueryEscape(cursor) + } + body := getEventsBody(t, u) + collected = append(collected, eventIDs(body.Events)...) + if body.NextCursor == nil { + done = true + } else { + cursor = *body.NextCursor + } + } + if !done { + t.Fatal("cursor paging did not terminate within 5 pages") + } + for _, id := range collected { + if id == agedT { + t.Fatalf("cursor pages returned aged-out event %d: %v", agedT, collected) + } + } + if want := []int64{older, newer}; !sameIDs(collected, want) { + t.Fatalf("cursor-paged ids = %v, want %v", collected, want) + } + + t2 := getEventsBody(t, fmt.Sprintf("%s/events?tag_id=%d", ts.URL, tagT2)) + if len(t2.Events) != 1 { + t.Fatalf("tag T2 events = %v, want exactly 1", eventIDs(t2.Events)) + } + if t2.HasOlderEvents { + t.Fatal("tag T2 has_older_events = true, want false (the untagged artist's aged event must not count)") + } +} + +// TestListEvents_TagFilterComposesWithOtherFilters pins AND composition with +// artist_id/event_type and the empty-page contract for an unknown tag id. +func TestListEvents_TagFilterComposesWithOtherFilters(t *testing.T) { + pool := testutil.NewTestPool(t) + base := testMBID(t) + mbidT, mbidU := base+"-t", base+"-u" + + artistT := insertTestArtist(t, pool, mbidT) + artistU := insertTestArtist(t, pool, mbidU) + tagT := insertTestTag(t, pool, base+"-tag-t") + goneTag := insertTestTag(t, pool, base+"-tag-gone") + t.Cleanup(func() { + ctx := context.Background() + if _, err := pool.Exec(ctx, "DELETE FROM artists WHERE mbid = ANY($1)", []string{mbidT, mbidU}); err != nil { + t.Fatalf("cleanup: delete artists: %v", err) + } + if _, err := pool.Exec(ctx, "DELETE FROM tags WHERE id = ANY($1)", []int64{tagT, goneTag}); err != nil { + t.Fatalf("cleanup: delete tags: %v", err) + } + }) + linkTestTag(t, pool, artistT, tagT) + + insertTestEventTyped(t, pool, artistT, mbidT+"-rel", "new_release") + guest := insertTestEventTyped(t, pool, artistT, mbidT+"-guest", "guest_feature") + insertTestEventTyped(t, pool, artistU, mbidU+"-rel", "new_release") + + if _, err := pool.Exec(context.Background(), "DELETE FROM tags WHERE id = $1", goneTag); err != nil { + t.Fatalf("delete tag: %v", err) + } + + svc := events.NewService(sqlc.New(pool), 90) + srv := httpserver.New(pool, stubStore{}, svc, nil, discardLogger()) + ts := httptest.NewServer(srv.Router()) + defer ts.Close() + + got := getEventsBody(t, fmt.Sprintf("%s/events?tag_id=%d&artist_id=%d", ts.URL, tagT, artistU)) + if len(got.Events) != 0 { + t.Fatalf("tag + other artist ids = %v, want none", eventIDs(got.Events)) + } + + got = getEventsBody(t, fmt.Sprintf("%s/events?tag_id=%d&event_type=guest_feature", ts.URL, tagT)) + if want := []int64{guest}; !sameIDs(eventIDs(got.Events), want) { + t.Fatalf("tag + guest_feature ids = %v, want %v", eventIDs(got.Events), want) + } + + got = getEventsBody(t, fmt.Sprintf("%s/events?tag_id=%d", ts.URL, goneTag)) + if got.Events == nil || len(got.Events) != 0 || got.HasOlderEvents { + t.Fatalf("unknown tag = %+v, want events [] and has_older_events false", got) + } +} From f444b94b66b0a27598e5b5647f743005fb997f6e Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:37:35 -0500 Subject: [PATCH 011/142] docs(25-02): complete History tag filter plan --- .planning/REQUIREMENTS.md | 4 +- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 15 +- .../25-02-SUMMARY.md | 150 ++++++++++++++++++ 4 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 .planning/phases/25-find-filter-watchlist-and-history/25-02-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 1fdffbd..252e974 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -31,7 +31,7 @@ ### History -- [ ] **HIST-02**: User can filter the History feed by tag, combined with existing filters and respecting event retention +- [x] **HIST-02**: User can filter the History feed by tag, combined with existing filters and respecting event retention ### Bulk Edit @@ -95,7 +95,7 @@ Which phases cover which requirements. Updated during roadmap creation. | WLVW-04 | Phase 25 | Pending | | WLVW-05 | Phase 25 | Pending | | WLVW-06 | Phase 25 | Pending | -| HIST-02 | Phase 25 | Pending | +| HIST-02 | Phase 25 | Complete | | BULK-01 | Phase 26 | Pending | | BULK-02 | Phase 26 | Pending | | BULK-03 | Phase 26 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 3dcb363..cb561aa 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -193,7 +193,7 @@ Plans: 4. The user can filter History by a tag alongside the existing filters. It lists events for artists carrying that tag, including artists since removed from the watchlist, and it never shows an event older than the retention window, either on the first page or via "load older". 5. Opening the Watchlist makes one request regardless of watchlist size. Searching, sorting, and filtering then happen instantly with no further requests. -**Plans:** 2/10 plans executed +**Plans:** 3/10 plans executed Plans: **Wave 1** @@ -203,7 +203,7 @@ Plans: **Wave 2** *(blocked on Wave 1 completion)* -- [ ] 25-02-PLAN.md — GET /events `tag_id` filter inside the retention-aware ListEvents and HasOlderEvents, with retention, paging, and validation tests +- [x] 25-02-PLAN.md — GET /events `tag_id` filter inside the retention-aware ListEvents and HasOlderEvents, with retention, paging, and validation tests - [ ] 25-04-PLAN.md — Watchlist name filter (accent and `$ø æ ß` folding), "N of M artists", URL state helper, Clear filters and filtered-to-zero state, live announcements, and sticky cards (G4) - [ ] 25-06-PLAN.md — History Tag control (D-11), `Combobox` hardening, loading/unavailable labels, and the tag empty state with `Show all tags` @@ -327,7 +327,7 @@ Phases execute in numeric order: 24 → 25 → 26 → 27 → 28 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 24. Artist Tags & Notes | 14/14 | Complete | 2026-10-06 | -| 25. Find & Filter — Watchlist and History | 2/10 | In Progress| | +| 25. Find & Filter — Watchlist and History | 3/10 | In Progress| | | 26. Bulk Edit & Remove | 0/? | Not started | - | | 27. Paste-a-List Bulk Add | 0/? | Not started | - | | 28. Tags on Discord Notifications | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index ab7c43a..c052b01 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,16 +5,16 @@ milestone_name: Watchlist Organization current_phase: 25 current_phase_name: Find & Filter — Watchlist and History status: executing -stopped_at: Completed 25-03-PLAN.md -last_updated: "2026-10-10T16:26:14.453Z" +stopped_at: Completed 25-02-PLAN.md +last_updated: "2026-10-10T16:37:29.600Z" last_activity: 2026-10-10 last_activity_desc: Phase 25 execution started -state_head: 10e454d1501e74bf47a6c81c17847817ab8ce963 +state_head: 00e0ada558885d2ff83a7c713a5cafdbde100b56 progress: total_phases: 5 completed_phases: 1 total_plans: 24 - completed_plans: 16 + completed_plans: 17 percent: 20 --- @@ -30,7 +30,7 @@ See: .planning/PROJECT.md (updated 2026-10-06) ## Current Position Phase: 25 (Find & Filter — Watchlist and History) — EXECUTING -Plan: 3 of 10 +Plan: 4 of 10 Status: Ready to execute Last activity: 2026-10-10 — Phase 25 execution started @@ -164,6 +164,7 @@ Last activity: 2026-10-10 — Phase 25 execution started | Phase 24 P14 | 20 min | 3 tasks | 5 files | | Phase 25 P01 | 25 min | 2 tasks | 6 files | | Phase 25 P03 | 8 min | 2 tasks | 14 files | +| Phase 25 P02 | 25min | 2 tasks | 5 files | ## Accumulated Context @@ -486,8 +487,8 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-10T16:26:14.370Z -Stopped at: Completed 25-03-PLAN.md +Last session: 2026-10-10T16:37:29.528Z +Stopped at: Completed 25-02-PLAN.md Resume file: None ## Operator Next Steps diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-02-SUMMARY.md b/.planning/phases/25-find-filter-watchlist-and-history/25-02-SUMMARY.md new file mode 100644 index 0000000..350be9c --- /dev/null +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-02-SUMMARY.md @@ -0,0 +1,150 @@ +--- +phase: 25-find-filter-watchlist-and-history +plan: 02 +subsystem: api +tags: [go, chi, sqlc, postgres, events, tags, retention] + +requires: + - phase: 25-find-filter-watchlist-and-history + provides: "25-01 sqlc regeneration (serialized to keep sqlc-check clean)" +provides: + - "GET /events?tag_id=N filtered by an artist_tags EXISTS predicate inside ListEvents and HasOlderEvents" + - "events.ListParams.TagID passed to both queries from one retention value" + - "tag_id validation (400 invalid tag_id) before the store is called" +affects: [25-06 History Tag control, listEvents({ tagId }) client] + +actuals: + tokens: 5340 + tasks: 2 + commits: 2 +plan_head_before: 66054460b461e37f24f92bad404ff72ef5523624 +commits: 2 + +tech-stack: + added: [] + patterns: + - "Optional sqlc.narg filter as an EXISTS semi-join, repeated verbatim in the list query and its has-older companion" + +key-files: + created: [] + modified: + - queries/events.sql + - internal/db/sqlc/events.sql.go + - internal/events/service.go + - internal/httpserver/events.go + - internal/httpserver/events_test.go + +key-decisions: + - "Tag predicate keys on events.artist_id via artist_tags, so removed artists (no watchlist row) still match (TAG-07)." + - "Reused parseOptionalPositiveInt64 for tag_id: absent/empty is no filter, malformed/<1 is 400 invalid tag_id." + +patterns-established: + - "Any new History filter goes into both ListEvents and HasOlderEvents so retention and has_older_events stay scoped together." + +requirements-completed: [HIST-02] + +coverage: + - id: D1 + description: "GET /events?tag_id=N lists only events of artists carrying tag N, including artists with no watchlist row" + requirement: HIST-02 + verification: + - kind: integration + ref: "internal/httpserver/events_test.go#TestListEvents_TagFilter" + status: pass + human_judgment: false + - id: D2 + description: "No aged-out event appears under a tag filter on the first page, any cursor page, or in has_older_events" + requirement: HIST-02 + verification: + - kind: integration + ref: "internal/httpserver/events_test.go#TestRetention_TagFilterComposesWithCutoff" + status: pass + human_judgment: false + - id: D3 + description: "tag_id composes with artist_id and event_type; unknown tag id gives an empty page with has_older_events false" + requirement: HIST-02 + verification: + - kind: integration + ref: "internal/httpserver/events_test.go#TestListEvents_TagFilterComposesWithOtherFilters" + status: pass + human_judgment: false + - id: D4 + description: "Malformed tag_id returns 400 invalid tag_id without calling the store; TagID reaches both queries" + requirement: HIST-02 + verification: + - kind: unit + ref: "internal/httpserver/events_test.go#TestHandleListEvents_Validation" + status: pass + human_judgment: false + +duration: 25min +completed: 2026-10-10 +status: complete +--- + +# Phase 25 Plan 02: History tag filter API Summary + +**GET /events accepts tag_id via an artist_tags EXISTS predicate placed inside the retention-aware ListEvents and HasOlderEvents queries, so a tag filter can never surface aged-out events.** + +## Performance + +- **Duration:** 25 min +- **Started:** 2026-10-10T16:12:00Z +- **Completed:** 2026-10-10T16:37:06Z +- **Tasks:** 2 +- **Files modified:** 5 + +## Accomplishments +- Tag predicate added to both queries; sqlc regenerated with `TagID *int64` on both param structs. +- `events.ListParams.TagID` flows into both queries alongside the single retention cutoff. +- `handleListEvents` parses `tag_id` with the fixed `invalid tag_id` 400 message. +- Tests prove removed-artist matching, retention on the first page and every cursor page, `has_older_events` scoping, AND composition, the unknown-tag empty page, and the validation contract. + +## Task Commits + +1. **Task 1: Filter GET /events by one tag, end to end** - `c421197` (feat) +2. **Task 2: Retention, paging, composition, and validation under a tag filter** - `00e0ada` (test) + +**Plan metadata:** committed separately (docs: complete plan) + +_Note: Task 2 is tdd="true", but the Task 1 tracer already shipped the implementation, so its tests passed on first run with no separate RED commit._ + +## Files Created/Modified +- `queries/events.sql` - `sqlc.narg('tag_id')` EXISTS predicate in ListEvents and HasOlderEvents +- `internal/db/sqlc/events.sql.go` - regenerated; `TagID` on both param structs +- `internal/events/service.go` - `ListParams.TagID`, passed to both queries +- `internal/httpserver/events.go` - `tag_id` parsing and doc comment +- `internal/httpserver/events_test.go` - tag/link helpers, three integration tests, validation cases, `recordingQuerier.gotOlderParams` + +## Decisions Made +None - followed plan as specified. + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered +- `make` is not on PATH and cgo is disabled, so the Makefile recipes were run directly without `-race`. `sqlc-check` was run as `git add internal/db/sqlc`, `sqlc generate`, `git diff --exit-code` (clean); the sqlc version matches the pinned v1.31.1. +- Coverage gate recipe run directly: backend total 90.25% (floor 80%). Go printed `no such tool "covdata"` for `internal/testutil` (no test files); the profile and total are unaffected. +- `gofmt -l` lists five pre-existing files (`artistart/match_test.go`, `httpserver/server.go`, `settings_test.go`, `notifier/timeout_test.go`, `sqlscan/lex.go`). None were touched by this plan. + +## Known Stubs +None. + +## Threat Flags +None - the only new surface is the `tag_id` query parameter covered by T-25-05 to T-25-07. It is validated before the store and bound as a sqlc parameter. + +## User Setup Required +None - no external service configuration required. + +## Next Phase Readiness +- `GET /events?tag_id=N` is ready for 25-06's History Tag control (`listEvents({ tagId })`). + +## Self-Check: PASSED + +- FOUND: queries/events.sql, internal/db/sqlc/events.sql.go, internal/events/service.go, internal/httpserver/events.go, internal/httpserver/events_test.go +- FOUND commits: c421197, 00e0ada + +--- +*Phase: 25-find-filter-watchlist-and-history* +*Completed: 2026-10-10* From 115e13fd06f0505d1cf1e66f9e2826db63213b78 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:39:24 -0500 Subject: [PATCH 012/142] feat(25-04): filter the Watchlist by name with live count and URL state - pure watchlistView fold/match/select helpers, kept out of api.ts - useUrlParams as the single URL writer (preventScrollReset always on) - Your artists section, name filter toolbar, debounced q write --- .../components/watchlist/WatchlistToolbar.tsx | 48 ++++++++ web/app/lib/test/routeStub.tsx | 15 ++- web/app/lib/useUrlParams.ts | 29 +++++ web/app/lib/watchlistView.ts | 44 ++++++++ web/app/routes/watchlist.test.tsx | 84 +++++++++++++- web/app/routes/watchlist.tsx | 103 +++++++++++++++--- 6 files changed, 306 insertions(+), 17 deletions(-) create mode 100644 web/app/components/watchlist/WatchlistToolbar.tsx create mode 100644 web/app/lib/useUrlParams.ts create mode 100644 web/app/lib/watchlistView.ts diff --git a/web/app/components/watchlist/WatchlistToolbar.tsx b/web/app/components/watchlist/WatchlistToolbar.tsx new file mode 100644 index 0000000..6054db2 --- /dev/null +++ b/web/app/components/watchlist/WatchlistToolbar.tsx @@ -0,0 +1,48 @@ +import { ListFilter } from "lucide-react" + +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from "~/components/ui/input-group" + +export interface WatchlistToolbarProps { + query: string + onQueryChange(next: string): void + filterInputRef: React.RefObject + showClear: boolean + onClearFilters(): void +} + +export function WatchlistToolbar({ + query, + onQueryChange, + filterInputRef, +}: WatchlistToolbarProps) { + return ( +
+ +
+ ) +} diff --git a/web/app/lib/test/routeStub.tsx b/web/app/lib/test/routeStub.tsx index fbd7796..0b9f879 100644 --- a/web/app/lib/test/routeStub.tsx +++ b/web/app/lib/test/routeStub.tsx @@ -7,9 +7,18 @@ import type { ComponentType } from "react" import { render } from "@testing-library/react" -import { createRoutesStub } from "react-router" +import { createRoutesStub, useLocation } from "react-router" -export function renderRoute(Component: ComponentType, path: string) { +export function renderRoute( + Component: ComponentType, + path: string, + initialEntry: string = path +) { const Stub = createRoutesStub([{ path, Component }]) - return render() + return render() +} + +// Exposes the current query string so tests can assert URL writes. +export function LocationProbe() { + return {useLocation().search} } diff --git a/web/app/lib/useUrlParams.ts b/web/app/lib/useUrlParams.ts new file mode 100644 index 0000000..6c94bb2 --- /dev/null +++ b/web/app/lib/useUrlParams.ts @@ -0,0 +1,29 @@ +import { useCallback, useRef } from "react" +import { useSearchParams } from "react-router" + +export function urlWriteOptions(opts: { replace?: boolean } = {}) { + return { replace: opts.replace ?? false, preventScrollReset: true as const } +} + +// The one URL writer. It builds on a ref so two writes in one tick compose, +// and always passes preventScrollReset so filtering never jumps the page. +export function useUrlParams() { + const [params, setParams] = useSearchParams() + const latestRef = useRef(params) + latestRef.current = params + + const write = useCallback( + ( + mutate: (next: URLSearchParams) => void, + opts: { replace?: boolean } = {} + ) => { + const next = new URLSearchParams(latestRef.current) + mutate(next) + latestRef.current = next + setParams(next, urlWriteOptions(opts)) + }, + [setParams] + ) + + return { params, write } +} diff --git a/web/app/lib/watchlistView.ts b/web/app/lib/watchlistView.ts new file mode 100644 index 0000000..ccc6741 --- /dev/null +++ b/web/app/lib/watchlistView.ts @@ -0,0 +1,44 @@ +import type { WatchlistEntry } from "~/lib/api" + +// Pure view-model: never import runtime values from ~/lib/api here, route +// tests automock that module. + +const FOLD: Record = { $: "s", ø: "o", æ: "ae", ß: "ss" } + +// NFD does not decompose ø, æ, ß, so they go through a table (G5). +export function foldName(s: string): string { + return s + .normalize("NFD") + .replace(/\p{M}/gu, "") + .toLowerCase() + .replace(/[$øæß]/g, (c) => FOLD[c]) +} + +export function matchesName(name: string, query: string): boolean { + const trimmed = query.trim() + if (trimmed === "") return true + return foldName(name).includes(foldName(trimmed)) +} + +export interface WatchlistView { + q: string +} + +export function parseWatchlistParams(params: URLSearchParams): WatchlistView { + return { q: params.get("q") ?? "" } +} + +// An entry is visible when it matches every active axis or is sticky (G4). +export function selectVisible( + entries: WatchlistEntry[], + view: WatchlistView, + stickyIds: ReadonlySet +): WatchlistEntry[] { + return entries.filter( + (entry) => stickyIds.has(entry.id) || matchesName(entry.name, view.q) + ) +} + +export function formatArtistCount(n: number, m: number): string { + return `${n} of ${m} artist${m === 1 ? "" : "s"}` +} diff --git a/web/app/routes/watchlist.test.tsx b/web/app/routes/watchlist.test.tsx index a7de09e..3afc634 100644 --- a/web/app/routes/watchlist.test.tsx +++ b/web/app/routes/watchlist.test.tsx @@ -18,6 +18,7 @@ import { attachTag, deleteTag, detachTag, + listEvents, listTags, listWatchlist, mergeTag, @@ -27,7 +28,7 @@ import { type WatchlistEntry, } from "~/lib/api" import { makeEntry } from "~/lib/test/fixtures" -import { renderRoute } from "~/lib/test/routeStub" +import { LocationProbe, renderRoute } from "~/lib/test/routeStub" import Watchlist from "./watchlist" @@ -983,3 +984,84 @@ describe("Watchlist route under the passphrase gate (GATE-05 / UI-SPEC E6)", () expect(card.querySelector("b")).toBeNull() }) }) + +describe("Watchlist name filter (WLVW-01)", () => { + const filterEntries = [ + makeEntry({ id: 1, name: "Bad Bunny" }), + makeEntry({ id: 2, name: "Beyoncé", disambiguation: "bad influence" }), + makeEntry({ id: 3, name: "A$AP Rocky" }), + ] + + function WatchlistWithProbe() { + return ( + <> + + + + ) + } + + it("narrows the list on every keystroke, ignores disambiguation, and keeps the count in step", async () => { + mockListWatchlist.mockResolvedValue(filterEntries) + + renderRoute(Watchlist, "/") + + await screen.findByText("Bad Bunny") + expect(screen.getByText("3 of 3 artists")).toBeInTheDocument() + + await userEvent.type( + screen.getByRole("textbox", { name: "Filter watchlist" }), + "bad" + ) + + expect(screen.getByText("Bad Bunny")).toBeInTheDocument() + expect(screen.queryByText("Beyoncé")).not.toBeInTheDocument() + expect(screen.queryByText("A$AP Rocky")).not.toBeInTheDocument() + expect(screen.getByText("1 of 3 artists")).toBeInTheDocument() + }) + + it("writes q to the URL after the debounce", async () => { + mockListWatchlist.mockResolvedValue(filterEntries) + + renderRoute(WatchlistWithProbe, "/") + await screen.findByText("Bad Bunny") + + await userEvent.type( + screen.getByRole("textbox", { name: "Filter watchlist" }), + "bad" + ) + + await waitFor(() => + expect(screen.getByTestId("location-search")).toHaveTextContent("?q=bad") + ) + }) + + it("reproduces the filtered view from ?q= on load, with the input pre-filled", async () => { + mockListWatchlist.mockResolvedValue(filterEntries) + + renderRoute(Watchlist, "/", "/?q=bey") + + await screen.findByText("Beyoncé") + expect(screen.queryByText("Bad Bunny")).not.toBeInTheDocument() + expect( + screen.getByRole("textbox", { name: "Filter watchlist" }) + ).toHaveValue("bey") + }) + + it("issues exactly one GET /watchlist while filtering and never asks for tags or events (SC5)", async () => { + mockListWatchlist.mockResolvedValue(filterEntries) + + renderRoute(Watchlist, "/") + await screen.findByText("Bad Bunny") + + await userEvent.type( + screen.getByRole("textbox", { name: "Filter watchlist" }), + "beyonce" + ) + + expect(screen.getByText("Beyoncé")).toBeInTheDocument() + expect(mockListWatchlist).toHaveBeenCalledTimes(1) + expect(vi.mocked(listTags)).not.toHaveBeenCalled() + expect(vi.mocked(listEvents)).not.toHaveBeenCalled() + }) +}) diff --git a/web/app/routes/watchlist.tsx b/web/app/routes/watchlist.tsx index b60e3f6..b0a6c7e 100644 --- a/web/app/routes/watchlist.tsx +++ b/web/app/routes/watchlist.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { Tags } from "lucide-react" import { toast } from "sonner" @@ -9,6 +9,7 @@ import { ManageTagsDialog } from "~/components/watchlist/ManageTagsDialog" import { SearchBox } from "~/components/watchlist/SearchBox" import { SearchResultsColumns } from "~/components/watchlist/SearchResultsColumns" import { WatchlistRow } from "~/components/watchlist/WatchlistRow" +import { WatchlistToolbar } from "~/components/watchlist/WatchlistToolbar" import { ApiError, type SearchArtist, @@ -20,6 +21,12 @@ import { removeWatchlist, } from "~/lib/api" import { isAddableSource } from "~/lib/sources" +import { useUrlParams } from "~/lib/useUrlParams" +import { + formatArtistCount, + parseWatchlistParams, + selectVisible, +} from "~/lib/watchlistView" import { rewriteTags, TagVocabularyProvider, @@ -34,6 +41,9 @@ import { // so a background re-sync (task 2's post-mutation refresh, plan 06-04's // post-add refresh) never flashes the loading skeletons over an already- // populated list -- only the very first mount renders them. +const Q_WRITE_DEBOUNCE_MS = 300 +const NO_STICKY: ReadonlySet = new Set() + export default function Watchlist() { const [entries, setEntries] = useState(null) const [error, setError] = useState(false) @@ -43,6 +53,46 @@ export default function Watchlist() { const [statusMessage, setStatusMessage] = useState("") const [manageTagsOpen, setManageTagsOpen] = useState(false) + const { params, write } = useUrlParams() + const view = parseWatchlistParams(params) + const [query, setQuery] = useState(view.q) + const lastWrittenQ = useRef(view.q) + const qTimer = useRef | null>(null) + const filterInputRef = useRef(null) + const stickyIds = NO_STICKY + + // Filtering uses the local value; the URL only follows, debounced (D-12). + function handleQueryChange(next: string) { + setQuery(next) + if (qTimer.current) clearTimeout(qTimer.current) + qTimer.current = setTimeout(() => { + lastWrittenQ.current = next + write( + (p) => { + if (next === "") p.delete("q") + else p.set("q", next) + }, + { replace: true } + ) + }, Q_WRITE_DEBOUNCE_MS) + } + + // Re-sync the input only for Back/forward or external URL changes, never + // for an echo of our own write. + useEffect(() => { + if (view.q !== lastWrittenQ.current) { + if (qTimer.current) clearTimeout(qTimer.current) + lastWrittenQ.current = view.q + setQuery(view.q) + } + }, [view.q]) + + useEffect(() => { + return () => { + if (qTimer.current) clearTimeout(qTimer.current) + } + }, []) + const refresh = useCallback(() => { setError(false) listWatchlist() @@ -218,6 +268,10 @@ export default function Watchlist() { }) } + const visible = entries + ? selectVisible(entries, { ...view, q: query }, stickyIds) + : [] + return (
@@ -275,18 +329,41 @@ export default function Watchlist() { )} {!error && entries !== null && entries.length > 0 && ( -
    - {entries.map((entry) => ( - - ))} -
+
+
+

+ Your artists +

+

+ {formatArtistCount(visible.length, entries.length)} +

+
+ {}} + /> +
    + {visible.map((entry) => ( + + ))} +
+
)}
From 3062fd971a938740f6e6e583c6554fcdb985f7bd Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:39:56 -0500 Subject: [PATCH 013/142] test(25-04): pin fold table, URL writer, and name filter keys - toolbar clear x and Esc cases fail until implemented --- .../watchlist/WatchlistToolbar.test.tsx | 74 +++++++++++++++++ web/app/lib/useUrlParams.test.tsx | 66 +++++++++++++++ web/app/lib/watchlistView.test.ts | 81 +++++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 web/app/components/watchlist/WatchlistToolbar.test.tsx create mode 100644 web/app/lib/useUrlParams.test.tsx create mode 100644 web/app/lib/watchlistView.test.ts diff --git a/web/app/components/watchlist/WatchlistToolbar.test.tsx b/web/app/components/watchlist/WatchlistToolbar.test.tsx new file mode 100644 index 0000000..082fd07 --- /dev/null +++ b/web/app/components/watchlist/WatchlistToolbar.test.tsx @@ -0,0 +1,74 @@ +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { createRef } from "react" +import { describe, expect, it, vi } from "vitest" + +import { WatchlistToolbar } from "~/components/watchlist/WatchlistToolbar" + +function setup(query: string) { + const onQueryChange = vi.fn() + const filterInputRef = createRef() + render( + {}} + /> + ) + return { onQueryChange, input: screen.getByRole("textbox") } +} + +describe("WatchlistToolbar name filter", () => { + it("labels the group, the input, and the placeholder", () => { + const { input } = setup("") + + expect( + screen.getByRole("group", { name: "Filter and sort artists" }) + ).toBeInTheDocument() + expect(screen.getByRole("textbox", { name: "Filter watchlist" })).toBe( + input + ) + expect(input).toHaveAttribute("placeholder", "Filter by name…") + }) + + it("renders no clear × while the query is empty", () => { + setup("") + + expect( + screen.queryByRole("button", { name: "Clear name filter" }) + ).not.toBeInTheDocument() + }) + + it("clears through the × and keeps focus in the input", async () => { + const { onQueryChange, input } = setup("bad") + + await userEvent.click( + screen.getByRole("button", { name: "Clear name filter" }) + ) + + expect(onQueryChange).toHaveBeenCalledWith("") + expect(input).toHaveFocus() + }) + + it("clears a non-empty value on Esc", async () => { + const { onQueryChange, input } = setup("bad") + + input.focus() + await userEvent.keyboard("{Escape}") + + expect(onQueryChange).toHaveBeenCalledWith("") + expect(input).toHaveFocus() + }) + + it("does nothing on Esc when empty or on Enter", async () => { + const { onQueryChange, input } = setup("") + + input.focus() + await userEvent.keyboard("{Escape}") + await userEvent.keyboard("{Enter}") + + expect(onQueryChange).not.toHaveBeenCalled() + }) +}) diff --git a/web/app/lib/useUrlParams.test.tsx b/web/app/lib/useUrlParams.test.tsx new file mode 100644 index 0000000..5913fe5 --- /dev/null +++ b/web/app/lib/useUrlParams.test.tsx @@ -0,0 +1,66 @@ +import { screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { useNavigate } from "react-router" +import { describe, expect, it } from "vitest" + +import { LocationProbe, renderRoute } from "~/lib/test/routeStub" +import { urlWriteOptions, useUrlParams } from "~/lib/useUrlParams" + +describe("urlWriteOptions", () => { + it("always prevents scroll reset and defaults to push", () => { + expect(urlWriteOptions()).toEqual({ + replace: false, + preventScrollReset: true, + }) + }) + + it("carries replace through", () => { + expect(urlWriteOptions({ replace: true })).toEqual({ + replace: true, + preventScrollReset: true, + }) + }) +}) + +function Harness() { + const { write } = useUrlParams() + const navigate = useNavigate() + return ( + <> + + + + + + ) +} + +describe("useUrlParams", () => { + it("lets two writes in one handler both land", async () => { + renderRoute(Harness, "/") + + await userEvent.click(screen.getByRole("button", { name: "write twice" })) + + const search = screen.getByTestId("location-search").textContent ?? "" + expect(search).toContain("a=1") + expect(search).toContain("b=2") + }) + + it("keeps params set by an external navigation on the next write", async () => { + renderRoute(Harness, "/") + + await userEvent.click(screen.getByRole("button", { name: "navigate" })) + await userEvent.click(screen.getByRole("button", { name: "write once" })) + + const search = screen.getByTestId("location-search").textContent ?? "" + expect(search).toContain("x=1") + expect(search).toContain("a=1") + }) +}) diff --git a/web/app/lib/watchlistView.test.ts b/web/app/lib/watchlistView.test.ts new file mode 100644 index 0000000..93d5144 --- /dev/null +++ b/web/app/lib/watchlistView.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "vitest" + +import { makeEntry } from "~/lib/test/fixtures" +import { + foldName, + formatArtistCount, + matchesName, + parseWatchlistParams, + selectVisible, +} from "~/lib/watchlistView" + +describe("matchesName", () => { + it.each([ + ["Beyoncé", "beyonce"], + ["A$AP Rocky", "asap"], + ["Mø", "mo"], + ["Ænima", "aenima"], + ["Straße", "strasse"], + ["Bad Bunny", "BAD"], + ])("matches %s for %s", (name, query) => { + expect(matchesName(name, query)).toBe(true) + }) + + it("matches everything for a blank or whitespace-only query", () => { + expect(matchesName("Drake", "")).toBe(true) + expect(matchesName("Drake", " ")).toBe(true) + }) + + it("ignores surrounding whitespace in the query", () => { + expect(matchesName("Bad Bunny", " bad ")).toBe(true) + }) + + it("rejects a non-matching name", () => { + expect(matchesName("Drake", "bad")).toBe(false) + }) +}) + +describe("foldName", () => { + it("folds Ø and Æ after lowercasing", () => { + expect(foldName("ØÆ")).toBe("oae") + }) +}) + +describe("selectVisible", () => { + const entries = [ + makeEntry({ id: 1, name: "Bad Bunny" }), + makeEntry({ id: 2, name: "Drake" }), + makeEntry({ id: 3, name: "Badmómó" }), + ] + + it("keeps input order", () => { + const ids = selectVisible(entries, { q: "bad" }, new Set()).map((e) => e.id) + expect(ids).toEqual([1, 3]) + }) + + it("unions sticky ids with matches", () => { + const ids = selectVisible(entries, { q: "bad" }, new Set([2])).map( + (e) => e.id + ) + expect(ids).toEqual([1, 2, 3]) + }) + + it("returns an empty list for no entries", () => { + expect(selectVisible([], { q: "x" }, new Set([1]))).toEqual([]) + }) +}) + +describe("parseWatchlistParams", () => { + it("reads q and defaults to empty", () => { + expect(parseWatchlistParams(new URLSearchParams("q=bad")).q).toBe("bad") + expect(parseWatchlistParams(new URLSearchParams("")).q).toBe("") + }) +}) + +describe("formatArtistCount", () => { + it("pluralizes on the total", () => { + expect(formatArtistCount(1, 1)).toBe("1 of 1 artist") + expect(formatArtistCount(0, 52)).toBe("0 of 52 artists") + expect(formatArtistCount(52, 52)).toBe("52 of 52 artists") + }) +}) From e11bb20e6a2cece27a9dd439f90ee72e394346ba Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:40:16 -0500 Subject: [PATCH 014/142] feat(25-04): add the name filter clear button and Esc handling --- .../components/watchlist/WatchlistToolbar.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/web/app/components/watchlist/WatchlistToolbar.tsx b/web/app/components/watchlist/WatchlistToolbar.tsx index 6054db2..8d5bd8f 100644 --- a/web/app/components/watchlist/WatchlistToolbar.tsx +++ b/web/app/components/watchlist/WatchlistToolbar.tsx @@ -1,8 +1,9 @@ -import { ListFilter } from "lucide-react" +import { ListFilter, X } from "lucide-react" import { InputGroup, InputGroupAddon, + InputGroupButton, InputGroupInput, } from "~/components/ui/input-group" @@ -40,7 +41,27 @@ export function WatchlistToolbar({ spellCheck={false} value={query} onChange={(e) => onQueryChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Escape" && query !== "") { + e.preventDefault() + onQueryChange("") + } + }} /> + {query !== "" && ( + + { + onQueryChange("") + filterInputRef.current?.focus() + }} + > + + + )} From e431c1306423a886bba133bd0791bef244de6b84 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:41:51 -0500 Subject: [PATCH 015/142] test(25-04): pin clear filters, announcements, and sticky adds --- web/app/routes/watchlist.test.tsx | 233 ++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) diff --git a/web/app/routes/watchlist.test.tsx b/web/app/routes/watchlist.test.tsx index 3afc634..9b20249 100644 --- a/web/app/routes/watchlist.test.tsx +++ b/web/app/routes/watchlist.test.tsx @@ -24,6 +24,7 @@ import { mergeTag, removeWatchlist, renameTag, + searchArtists, type TagSummary, type WatchlistEntry, } from "~/lib/api" @@ -1065,3 +1066,235 @@ describe("Watchlist name filter (WLVW-01)", () => { expect(vi.mocked(listEvents)).not.toHaveBeenCalled() }) }) + +describe("Watchlist filtered state (WLVW-06)", () => { + const three = [ + makeEntry({ id: 1, name: "Bad Bunny" }), + makeEntry({ id: 2, name: "Beyoncé" }), + makeEntry({ id: 3, name: "A$AP Rocky" }), + ] + const filterBox = () => + screen.getByRole("textbox", { name: "Filter watchlist" }) + + function WatchlistWithProbe() { + return ( + <> + + + + ) + } + + function status() { + return screen.getByRole("status") + } + + function lastUndo() { + const calls = vi.mocked(toast.success).mock.calls + const call = calls[calls.length - 1] as [ + string, + { action?: { onClick: () => void } }, + ] + return call[1].action + } + + it("replaces the list with the filtered-to-zero state and clears everything but sort", async () => { + mockListWatchlist.mockResolvedValue(three) + + renderRoute(WatchlistWithProbe, "/", "/?q=zzz&sort=latest-desc") + + await screen.findByRole("heading", { + name: "No artists match these filters", + }) + expect( + screen.getByText( + "Try a different name or fewer filters, or clear them to see all 3 artists." + ) + ).toBeInTheDocument() + expect(screen.getByText("0 of 3 artists")).toBeInTheDocument() + expect(screen.queryByRole("list")).not.toBeInTheDocument() + const clearButtons = screen.getAllByRole("button", { + name: "Clear filters", + }) + expect(clearButtons).toHaveLength(1) + + await userEvent.click(clearButtons[0]) + + await waitFor(() => + expect(screen.getByTestId("location-search")).toHaveTextContent( + /^\?sort=latest-desc$/ + ) + ) + expect(filterBox()).toHaveValue("") + expect(filterBox()).toHaveFocus() + expect(status()).toHaveTextContent( + "Filters cleared. Showing 3 of 3 artists." + ) + expect(screen.getByText("Bad Bunny")).toBeInTheDocument() + }) + + it("shows the toolbar Clear filters only for a non-blank query with results", async () => { + mockListWatchlist.mockResolvedValue(three) + + renderRoute(Watchlist, "/") + await screen.findByText("Bad Bunny") + const clear = () => + screen.queryAllByRole("button", { name: "Clear filters" }) + expect(clear()).toHaveLength(0) + + await userEvent.type(filterBox(), " ") + expect(clear()).toHaveLength(0) + + await userEvent.clear(filterBox()) + await userEvent.type(filterBox(), "bad") + expect(clear()).toHaveLength(1) + expect(screen.getByText("Bad Bunny")).toBeInTheDocument() + + await userEvent.type(filterBox(), "zzz") + await screen.findByRole("heading", { + name: "No artists match these filters", + }) + expect(clear()).toHaveLength(1) + expect( + clear()[0].closest("[aria-label='Filter and sort artists']") + ).toBeNull() + }) + + it("announces the count 500ms after the last keystroke", async () => { + mockListWatchlist.mockResolvedValue(three) + + renderRoute(Watchlist, "/") + await screen.findByText("Bad Bunny") + expect(status()).toHaveTextContent("") + + await userEvent.type(filterBox(), "bad") + await waitFor( + () => expect(status()).toHaveTextContent("Showing 1 of 3 artists."), + { timeout: 2000 } + ) + }) + + it("announces when nothing matches", async () => { + mockListWatchlist.mockResolvedValue(three) + + renderRoute(Watchlist, "/") + await screen.findByText("Bad Bunny") + + await userEvent.type(filterBox(), "zzz") + await waitFor( + () => + expect(status()).toHaveTextContent("No artists match these filters."), + { timeout: 2000 } + ) + }) + + it("keeps an artist added from search visible under an active filter, until the search text changes", async () => { + const drizzy = makeEntry({ id: 99, name: "Drizzy", mbid: "mbid-99" }) + mockListWatchlist + .mockResolvedValueOnce([three[0], three[1]]) + .mockResolvedValueOnce([three[0], three[1], drizzy]) + vi.mocked(searchArtists).mockResolvedValue({ + query: "Drizzy", + sources: { + musicbrainz: { + status: "ok", + artists: [ + { + source: "musicbrainz", + id: "mbid-99", + name: "Drizzy", + disambiguation: null, + country: null, + type: "Person", + image_url: null, + }, + ], + }, + }, + }) + mockAddWatchlist.mockResolvedValue(drizzy) + + renderRoute(Watchlist, "/", "/?q=bad") + await screen.findByText("Bad Bunny") + expect(screen.queryByText("Beyoncé")).not.toBeInTheDocument() + + await userEvent.type( + screen.getByRole("textbox", { name: "Search artists" }), + "Drizzy" + ) + await userEvent.click( + await screen.findByRole("button", { name: "Add to Watchlist" }) + ) + + await screen.findByRole("button", { name: "Remove Drizzy from watchlist" }) + expect(screen.getByText("2 of 3 artists")).toBeInTheDocument() + + await userEvent.type(filterBox(), "x") + await waitFor(() => + expect( + screen.queryByRole("button", { name: "Remove Drizzy from watchlist" }) + ).not.toBeInTheDocument() + ) + }) + + it("keeps an Undo-restored artist visible under an active filter", async () => { + const bunny = three[0] + const restored = makeEntry({ id: 77, name: "Bad Bunny", mbid: bunny.mbid }) + mockListWatchlist + .mockResolvedValueOnce([bunny, three[1]]) + .mockResolvedValueOnce([three[1], restored]) + mockRemoveWatchlist.mockResolvedValueOnce(undefined) + mockAddWatchlist.mockResolvedValue(restored) + + renderRoute(Watchlist, "/", "/?q=bad") + await screen.findByText("Bad Bunny") + + await userEvent.click( + screen.getByRole("button", { name: "Remove Bad Bunny from watchlist" }) + ) + await waitFor(() => expect(mockRemoveWatchlist).toHaveBeenCalled()) + await userEvent.type(filterBox(), "x") + + lastUndo()?.onClick() + + await screen.findByRole("button", { + name: "Remove Bad Bunny from watchlist", + }) + expect(screen.getByText("1 of 2 artists")).toBeInTheDocument() + }) + + it("renders no section or toolbar while loading or with no artists", async () => { + const pending = deferred() + mockListWatchlist.mockReturnValueOnce(pending.promise) + + const view = renderRoute(Watchlist, "/") + expect( + screen.queryByRole("textbox", { name: "Filter watchlist" }) + ).not.toBeInTheDocument() + expect(view.container.querySelectorAll(".h-24")).toHaveLength(3) + + await act(async () => { + pending.resolve([]) + }) + await screen.findByRole("heading", { name: "No artists yet" }) + expect( + screen.queryByRole("heading", { name: "Your artists" }) + ).not.toBeInTheDocument() + expect( + screen.queryByRole("textbox", { name: "Filter watchlist" }) + ).not.toBeInTheDocument() + }) + + it("shows no toolbar when the load fails", async () => { + mockListWatchlist.mockRejectedValueOnce(new Error("down")) + + renderRoute(Watchlist, "/") + + await screen.findByRole("heading", { + name: "Couldn't load your watchlist.", + }) + expect( + screen.queryByRole("textbox", { name: "Filter watchlist" }) + ).not.toBeInTheDocument() + }) +}) From 882a04777502907dffa003d3f8fbb50e4d4f9e96 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:43:57 -0500 Subject: [PATCH 016/142] feat(25-04): add Clear filters, filtered-to-zero state, announcements, sticky cards - Clear filters resets q/tags/muted/custom in one push and keeps sort - count announcements debounced 500ms through the route status region - in-card edits, adds, and Undo restores join the sticky set (G4) --- .../components/watchlist/WatchlistToolbar.tsx | 15 ++- web/app/lib/watchlistView.ts | 7 ++ web/app/routes/watchlist.test.tsx | 7 +- web/app/routes/watchlist.tsx | 115 ++++++++++++++---- 4 files changed, 120 insertions(+), 24 deletions(-) diff --git a/web/app/components/watchlist/WatchlistToolbar.tsx b/web/app/components/watchlist/WatchlistToolbar.tsx index 8d5bd8f..0d791f7 100644 --- a/web/app/components/watchlist/WatchlistToolbar.tsx +++ b/web/app/components/watchlist/WatchlistToolbar.tsx @@ -1,5 +1,6 @@ -import { ListFilter, X } from "lucide-react" +import { FilterX, ListFilter, X } from "lucide-react" +import { Button } from "~/components/ui/button" import { InputGroup, InputGroupAddon, @@ -19,6 +20,8 @@ export function WatchlistToolbar({ query, onQueryChange, filterInputRef, + showClear, + onClearFilters, }: WatchlistToolbarProps) { return (
+ {showClear && ( + + )}
) } diff --git a/web/app/lib/watchlistView.ts b/web/app/lib/watchlistView.ts index ccc6741..3296054 100644 --- a/web/app/lib/watchlistView.ts +++ b/web/app/lib/watchlistView.ts @@ -39,6 +39,13 @@ export function selectVisible( ) } +export function isFiltered(view: WatchlistView): boolean { + return view.q.trim() !== "" +} + +// What Clear filters deletes; sort is deliberately absent (D-10). +export const CLEAR_FILTER_KEYS = ["q", "tags", "muted", "custom"] as const + export function formatArtistCount(n: number, m: number): string { return `${n} of ${m} artist${m === 1 ? "" : "s"}` } diff --git a/web/app/routes/watchlist.test.tsx b/web/app/routes/watchlist.test.tsx index 9b20249..7d0a470 100644 --- a/web/app/routes/watchlist.test.tsx +++ b/web/app/routes/watchlist.test.tsx @@ -1085,8 +1085,13 @@ describe("Watchlist filtered state (WLVW-06)", () => { ) } + // LocationProbe's is also an implicit status role. function status() { - return screen.getByRole("status") + const region = screen + .getAllByRole("status") + .find((el) => el.getAttribute("aria-atomic") === "true") + if (!region) throw new Error("route status region not found") + return region } function lastUndo() { diff --git a/web/app/routes/watchlist.tsx b/web/app/routes/watchlist.tsx index b0a6c7e..6589012 100644 --- a/web/app/routes/watchlist.tsx +++ b/web/app/routes/watchlist.tsx @@ -23,7 +23,9 @@ import { import { isAddableSource } from "~/lib/sources" import { useUrlParams } from "~/lib/useUrlParams" import { + CLEAR_FILTER_KEYS, formatArtistCount, + isFiltered, parseWatchlistParams, selectVisible, } from "~/lib/watchlistView" @@ -42,7 +44,7 @@ import { // post-add refresh) never flashes the loading skeletons over an already- // populated list -- only the very first mount renders them. const Q_WRITE_DEBOUNCE_MS = 300 -const NO_STICKY: ReadonlySet = new Set() +const COUNT_ANNOUNCE_DEBOUNCE_MS = 500 export default function Watchlist() { const [entries, setEntries] = useState(null) @@ -59,11 +61,38 @@ export default function Watchlist() { const lastWrittenQ = useRef(view.q) const qTimer = useRef | null>(null) const filterInputRef = useRef(null) - const stickyIds = NO_STICKY + const countTimer = useRef | null>(null) + const countRef = useRef({ n: 0, m: 0 }) + // Cards the user just touched or added stay visible past a filter (G4). + const [stickyIds, setStickyIds] = useState>( + () => new Set() + ) + + function markSticky(id: number) { + setStickyIds((prev) => (prev.has(id) ? prev : new Set(prev).add(id))) + } + + function clearSticky() { + setStickyIds((prev) => (prev.size === 0 ? prev : new Set())) + } + + function scheduleCountAnnouncement() { + if (countTimer.current) clearTimeout(countTimer.current) + countTimer.current = setTimeout(() => { + const { n, m } = countRef.current + announce( + n > 0 + ? `Showing ${formatArtistCount(n, m)}.` + : "No artists match these filters." + ) + }, COUNT_ANNOUNCE_DEBOUNCE_MS) + } // Filtering uses the local value; the URL only follows, debounced (D-12). function handleQueryChange(next: string) { setQuery(next) + clearSticky() + scheduleCountAnnouncement() if (qTimer.current) clearTimeout(qTimer.current) qTimer.current = setTimeout(() => { lastWrittenQ.current = next @@ -90,14 +119,22 @@ export default function Watchlist() { useEffect(() => { return () => { if (qTimer.current) clearTimeout(qTimer.current) + if (countTimer.current) clearTimeout(countTimer.current) } }, []) + // Resolves with the loaded rows (null on failure); never touches stickyIds. const refresh = useCallback(() => { setError(false) - listWatchlist() - .then((rows) => setEntries(rows)) - .catch(() => setError(true)) + return listWatchlist() + .then((rows) => { + setEntries(rows) + return rows + }) + .catch(() => { + setError(true) + return null + }) }, []) useEffect(() => { @@ -114,6 +151,7 @@ export default function Watchlist() { // concurrent single-axis PATCHes must never let one axis's response // clobber the other axis's already-applied value. function handleEntryChange(id: number, patch: Partial) { + markSticky(id) setEntries((rows) => rows ? rows.map((r) => (r.id === id ? { ...r, ...patch } : r)) : rows ) @@ -123,6 +161,7 @@ export default function Watchlist() { // don't clobber each other (D-24). addTag is idempotent and restores the // chip's position on rollback. function addTag(entryId: number, tag: TagRef, index?: number) { + markSticky(entryId) setEntries((rows) => rows ? rows.map((r) => { @@ -142,6 +181,7 @@ export default function Watchlist() { } function removeTag(entryId: number, tagId: number) { + markSticky(entryId) setEntries((rows) => rows ? rows.map((r) => @@ -205,17 +245,20 @@ export default function Watchlist() { return } try { - await addWatchlist({ + const added = await addWatchlist({ mbid: result.id, name: result.name, imageUrl: result.image_url ?? undefined, disambiguation: result.disambiguation ?? undefined, deezerId: undefined, }) + markSticky(added.id) refresh() } catch (err) { if (err instanceof ApiError && err.status === 409) { - refresh() + const rows = await refresh() + const existing = rows?.find((r) => r.mbid === result.id) + if (existing) markSticky(existing.id) return } toast.error("Couldn't add this artist. Try again in a moment.") @@ -257,7 +300,10 @@ export default function Watchlist() { imageUrl: entry.image_url ?? undefined, note: entry.note ?? undefined, }) - .then(refresh) + .then((restored) => { + markSticky(restored.id) + return refresh() + }) .catch(() => { toast.error( `Couldn't restore ${entry.name}. Try adding it again.` @@ -271,6 +317,21 @@ export default function Watchlist() { const visible = entries ? selectVisible(entries, { ...view, q: query }, stickyIds) : [] + countRef.current = { n: visible.length, m: entries?.length ?? 0 } + + function clearFilters() { + if (qTimer.current) clearTimeout(qTimer.current) + if (countTimer.current) clearTimeout(countTimer.current) + setQuery("") + lastWrittenQ.current = "" + write((p) => { + for (const key of CLEAR_FILTER_KEYS) p.delete(key) + }) + clearSticky() + filterInputRef.current?.focus() + const m = entries?.length ?? 0 + announce(`Filters cleared. Showing ${formatArtistCount(m, m)}.`) + } return ( @@ -348,21 +409,31 @@ export default function Watchlist() { query={query} onQueryChange={handleQueryChange} filterInputRef={filterInputRef} - showClear={false} - onClearFilters={() => {}} + showClear={ + isFiltered({ ...view, q: query }) && visible.length > 0 + } + onClearFilters={clearFilters} /> -
    - {visible.map((entry) => ( - - ))} -
+ {visible.length === 0 ? ( + Clear filters} + /> + ) : ( +
    + {visible.map((entry) => ( + + ))} +
+ )} )} From 48186998ff80f11ca6034379c6e7d05a36071b92 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:44:28 -0500 Subject: [PATCH 017/142] docs(25-04): add plan summary --- .../25-04-SUMMARY.md | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 .planning/phases/25-find-filter-watchlist-and-history/25-04-SUMMARY.md diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-04-SUMMARY.md b/.planning/phases/25-find-filter-watchlist-and-history/25-04-SUMMARY.md new file mode 100644 index 0000000..9198681 --- /dev/null +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-04-SUMMARY.md @@ -0,0 +1,145 @@ +--- +phase: 25-find-filter-watchlist-and-history +plan: 04 +subsystem: web +tags: [watchlist, name-filter, url-state, sticky-cards, react, vitest] +requires: + - phase: 25-03 + provides: "makeEntry fixture and the Watchlist card list this plan wraps in a section" +provides: + - "watchlistView.ts: foldName, matchesName, parseWatchlistParams, selectVisible, isFiltered, formatArtistCount, CLEAR_FILTER_KEYS" + - "useUrlParams: the single URL writer (ref-based, preventScrollReset always on)" + - "WatchlistToolbar with the Filter watchlist name filter and Clear filters" + - "renderRoute initialEntry parameter and LocationProbe test helper" + - "Your artists section with N of M count, filtered-to-zero state, count announcements, and the G4 sticky set" +affects: [25-05, 25-07, 25-08] +tech-stack: + added: [] + patterns: + - "Pure view-model module with no runtime import from ~/lib/api (route tests automock it)" + - "All URL writes through one ref-based helper so same-tick writes compose and scroll never resets" + - "Local input state filters instantly; debounced replace write to the URL; resync only when the URL differs from the last own write" +key-files: + created: + - web/app/lib/watchlistView.ts + - web/app/lib/watchlistView.test.ts + - web/app/lib/useUrlParams.ts + - web/app/lib/useUrlParams.test.tsx + - web/app/components/watchlist/WatchlistToolbar.tsx + - web/app/components/watchlist/WatchlistToolbar.test.tsx + modified: + - web/app/lib/test/routeStub.tsx + - web/app/routes/watchlist.tsx + - web/app/routes/watchlist.test.tsx +key-decisions: + - "q is filtered from local state on every keystroke and written to the URL 300ms trailing with replace; the input resyncs from the URL only when view.q differs from lastWrittenQ (Back/forward), never on an echo" + - "refresh() now resolves with the loaded rows (null on failure) so the 409 add path can mark the existing artist sticky; it never touches the sticky set" + - "Sticky set is route state keyed by id, cleared only by a search text change or Clear filters" +requirements-completed: [WLVW-01, WLVW-06] +status: complete +commits: 5 +plan_head_before: f444b94b66b0a27598e5b5647f743005fb997f6e +actuals: + tokens: 23000 + tasks: 3 + commits: 5 +duration: 18 min +completed: 2026-10-10 +coverage: + - deliverable: "Name filter narrows the list per keystroke over name only, with N of M count and no extra requests" + verification: + - kind: test + ref: "web/app/routes/watchlist.test.tsx#Watchlist name filter (WLVW-01)" + status: pass + human_judgment: false + - deliverable: "Fold table (accents, A$AP, Mø, Ænima, Straße) and blank-query behavior" + verification: + - kind: test + ref: "web/app/lib/watchlistView.test.ts#matchesName" + status: pass + human_judgment: false + - deliverable: "URL writer composes same-tick writes, keeps external params, and always sets preventScrollReset" + verification: + - kind: test + ref: "web/app/lib/useUrlParams.test.tsx" + status: pass + human_judgment: false + - deliverable: "Name filter clear x and Esc keep focus; Enter does nothing" + verification: + - kind: test + ref: "web/app/components/watchlist/WatchlistToolbar.test.tsx" + status: pass + human_judgment: false + - deliverable: "Clear filters, filtered-to-zero state, announcements, and sticky adds/Undo (G4)" + verification: + - kind: test + ref: "web/app/routes/watchlist.test.tsx#Watchlist filtered state (WLVW-06)" + status: pass + human_judgment: false + - deliverable: "No request during search and no browser history throttling on key hold (real browser network panel)" + verification: [] + human_judgment: true + rationale: "Network-panel request counting and history throttling are only observable in a real browser; deferred to end-of-phase UAT per the plan's human-check" +--- + +# Phase 25 Plan 04: Watchlist name filter, count, URL state, and sticky cards Summary + +The Watchlist gains a `Your artists` section with a live `Filter watchlist` name filter (accent and `$ø æ ß` folding), an always-visible `N of M artists` count, debounced replace-mode URL state through a single scroll-safe writer, a Clear filters flow that preserves sort, and a sticky set so adds and Undo restores never succeed invisibly. + +## Accomplishments + +- `watchlistView.ts` is a pure view-model (`foldName`, `matchesName`, `selectVisible`, `isFiltered`, `formatArtistCount`, `CLEAR_FILTER_KEYS`) with no runtime import from `~/lib/api`, so it survives route-test automocking. +- `useUrlParams` is the one URL writer: it builds on a ref so two same-tick writes compose, keeps params from external navigation, and always passes `preventScrollReset: true`. +- `WatchlistToolbar` renders the `Filter and sort artists` group with the name filter (label, `Filter by name…`, `ListFilter` icon), a `Clear name filter` x, Esc-to-clear, and the ghost `Clear filters` button. +- The route filters from local state on every keystroke with zero requests, writes `q` 300ms trailing with replace, and resyncs the input only on Back/forward or external URL changes. +- Filtered-to-zero shows `No artists match these filters` with a single default `Clear filters`; the toolbar button renders only when filtered and N > 0, so no duplicate buttons. +- Clear filters deletes `q`, `tags`, `muted`, `custom` in one push, keeps `sort`, focuses the input, and announces `Filters cleared. Showing {M} of {M} artists.`; search changes announce the count 500ms after the last keystroke through the existing status region. +- In-card edits (entry change, tag add/remove), add-from-search (including the 409 path), and Undo restores mark their card sticky; a search text change or Clear filters empties the set; `refresh()` never does. + +## Task Commits + +1. Task 1 (tracer): `115e13f` feat(25-04): filter the Watchlist by name with live count and URL state +2. Task 2 RED: `3062fd9` test(25-04): pin fold table, URL writer, and name filter keys +3. Task 2 GREEN: `e11bb20` feat(25-04): add the name filter clear button and Esc handling +4. Task 3 RED: `e431c13` test(25-04): pin clear filters, announcements, and sticky adds +5. Task 3 GREEN: `882a047` feat(25-04): add Clear filters, filtered-to-zero state, announcements, sticky cards + +## Deviations from Plan + +### Notes + +**1. [TDD ordering] Fold-table and URL-writer tests were green on first run** +- **Issue:** `foldName`, `matchesName`, `useUrlParams`, and `formatArtistCount` were already built in the Task 1 tracer, so those Task 2 tests pinned existing behavior and could not be observed RED. The toolbar clear x and Esc cases were genuinely RED (2 failing on the planned assertions) and drove the Task 2 feature commit. + +**2. [Test harness] LocationProbe's `` has an implicit status role** +- **Found during:** Task 3 GREEN +- **Issue:** with the probe rendered beside the route, `getByRole("status")` matched two elements. +- **Fix:** the test helper selects the route status region by `aria-atomic="true"`. Production markup is unchanged; the "exactly one status region" test renders without the probe and still passes. +- **Commit:** `882a047` + +**3. [Scope] No test for "in-card edit keeps a card that stopped matching"** +- **Issue:** with only the name axis, an in-card edit cannot change whether a card matches, so the behavior is unobservable until tag and preference filters land (plans 25-07/08). `markSticky` is wired into `handleEntryChange`, `addTag`, `removeTag` (grep shows 6 call sites plus the definition) and should be covered in those plans. + +**Total deviations:** 0 auto-fixed, 3 notes. **Impact:** none on behavior or scope. + +## Verification + +- `corepack pnpm test` (full web suite with coverage): 28 files, 432 tests passed +- `corepack pnpm run typecheck`: exit 0 +- `prettier --check "**/*.{ts,tsx}"`: clean +- Acceptance greps: `preventScrollReset: true` x1, `useUrlParams()` x1, `watchlist-artists-heading` x1, `LocationProbe` export x1, `Clear name filter` x1, `A$AP Rocky` in tests, `No artists match these filters` x2, `Filters cleared. Showing` x1, `markSticky(` x7 +- Pending human check: network panel on a 50+ artist dev DB (one `GET /watchlist`, no requests while typing, no history throttling warning). Deferred to end-of-phase UAT. + +## Known Stubs + +None. + +## Threat Flags + +None. T-25-11 (`q` rendered only as controlled input value and JSX text), T-25-12 (300ms debounced replace writes), T-25-13 (route test asserts one `listWatchlist` call and no `listTags`/`listEvents`), and T-25-14 (sticky set keyed by POST response id, add and Undo tests) are mitigated as planned. + +## Self-Check: PASSED + +- FOUND: web/app/lib/watchlistView.ts, web/app/lib/useUrlParams.ts, web/app/components/watchlist/WatchlistToolbar.tsx, and the three new test files +- FOUND commits 115e13f, 3062fd9, e11bb20, e431c13, 882a047 +- All task acceptance criteria re-run and passing From c6c7e46e46a8d1a1ccf6887d24861adfb257ae73 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:44:42 -0500 Subject: [PATCH 018/142] docs(25-04): update state and roadmap for completed plan --- .planning/ROADMAP.md | 6 +++--- .planning/STATE.md | 17 ++++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index cb561aa..0e4c516 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -193,7 +193,7 @@ Plans: 4. The user can filter History by a tag alongside the existing filters. It lists events for artists carrying that tag, including artists since removed from the watchlist, and it never shows an event older than the retention window, either on the first page or via "load older". 5. Opening the Watchlist makes one request regardless of watchlist size. Searching, sorting, and filtering then happen instantly with no further requests. -**Plans:** 3/10 plans executed +**Plans:** 4/10 plans executed Plans: **Wave 1** @@ -204,7 +204,7 @@ Plans: **Wave 2** *(blocked on Wave 1 completion)* - [x] 25-02-PLAN.md — GET /events `tag_id` filter inside the retention-aware ListEvents and HasOlderEvents, with retention, paging, and validation tests -- [ ] 25-04-PLAN.md — Watchlist name filter (accent and `$ø æ ß` folding), "N of M artists", URL state helper, Clear filters and filtered-to-zero state, live announcements, and sticky cards (G4) +- [x] 25-04-PLAN.md — Watchlist name filter (accent and `$ø æ ß` folding), "N of M artists", URL state helper, Clear filters and filtered-to-zero state, live announcements, and sticky cards (G4) - [ ] 25-06-PLAN.md — History Tag control (D-11), `Combobox` hardening, loading/unavailable labels, and the tag empty state with `Show all tags` **Wave 3** *(blocked on Wave 2 completion)* @@ -327,7 +327,7 @@ Phases execute in numeric order: 24 → 25 → 26 → 27 → 28 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 24. Artist Tags & Notes | 14/14 | Complete | 2026-10-06 | -| 25. Find & Filter — Watchlist and History | 3/10 | In Progress| | +| 25. Find & Filter — Watchlist and History | 4/10 | In Progress| | | 26. Bulk Edit & Remove | 0/? | Not started | - | | 27. Paste-a-List Bulk Add | 0/? | Not started | - | | 28. Tags on Discord Notifications | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index c052b01..da69205 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,16 +5,16 @@ milestone_name: Watchlist Organization current_phase: 25 current_phase_name: Find & Filter — Watchlist and History status: executing -stopped_at: Completed 25-02-PLAN.md -last_updated: "2026-10-10T16:37:29.600Z" +stopped_at: Completed 25-04-PLAN.md +last_updated: "2026-10-10T16:44:37.902Z" last_activity: 2026-10-10 last_activity_desc: Phase 25 execution started -state_head: 00e0ada558885d2ff83a7c713a5cafdbde100b56 +state_head: 48186998ff80f11ca6034379c6e7d05a36071b92 progress: total_phases: 5 completed_phases: 1 total_plans: 24 - completed_plans: 17 + completed_plans: 18 percent: 20 --- @@ -30,7 +30,7 @@ See: .planning/PROJECT.md (updated 2026-10-06) ## Current Position Phase: 25 (Find & Filter — Watchlist and History) — EXECUTING -Plan: 4 of 10 +Plan: 5 of 10 Status: Ready to execute Last activity: 2026-10-10 — Phase 25 execution started @@ -165,6 +165,7 @@ Last activity: 2026-10-10 — Phase 25 execution started | Phase 25 P01 | 25 min | 2 tasks | 6 files | | Phase 25 P03 | 8 min | 2 tasks | 14 files | | Phase 25 P02 | 25min | 2 tasks | 5 files | +| Phase 25 P04 | 18 min | 3 tasks | 9 files | ## Accumulated Context @@ -394,6 +395,8 @@ Recent decisions affecting current work: - [Phase 24]: 24-14: overflow latch via dependency-plus-early-return in ArtistNote; Manage tags hand-off consumed only by a successful load (G-24-9..11) - [Phase 25]: 25-01: ListWatchlist keeps zero-arg signature; latest/next release dates need no retention setting (G1) - [Phase 25]: Release dates render via string-only formatReleaseDate; malformed values pass through as plain text — No Date construction avoids timezone shifts; server decides upcoming (G2/G3) +- [Phase 25]: 25-04: Watchlist q filters from local state per keystroke, URL write debounced 300ms with replace; input resyncs only when URL differs from last own write — Avoids history flooding and echo loops (Pitfall 6) +- [Phase 25]: 25-04: refresh() resolves with loaded rows so the 409 add path can mark the existing artist sticky; sticky set cleared only by search change or Clear filters — G4: add/Undo never succeed invisibly under a filter ### Pending Todos @@ -487,8 +490,8 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-10T16:37:29.528Z -Stopped at: Completed 25-02-PLAN.md +Last session: 2026-10-10T16:44:37.827Z +Stopped at: Completed 25-04-PLAN.md Resume file: None ## Operator Next Steps From cac69cd8884e68053f433bc3473934f437b79a45 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 11:46:37 -0500 Subject: [PATCH 019/142] feat(25-06): add Tag filter to History feed - listEvents sends tag_id; sortTagsByName helper - Tag combobox after Event type, one GET /tags per mount - Load more carries the selected tag --- .../history/HistoryFilters.test.tsx | 59 +++++++++++++++---- web/app/components/history/HistoryFilters.tsx | 44 ++++++++++++-- web/app/lib/api.test.ts | 6 ++ web/app/lib/api.ts | 2 + web/app/lib/tags.ts | 12 ++++ web/app/routes/history.test.tsx | 58 +++++++++++++++++- web/app/routes/history.tsx | 37 +++++++++++- 7 files changed, 198 insertions(+), 20 deletions(-) diff --git a/web/app/components/history/HistoryFilters.test.tsx b/web/app/components/history/HistoryFilters.test.tsx index edd3169..828b704 100644 --- a/web/app/components/history/HistoryFilters.test.tsx +++ b/web/app/components/history/HistoryFilters.test.tsx @@ -48,7 +48,13 @@ const artists: WatchlistEntry[] = [ }, ] -const emptyValue: HistoryFiltersValue = { artistId: null, eventType: null } +const emptyValue: HistoryFiltersValue = { + artistId: null, + eventType: null, + tagId: null, +} + +const baseProps = { tags: [], tagsStatus: "loaded" as const } // getArtistSelect / getEventTypeSelect return the trigger button (D-13: a // hand-rolled combobox, not a native onSortChange(v as SortKey)}> + + + {(value: SortKey) => + SORT_OPTIONS.find((o) => o.value === value)?.label + } + + + + {SORT_OPTIONS.map((o) => ( + + {o.label} + + ))} + + + {showClear && ( + } + + function WatchlistWithProbe() { + return ( + <> + + + + + ) + } + + const search = () => screen.getByTestId("location-search").textContent + const shown = () => + screen + .queryAllByRole("button", { + name: /^Remove .* from watchlist$/, + hidden: true, + }) + .map((b) => b.getAttribute("aria-label")?.slice(7, -15)) + const filterChip = (name: string) => + screen.queryByRole("button", { + name: `Remove ${name} from tag filter`, + hidden: true, + }) + + async function openManageTags() { + await userEvent.click(screen.getByRole("button", { name: "Manage tags" })) + await screen.findByRole("heading", { name: "Manage tags" }) + } + + async function deleteTagInDialog(name: string) { + await userEvent.click( + screen.getByRole("button", { name: `Delete tag ${name}` }) + ) + await userEvent.click( + await screen.findByRole("button", { name: "Delete tag" }) + ) + } + + async function mergeRapIntoTrap() { + await userEvent.click( + screen.getByRole("button", { name: "Rename tag rap" }) + ) + const input = screen.getByRole("textbox", { name: "New name for rap" }) + await userEvent.clear(input) + await userEvent.type(input, "trap{Enter}") + await screen.findByText("Merge “rap” into “trap”?") + await userEvent.click(screen.getByRole("button", { name: "Merge tags" })) + } + + function mockVocabulary() { + mockListTags.mockResolvedValue([ + { id: 5, name: "rap", carrier_count: 1 }, + { id: 9, name: "trap", carrier_count: 2 }, + ]) + } + + it("drops URL tag ids that no loaded artist carries, with replace", async () => { + mockListWatchlist.mockResolvedValue(rows) + + renderRoute(WatchlistWithProbe, "/", "/?tags=5,99") + await screen.findByText("Bad Bunny") + + await waitFor(() => expect(search()).toBe("?tags=5")) + expect(shown()).toEqual(["Bad Bunny"]) + }) + + it("clears a tags param that matches nothing and shows the full list", async () => { + mockListWatchlist.mockResolvedValue(rows) + + renderRoute(WatchlistWithProbe, "/", "/?tags=99") + + await waitFor(() => expect(search()).toBe("")) + await screen.findByText("Bad Bunny") + expect(shown()).toEqual(["Bad Bunny", "Daddy Yankee", "Travis Scott"]) + }) + + it("drops a tag from the filter when Manage tags deletes it", async () => { + mockListWatchlist.mockResolvedValue(rows) + mockVocabulary() + mockDeleteTag.mockResolvedValueOnce({ carrier_count: 1 }) + + renderRoute(WatchlistWithProbe, "/", "/?tags=5") + await screen.findByText("Bad Bunny") + expect(filterChip("rap")).toBeInTheDocument() + + await openManageTags() + await deleteTagInDialog("rap") + + await waitFor(() => expect(search()).toBe("")) + expect(filterChip("rap")).not.toBeInTheDocument() + }) + + it("swaps a merged-away tag for its target in the filter", async () => { + mockListWatchlist.mockResolvedValue(rows) + mockVocabulary() + mockRenameTag.mockResolvedValueOnce({ + kind: "collision", + target: trap, + carrierCountAfterMerge: 3, + }) + mockMergeTag.mockResolvedValueOnce({ ...trap, carrier_count: 3 }) + + renderRoute(WatchlistWithProbe, "/", "/?tags=5") + await screen.findByText("Bad Bunny") + + await openManageTags() + await mergeRapIntoTrap() + + await waitFor(() => expect(search()).toBe("?tags=9")) + expect(filterChip("trap")).toBeInTheDocument() + expect(filterChip("rap")).not.toBeInTheDocument() + }) + + it("de-duplicates when both sides of a merge were selected", async () => { + mockListWatchlist.mockResolvedValue(rows) + mockVocabulary() + mockRenameTag.mockResolvedValueOnce({ + kind: "collision", + target: trap, + carrierCountAfterMerge: 3, + }) + mockMergeTag.mockResolvedValueOnce({ ...trap, carrier_count: 3 }) + + renderRoute(WatchlistWithProbe, "/", "/?tags=5,9") + await screen.findByText("Bad Bunny") + + await openManageTags() + await mergeRapIntoTrap() + + await waitFor(() => expect(search()).toBe("?tags=9")) + }) + + it("drops a deleted tag again when Back brings it into the URL", async () => { + mockListWatchlist.mockResolvedValue(rows) + mockVocabulary() + mockDeleteTag.mockResolvedValueOnce({ carrier_count: 1 }) + + renderRoute(WatchlistWithProbe, "/") + await screen.findByText("Bad Bunny") + + await userEvent.click(screen.getByLabelText("Filter by tags, any of")) + await userEvent.click(await screen.findByRole("option", { name: /^rap/ })) + await userEvent.click(await screen.findByRole("option", { name: /^trap/ })) + await waitFor(() => expect(search()).toBe("?tags=5%2C9")) + await userEvent.keyboard("{Escape}") + + await openManageTags() + await deleteTagInDialog("rap") + await waitFor(() => expect(search()).toBe("?tags=9")) + await userEvent.keyboard("{Escape}") + + await userEvent.click( + screen.getByRole("button", { name: "history back", hidden: true }) + ) + + await waitFor(() => expect(search()).toBe("?tags=9")) + expect(filterChip("rap")).not.toBeInTheDocument() + }) + + it("keeps the filter, its chip, and the card when the last carrier is detached in-session", async () => { + mockListWatchlist.mockResolvedValue(rows) + mockDetachTag.mockResolvedValue(undefined) + + renderRoute(WatchlistWithProbe, "/", "/?tags=5") + await screen.findByText("Bad Bunny") + + await userEvent.click( + screen.getByRole("button", { name: "Remove tag rap from Bad Bunny" }) + ) + + expect(search()).toBe("?tags=5") + expect(filterChip("rap")).toBeInTheDocument() + expect(shown()).toEqual(["Bad Bunny"]) + expect(mockDetachTag).toHaveBeenCalledWith(1, 5) + }) + + it("keeps the tag filter and the sticky added card across the refresh after an add", async () => { + const drizzy = makeEntry({ id: 99, name: "Drizzy", mbid: "mbid-99" }) + mockListWatchlist + .mockResolvedValueOnce(rows) + .mockResolvedValueOnce([...rows, drizzy]) + vi.mocked(searchArtists).mockResolvedValue({ + query: "Drizzy", + sources: { + musicbrainz: { + status: "ok", + artists: [ + { + source: "musicbrainz", + id: "mbid-99", + name: "Drizzy", + disambiguation: null, + country: null, + type: "Person", + image_url: null, + }, + ], + }, + }, + }) + mockAddWatchlist.mockResolvedValue(drizzy) + + renderRoute(WatchlistWithProbe, "/", "/?tags=5") + await screen.findByText("Bad Bunny") + + await userEvent.type( + screen.getByRole("textbox", { name: "Search artists" }), + "Drizzy" + ) + await userEvent.click( + await screen.findByRole("button", { name: "Add to Watchlist" }) + ) + + await screen.findByRole("button", { name: "Remove Drizzy from watchlist" }) + expect(search()).toBe("?tags=5") + expect(filterChip("rap")).toBeInTheDocument() + expect(shown()).toEqual(["Bad Bunny", "Drizzy"]) + expect(mockListWatchlist).toHaveBeenCalledTimes(2) + }) +}) From beef58684f24f0f2e9272def682c8db8d63ec61a Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 12:10:11 -0500 Subject: [PATCH 039/142] feat(25-07): keep the tag filter consistent with Manage tags, Back, and first load - validate URL tag ids once on the first successful load - Manage tags delete and merge rewrite the filter with replace - retired ids resurrected by Back are dropped or swapped --- web/app/routes/watchlist.test.tsx | 35 +++++++++++++++- web/app/routes/watchlist.tsx | 68 ++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/web/app/routes/watchlist.test.tsx b/web/app/routes/watchlist.test.tsx index 99723fc..7b5664b 100644 --- a/web/app/routes/watchlist.test.tsx +++ b/web/app/routes/watchlist.test.tsx @@ -1731,10 +1731,43 @@ describe("Watchlist tag filter consistency (D-13)", () => { screen.getByRole("button", { name: "history back", hidden: true }) ) - await waitFor(() => expect(search()).toBe("?tags=9")) + // Back lands on ?tags=5, whose only id is retired, so it is dropped. + await waitFor(() => expect(search()).toBe("")) expect(filterChip("rap")).not.toBeInTheDocument() }) + it("swaps a merged-away tag again when Back brings it into the URL", async () => { + mockListWatchlist.mockResolvedValue(rows) + mockVocabulary() + mockRenameTag.mockResolvedValueOnce({ + kind: "collision", + target: trap, + carrierCountAfterMerge: 3, + }) + mockMergeTag.mockResolvedValueOnce({ ...trap, carrier_count: 3 }) + + renderRoute(WatchlistWithProbe, "/") + await screen.findByText("Bad Bunny") + + await userEvent.click(screen.getByLabelText("Filter by tags, any of")) + await userEvent.click(await screen.findByRole("option", { name: /^rap/ })) + await userEvent.click(await screen.findByRole("option", { name: /^trap/ })) + await userEvent.keyboard("{Escape}") + + await openManageTags() + await mergeRapIntoTrap() + await waitFor(() => expect(search()).toBe("?tags=9")) + await userEvent.keyboard("{Escape}") + + await userEvent.click( + screen.getByRole("button", { name: "history back", hidden: true }) + ) + + // Back lands on ?tags=5, which swaps to the merge target. + await waitFor(() => expect(search()).toBe("?tags=9")) + expect(filterChip("trap")).toBeInTheDocument() + }) + it("keeps the filter, its chip, and the card when the last carrier is detached in-session", async () => { mockListWatchlist.mockResolvedValue(rows) mockDetachTag.mockResolvedValue(undefined) diff --git a/web/app/routes/watchlist.tsx b/web/app/routes/watchlist.tsx index b7f820d..6ae050c 100644 --- a/web/app/routes/watchlist.tsx +++ b/web/app/routes/watchlist.tsx @@ -76,6 +76,9 @@ export default function Watchlist() { // Last known name per tag id, so a chip survives its last carrier leaving. const tagNamesRef = useRef(new Map()) + // Deleted -> null, merged -> target id; maps a Back-resurrected id (D-13). + const retiredTagIdsRef = useRef(new Map()) + const tagIdsValidatedRef = useRef(false) function markSticky(id: number) { setStickyIds((prev) => (prev.has(id) ? prev : new Set(prev).add(id))) @@ -85,6 +88,26 @@ export default function Watchlist() { setStickyIds((prev) => (prev.size === 0 ? prev : new Set())) } + function settleTagIds(ids: number[]): number[] { + const out = new Set() + for (const id of ids) { + const retired = retiredTagIdsRef.current + const next = retired.has(id) ? retired.get(id) : id + if (next != null && tagNamesRef.current.has(next)) out.add(next) + } + return [...out].sort((a, b) => a - b) + } + + function writeTagIds(ids: number[]) { + write( + (p) => { + if (ids.length === 0) p.delete("tags") + else p.set("tags", ids.join(",")) + }, + { replace: true } + ) + } + function scheduleCountAnnouncement() { if (countTimer.current) clearTimeout(countTimer.current) countTimer.current = setTimeout(() => { @@ -125,10 +148,15 @@ export default function Watchlist() { } }, [view.q]) - // Drop an unknown or default sort and malformed tag ids from the URL - // without a history entry. + // Drop an unknown or default sort and bad tag ids from the URL without a + // history entry. Once validated, retired and unknown tag ids go too. useEffect(() => { const canonical = canonicalWatchlistParams(params) + if (tagIdsValidatedRef.current) { + const settled = settleTagIds(parseWatchlistParams(canonical).tagIds) + if (settled.length === 0) canonical.delete("tags") + else canonical.set("tags", settled.join(",")) + } if (canonical.toString() === params.toString()) return write( (p) => { @@ -140,6 +168,7 @@ export default function Watchlist() { }, { replace: true } ) + // eslint-disable-next-line react-hooks/exhaustive-deps -- settleTagIds only reads refs }, [params, write]) useEffect(() => { @@ -167,6 +196,16 @@ export default function Watchlist() { refresh() }, [refresh]) + // Runs once on the first successful load; later refreshes never prune (D-13). + useEffect(() => { + if (!entries || tagIdsValidatedRef.current) return + tagIdsValidatedRef.current = true + const carried = new Set(entries.flatMap((r) => r.tags.map((t) => t.id))) + const kept = view.tagIds.filter((id) => carried.has(id)) + if (kept.length !== view.tagIds.length) writeTagIds(kept) + // eslint-disable-next-line react-hooks/exhaustive-deps -- first load only + }, [entries]) + // handleEntryChange is passed down to PreferenceToggles (via // WatchlistRow) so its optimistic-update-then-rollback logic writes // straight into this route's own entries state -- the single source of @@ -219,8 +258,33 @@ export default function Watchlist() { ) } + // Swap or drop one selected tag id, de-duplicated and ascending. + function replaceSelectedTag(from: number, to: number | null) { + write( + (p) => { + const ids = parseWatchlistParams(p).tagIds + if (!ids.includes(from)) return + const next = new Set(ids.filter((id) => id !== from)) + if (to !== null) next.add(to) + if (next.size === 0) p.delete("tags") + else p.set("tags", [...next].sort((a, b) => a - b).join(",")) + }, + { replace: true } + ) + } + // The only place rows' tags change for a Manage tags rename/merge/delete. function applyTagChange(change: TagChange) { + if (change.kind === "renamed") { + tagNamesRef.current.set(change.tag.id, change.tag.name) + } else if (change.kind === "deleted") { + retiredTagIdsRef.current.set(change.tagId, null) + replaceSelectedTag(change.tagId, null) + } else { + retiredTagIdsRef.current.set(change.sourceId, change.target.id) + tagNamesRef.current.set(change.target.id, change.target.name) + replaceSelectedTag(change.sourceId, change.target.id) + } setEntries((rows) => rows ? rows.map((r) => { From 139429c8c6f1298280f7e9fa31930cbe1069beb1 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 12:10:44 -0500 Subject: [PATCH 040/142] docs(25-07): add Watchlist tag filter plan summary --- .../25-07-SUMMARY.md | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 .planning/phases/25-find-filter-watchlist-and-history/25-07-SUMMARY.md diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-07-SUMMARY.md b/.planning/phases/25-find-filter-watchlist-and-history/25-07-SUMMARY.md new file mode 100644 index 0000000..3ee2ba6 --- /dev/null +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-07-SUMMARY.md @@ -0,0 +1,153 @@ +--- +phase: 25-find-filter-watchlist-and-history +plan: 07 +subsystem: web +tags: [watchlist, tag-filter, combobox, url-state, react, vitest] +requires: + - phase: 25-04 + provides: "useUrlParams writer, sticky set, Clear filters that already deletes tags" + - phase: 25-05 + provides: "param canonicalization effect and sort" + - phase: 25-06 + provides: "sortTagsByName and the hardened combobox vendored parts" +provides: + - "watchlistView.ts: tagIds axis, matchesAnyTag, tagFilterOptions, TagFilterOption, tags parse and canonicalization" + - "WatchlistTagFilter: any-of multi-select on the vendored base-ui Combobox with chips" + - "ComboboxChip removeLabel prop and contrast-safe remove button" + - "Route tag-id lifecycle: first-load validation, Manage tags delete/merge sync, Back resurrection handling" +affects: [25-08] +tech-stack: + added: [] + patterns: + - "Tag names cached by id in a ref so a chip outlives its last carrier" + - "Retired-id map (deleted -> null, merged -> target) consulted by the canonicalization effect so Back cannot resurrect a dead tag" + - "Filter options derived from the loaded payload; GET /tags is never called" +key-files: + created: + - web/app/components/watchlist/WatchlistTagFilter.tsx + - web/app/components/watchlist/WatchlistTagFilter.test.tsx + modified: + - web/app/components/ui/combobox.tsx + - web/app/components/watchlist/WatchlistToolbar.tsx + - web/app/components/watchlist/WatchlistToolbar.test.tsx + - web/app/lib/watchlistView.ts + - web/app/lib/watchlistView.test.ts + - web/app/routes/watchlist.tsx + - web/app/routes/watchlist.test.tsx +key-decisions: + - "Esc on a closed popup makes base-ui clear every chip through onValueChange (reason escape-key); the filter cancels that reason so a second Esc does nothing" + - "Multiple mode already keeps the popup open on pick in jsdom (A2 held), so the item-press onOpenChange cancel is a defensive pin rather than a fix" + - "A1 held: base-ui's default filter is a case-insensitive contains, so no explicit filter prop was added" + - "tags in the URL is written through URLSearchParams, so commas serialize as %2C (?tags=5%2C9); the decoded value is the ascending comma-separated id list" +requirements-completed: [WLVW-04, WLVW-06] +status: complete +commits: 5 +plan_head_before: 27d472fe2dd9026e0d194a355762bf969cdb172e +actuals: + tokens: 30000 + tasks: 3 + commits: 5 +duration: 40 min +completed: 2026-10-10 +coverage: + - deliverable: "Tags: any of narrows the list to carriers (OR), empty selection is no constraint, options come from the payload with one request" + verification: + - kind: test + ref: "web/app/routes/watchlist.test.tsx#Watchlist tag filter (WLVW-04)" + status: pass + human_judgment: false + - deliverable: "Multi-select contract: stays open and focused after a pick, deselect, Backspace and chip x removal, Esc, placeholder, sorted chips, counts, empty copy" + verification: + - kind: test + ref: "web/app/components/watchlist/WatchlistTagFilter.test.tsx" + status: pass + human_judgment: false + - deliverable: "Tag axis in the view model (parse cap and shape, canonicalization, matchesAnyTag, tagFilterOptions ordering, selectVisible AND)" + verification: + - kind: test + ref: "web/app/lib/watchlistView.test.ts#tag axis" + status: pass + human_judgment: false + - deliverable: "First-load validation, Manage tags delete/merge sync, Back resurrection, detach and refresh leave the filter alone" + verification: + - kind: test + ref: "web/app/routes/watchlist.test.tsx#Watchlist tag filter consistency (D-13)" + status: pass + human_judgment: false + - deliverable: "Chip x meets 3:1 contrast against the chip fill, and a 32-character tag truncates at 375px" + verification: [] + human_judgment: true + rationale: "Contrast and truncation at width are only measurable in the rendered app; deferred to end-of-phase UAT per the plan's human-check" +--- + +# Phase 25 Plan 07: Watchlist tag filter Summary + +The Watchlist gains a `Tags: any of` multi-select (vendored base-ui Combobox with chips) fed by tags the loaded artists carry, narrowing the list with OR semantics and a `tags=` URL param that stays truthful across first load, Manage tags delete and merge, Back, detaches, and refreshes. + +## Accomplishments + +- `watchlistView.ts` owns the tag axis: `parseWatchlistParams` reads at most 200 `tags` tokens matching `^[1-9]\d*$` (ascending, de-duplicated), `canonicalWatchlistParams` rewrites or deletes the param, `matchesAnyTag` and `tagFilterOptions` (collator then id order) are pure, `selectVisible` ANDs the tag axis with name, and `isFiltered` counts it. +- `WatchlistTagFilter` is built only from vendored parts in `multiple` mode (no TagCombobox, no create option). Chips are `h-6` neutral and truncate with `title`; options show a `tabular-nums` carrier count; placeholder `All tags` only while empty; `No tags yet` and `No tags match` copy through `ComboboxEmpty`. +- `ComboboxChip` takes `removeLabel` (the remove button's aria-label) and uses `text-muted-foreground hover:text-foreground` instead of 50% opacity. +- The route caches tag names by id, so a filter chip survives its last carrier leaving. Picking pushes `tags`, clears sticky, and schedules the count announcement. +- First successful load drops URL tag ids no artist carries (replace) exactly once. Manage tags delete and merge rewrite `tags` with replace (merge swaps source for target, de-duplicated). A retired-id map lets the canonicalization effect drop or swap a deleted or merged id that Back brings into the URL. `refresh()` and chip detach never touch `tags`. + +## Task Commits + +1. Task 1 (tracer): `438144f` feat(25-07): filter the Watchlist by any of several tags +2. Task 2 RED: `a10a412` test(25-07): pin the tag filter's keep-open, keys, and empty copy +3. Task 2 GREEN: `5267ea8` feat(25-07): keep the tag filter open on pick and explain empty results +4. Task 3 RED: `4fb8ef1` test(25-07): pin tag filter validation, Manage tags sync, and Back +5. Task 3 GREEN: `beef586` feat(25-07): keep the tag filter consistent with Manage tags, Back, and first load + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Esc on a closed popup cleared every selected tag** +- **Found during:** Task 2 (RED test "keeps the selection when Esc is pressed with the popup closed") +- **Issue:** base-ui fires `onValueChange` with reason `escape-key` and an empty list when Escape is pressed in the input with the popup closed, so the second Esc wiped the filter. The plan's "a second Esc does nothing" requires otherwise. +- **Fix:** `onValueChange` cancels the `escape-key` reason before calling `onChange`. +- **Files modified:** `web/app/components/watchlist/WatchlistTagFilter.tsx` +- **Commit:** `5267ea8` + +### Notes + +**2. [Assumption result] A1 and A2 both held** +- The default filter is a case-insensitive contains, and multiple mode keeps the popup open on pick without help. The `item-press` close cancel (required by the plan's acceptance grep) is kept as a defensive pin; no controlled `open` state or explicit `filter` was needed. + +**3. [Test harness] Accessible name of the chips input** +- The input carries both `aria-labelledby="watchlist-tags-label"` and `aria-label="Filter by tags, any of"` as the UI-SPEC specifies, so its computed role name is `Tags: any of`. Tests locate it with `getByLabelText("Filter by tags, any of")`. + +**4. [Test harness] Open popup hides the page from role queries** +- While the combobox or Manage tags dialog is open, the rest of the page is aria-hidden, so route tests that read cards or filter chips behind them query with `hidden: true`. + +**5. [Scope] `%2C` in the URL** +- `URLSearchParams` serializes the comma, so multi-tag URLs read `?tags=5%2C9`. A hand-typed `?tags=5,9` is normalized to that form with a single replace on load. Single-tag URLs (`?tags=3`) are unaffected. + +**6. [TDD ordering] View-model cases and the detach/refresh route cases were green on first run** +- The Task 2 view-model tests and Task 3's detach-keeps-filter and add-refresh-keeps-filter tests pin behavior already delivered by Task 1's tracer and the 25-04 sticky design. The genuinely RED tests (empty copy, Esc clears chips, first-load validation, delete/merge sync, Back) drove the GREEN commits. + +**Total deviations:** 1 auto-fixed, 5 notes. **Impact:** none on scope. + +## Verification + +- `corepack pnpm test` (full web suite with coverage): 30 files, 539 tests passed +- `corepack pnpm run typecheck`: exit 0 +- `prettier --check "**/*.{ts,tsx}"`: clean +- Acceptance greps: `opacity-50 hover:opacity-100` x0, `removeLabel` x3 in `combobox.tsx`, `multiple` present and `TagCombobox` x0 in `WatchlistTagFilter.tsx`, `id="watchlist-tags-label"` x1, `item-press` present, `No tags yet — add one with` x1, `No tags match` x1, `tagIdsValidatedRef` x4, `retiredTagIdsRef` x4 +- Pending human check: measure the toolbar chip x against the `#27272a` chip fill (expected about 5.8:1), confirm a selected tag survives an add-and-refresh in the running app, and eyeball a 32-character tag at 375px. Deferred to end-of-phase UAT. + +## Known Stubs + +None. + +## Threat Flags + +None. T-25-20 (200-token cap, shape filter, first-load intersection), T-25-21 (tag names only as JSX text and escaped attributes) and T-25-22 (filter never calls attach/detach/delete; Manage tags only reports) are mitigated as planned. + +## Self-Check: PASSED + +- FOUND: web/app/components/watchlist/WatchlistTagFilter.tsx and WatchlistTagFilter.test.tsx +- FOUND commits 438144f, a10a412, 5267ea8, 4fb8ef1, beef586 +- All task acceptance criteria re-run and passing From 3aea8c3ab3caa79d0d0b22f897661f833f681b24 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 12:10:53 -0500 Subject: [PATCH 041/142] docs(25-07): update state and roadmap for completed plan --- .planning/REQUIREMENTS.md | 8 ++++---- .planning/ROADMAP.md | 6 +++--- .planning/STATE.md | 16 +++++++++------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 18d3114..8061b3b 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -25,9 +25,9 @@ - [ ] **WLVW-01**: User can search the watchlist by artist name - [x] **WLVW-02**: User can sort by name (A–Z/Z–A) or date added (newest/oldest), with a stable tie-break - [x] **WLVW-03**: User can sort by latest release (the artist's newest own `new_release` event date that is not upcoming); artists with none sort last in both directions -- [ ] **WLVW-04**: User can filter the watchlist by one or more tags +- [x] **WLVW-04**: User can filter the watchlist by one or more tags - [ ] **WLVW-05**: User can filter to artists with muted event types or non-default release-type filters -- [ ] **WLVW-06**: Search, sort, and filters combine; the view shows "N of M artists" and an empty state with a clear-filters action +- [x] **WLVW-06**: Search, sort, and filters combine; the view shows "N of M artists" and an empty state with a clear-filters action ### History @@ -92,9 +92,9 @@ Which phases cover which requirements. Updated during roadmap creation. | WLVW-01 | Phase 25 | Pending | | WLVW-02 | Phase 25 | Complete | | WLVW-03 | Phase 25 | Complete | -| WLVW-04 | Phase 25 | Pending | +| WLVW-04 | Phase 25 | Complete | | WLVW-05 | Phase 25 | Pending | -| WLVW-06 | Phase 25 | Pending | +| WLVW-06 | Phase 25 | Complete | | HIST-02 | Phase 25 | Complete | | BULK-01 | Phase 26 | Pending | | BULK-02 | Phase 26 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7ce778a..450d4b7 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -193,7 +193,7 @@ Plans: 4. The user can filter History by a tag alongside the existing filters. It lists events for artists carrying that tag, including artists since removed from the watchlist, and it never shows an event older than the retention window, either on the first page or via "load older". 5. Opening the Watchlist makes one request regardless of watchlist size. Searching, sorting, and filtering then happen instantly with no further requests. -**Plans:** 7/10 plans executed +**Plans:** 8/10 plans executed Plans: **Wave 1** @@ -214,7 +214,7 @@ Plans: **Wave 4** *(blocked on Wave 3 completion)* -- [ ] 25-07-PLAN.md — `Tags: any of` multi-select on the vendored base-ui Combobox, payload-derived options, and D-13 consistency (first-load validation, Manage tags delete/merge, Back) +- [x] 25-07-PLAN.md — `Tags: any of` multi-select on the vendored base-ui Combobox, payload-derived options, and D-13 consistency (first-load validation, Manage tags delete/merge, Back) **Wave 5** *(blocked on Wave 4 completion)* @@ -327,7 +327,7 @@ Phases execute in numeric order: 24 → 25 → 26 → 27 → 28 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 24. Artist Tags & Notes | 14/14 | Complete | 2026-10-06 | -| 25. Find & Filter — Watchlist and History | 7/10 | In Progress| | +| 25. Find & Filter — Watchlist and History | 8/10 | In Progress| | | 26. Bulk Edit & Remove | 0/? | Not started | - | | 27. Paste-a-List Bulk Add | 0/? | Not started | - | | 28. Tags on Discord Notifications | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 738a40b..a15f98b 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,16 +5,16 @@ milestone_name: Watchlist Organization current_phase: 25 current_phase_name: Find & Filter — Watchlist and History status: executing -stopped_at: Completed 25-09-PLAN.md -last_updated: "2026-10-10T17:01:19.940Z" +stopped_at: Completed 25-07-PLAN.md +last_updated: "2026-10-10T17:10:52.264Z" last_activity: 2026-10-10 last_activity_desc: Phase 25 execution started -state_head: 8de52e3c5100c7e13690e4bf4ae236942bf4f1a7 +state_head: 139429c8c6f1298280f7e9fa31930cbe1069beb1 progress: total_phases: 5 completed_phases: 1 total_plans: 24 - completed_plans: 21 + completed_plans: 22 percent: 20 --- @@ -30,7 +30,7 @@ See: .planning/PROJECT.md (updated 2026-10-06) ## Current Position Phase: 25 (Find & Filter — Watchlist and History) — EXECUTING -Plan: 8 of 10 +Plan: 9 of 10 Status: Ready to execute Last activity: 2026-10-10 — Phase 25 execution started @@ -169,6 +169,7 @@ Last activity: 2026-10-10 — Phase 25 execution started | Phase 25 P06 | 14 min | 3 tasks | 8 files | | Phase 25 P05 | 5 min | 2 tasks | 6 files | | Phase 25 P09 | 4 min | 2 tasks | 6 files | +| Phase 25 P07 | 40 min | 3 tasks | 9 files | ## Accumulated Context @@ -403,6 +404,7 @@ Recent decisions affecting current work: - [Phase 25]: 25-06: History Tag degraded trigger labels apply only when a tag is selected — With no selection a failed GET /tags still reads All tags; matches the plan behavior block - [Phase 25]: 25-05: Latest-release sort compares raw text (2024 before 2024-05), nulls last in both directions, artist_id ascending tie-break in every mode - [Phase 25]: 25-09: History stale artist/tag ids are checked once per source per mount, only after a successful load; a failed fetch keeps the filter and the trigger reads Artist unavailable / Tag unavailable — A failed fetch is not proof an id is stale (UI-SPEC History Tag filter) +- [Phase 25]: 25-07: Esc on a closed tag-filter popup is cancelled (escape-key reason) so it cannot clear every chip; tags URL param serializes commas as %2C ### Pending Todos @@ -496,8 +498,8 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-10T17:01:19.856Z -Stopped at: Completed 25-09-PLAN.md +Last session: 2026-10-10T17:10:52.181Z +Stopped at: Completed 25-07-PLAN.md Resume file: None ## Operator Next Steps From d88856812c0ea2765aaaa8418549973674393563 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 12:13:41 -0500 Subject: [PATCH 042/142] feat(25-08): filter the Watchlist to muted events and custom release types - Show only toggles for Has muted events and Custom release types - muted/custom URL params honor only 1; AND-compose with name and tags - sticky cards survive an in-card edit until the next filter change --- .../watchlist/WatchlistToolbar.test.tsx | 72 +++++++++ .../components/watchlist/WatchlistToolbar.tsx | 46 +++++- web/app/lib/watchlistView.test.ts | 137 ++++++++++++++++- web/app/lib/watchlistView.ts | 40 ++++- web/app/routes/watchlist.test.tsx | 144 ++++++++++++++++++ web/app/routes/watchlist.tsx | 19 ++- 6 files changed, 444 insertions(+), 14 deletions(-) diff --git a/web/app/components/watchlist/WatchlistToolbar.test.tsx b/web/app/components/watchlist/WatchlistToolbar.test.tsx index 65003b4..7ca9aa8 100644 --- a/web/app/components/watchlist/WatchlistToolbar.test.tsx +++ b/web/app/components/watchlist/WatchlistToolbar.test.tsx @@ -20,6 +20,10 @@ function setup(query: string) { tagOptions={[]} selectedTags={[]} onSelectedTagsChange={() => {}} + muted={false} + onMutedChange={() => {}} + custom={false} + onCustomChange={() => {}} /> ) return { onQueryChange, input: screen.getByRole("textbox") } @@ -93,6 +97,10 @@ describe("WatchlistToolbar sort", () => { tagOptions={[]} selectedTags={[]} onSelectedTagsChange={() => {}} + muted={false} + onMutedChange={() => {}} + custom={false} + onCustomChange={() => {}} /> ) @@ -115,3 +123,67 @@ describe("WatchlistToolbar sort", () => { expect(onSortChange).toHaveBeenCalledWith("added-asc") }) }) + +describe("WatchlistToolbar Show only", () => { + function renderToggles(muted: boolean, custom: boolean) { + const onMutedChange = vi.fn() + const onCustomChange = vi.fn() + render( + {}} + filterInputRef={createRef()} + showClear={false} + onClearFilters={() => {}} + sort="name-asc" + onSortChange={() => {}} + tagOptions={[]} + selectedTags={[]} + onSelectedTagsChange={() => {}} + muted={muted} + onMutedChange={onMutedChange} + custom={custom} + onCustomChange={onCustomChange} + /> + ) + return { onMutedChange, onCustomChange } + } + + it("renders the legend and two unpressed toggles with no Check", () => { + renderToggles(false, false) + + expect(screen.getByText("Show only")).toBeInTheDocument() + for (const name of ["Has muted events", "Custom release types"]) { + const button = screen.getByRole("button", { name }) + expect(button).toHaveAttribute("aria-pressed", "false") + expect(button.querySelector("svg")).toBeNull() + } + }) + + it("shows aria-pressed and a Check only on the active toggle", () => { + renderToggles(true, false) + + const on = screen.getByRole("button", { name: "Has muted events" }) + expect(on).toHaveAttribute("aria-pressed", "true") + expect(on.querySelector("svg")).toHaveAttribute("aria-hidden", "true") + expect( + screen + .getByRole("button", { name: "Custom release types" }) + .querySelector("svg") + ).toBeNull() + }) + + it("reports the flipped value and keeps focus on the clicked button", async () => { + const { onMutedChange, onCustomChange } = renderToggles(true, false) + const custom = screen.getByRole("button", { name: "Custom release types" }) + + await userEvent.click( + screen.getByRole("button", { name: "Has muted events" }) + ) + await userEvent.click(custom) + + expect(onMutedChange).toHaveBeenCalledWith(false) + expect(onCustomChange).toHaveBeenCalledWith(true) + expect(custom).toHaveFocus() + }) +}) diff --git a/web/app/components/watchlist/WatchlistToolbar.tsx b/web/app/components/watchlist/WatchlistToolbar.tsx index dc8bed0..d7ffcbd 100644 --- a/web/app/components/watchlist/WatchlistToolbar.tsx +++ b/web/app/components/watchlist/WatchlistToolbar.tsx @@ -1,4 +1,4 @@ -import { FilterX, ListFilter, X } from "lucide-react" +import { Check, FilterX, ListFilter, X } from "lucide-react" import { Button } from "~/components/ui/button" import { @@ -32,6 +32,31 @@ export interface WatchlistToolbarProps { tagOptions: TagFilterOption[] selectedTags: TagFilterOption[] onSelectedTagsChange(ids: number[]): void + muted: boolean + onMutedChange(next: boolean): void + custom: boolean + onCustomChange(next: boolean): void +} + +function ShowOnlyToggle({ + on, + label, + onToggle, +}: { + on: boolean + label: string + onToggle(next: boolean): void +}) { + return ( + + ) } export function WatchlistToolbar({ @@ -45,6 +70,10 @@ export function WatchlistToolbar({ tagOptions, selectedTags, onSelectedTagsChange, + muted, + onMutedChange, + custom, + onCustomChange, }: WatchlistToolbarProps) { return (
+
+ Show only +
+ + +
+
{showClear && ( - - ))} + {/* Filter toggle only; the x beside it is the sole detach (D-08). */} + + + + ) + })} {pending.map((item) => ( Date: Sat, 10 Oct 2026 12:19:29 -0500 Subject: [PATCH 046/142] docs(25-08): add preference toggles and chip click-to-filter plan summary --- .../25-08-SUMMARY.md | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 .planning/phases/25-find-filter-watchlist-and-history/25-08-SUMMARY.md diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-08-SUMMARY.md b/.planning/phases/25-find-filter-watchlist-and-history/25-08-SUMMARY.md new file mode 100644 index 0000000..699fdec --- /dev/null +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-08-SUMMARY.md @@ -0,0 +1,143 @@ +--- +phase: 25-find-filter-watchlist-and-history +plan: 08 +subsystem: web +tags: [watchlist, preference-filter, tag-chips, url-state, react, vitest] +requires: + - phase: 25-04 + provides: "useUrlParams writer, sticky set, Clear filters deleting muted/custom via CLEAR_FILTER_KEYS" + - phase: 25-07 + provides: "tags URL param, handleTagsChange, tagNamesRef, toolbar tag filter" +provides: + - "watchlistView.ts: DEFAULT_RELEASE_TYPES, muted/custom view axes, hasMutedEvents, hasCustomReleaseTypes, AND composition in selectVisible and isFiltered" + - "WatchlistToolbar: Show only fieldset with two aria-pressed toggles" + - "TagChips: chip label is a Filter by button; data-chip-remove focus targeting; TagActions.filterTagIds and toggleFilterTag" + - "Route: handleFlagChange for muted/custom, toggleFilterTag, filterTagIds set passed to each row" +affects: [] +tech-stack: + added: [] + patterns: + - "Chip focus logic selects [data-chip-remove] so extra buttons inside a chip cannot shift the index" + - "Boolean URL flags honor only the value 1; canonicalization effect rewrites anything else away with replace" +key-files: + created: [] + modified: + - web/app/lib/watchlistView.ts + - web/app/lib/watchlistView.test.ts + - web/app/components/watchlist/WatchlistToolbar.tsx + - web/app/components/watchlist/WatchlistToolbar.test.tsx + - web/app/components/watchlist/TagChips.tsx + - web/app/components/watchlist/TagChips.test.tsx + - web/app/routes/watchlist.tsx + - web/app/routes/watchlist.test.tsx +key-decisions: + - "Custom release types is a set comparison against the default four, so order and duplicates are ignored and an empty list counts as custom" + - "The existing canonicalization effect owns muted and custom too; no separate cleanup effect" + - "Chip label click reuses handleTagsChange (push, clear sticky, count announcement) and seeds tagNamesRef first so the toolbar chip has its name" +requirements-completed: [WLVW-05, WLVW-04, WLVW-06] +status: complete +commits: 4 +plan_head_before: 3aea8c3ab3caa79d0d0b22f897661f833f681b24 +actuals: + tokens: 28000 + tasks: 2 + commits: 4 +duration: 25 min +completed: 2026-10-10 +coverage: + - deliverable: "Has muted events and Custom release types toggles narrow the list, round-trip through the URL, keep focus, and AND-compose with name and tags" + verification: + - kind: test + ref: "web/app/routes/watchlist.test.tsx#Watchlist preference toggles (WLVW-05)" + status: pass + - kind: test + ref: "web/app/lib/watchlistView.test.ts#muted and custom axes" + status: pass + - kind: test + ref: "web/app/components/watchlist/WatchlistToolbar.test.tsx#WatchlistToolbar Show only" + status: pass + human_judgment: false + - deliverable: "In-card edit keeps a card that stopped matching until the next filter, sort, or search change" + verification: + - kind: test + ref: "web/app/routes/watchlist.test.tsx#keeps a card whose last mute was just unchecked until the next filter change" + status: pass + human_judgment: false + - deliverable: "Chip label toggles the shared tag filter without attach or detach; x still detaches; Phase 24 chip focus contract holds" + verification: + - kind: test + ref: "web/app/components/watchlist/TagChips.test.tsx#chip label filter toggle (D-08)" + status: pass + - kind: test + ref: "web/app/routes/watchlist.test.tsx#Watchlist card chip click-to-filter (D-08)" + status: pass + human_judgment: false + - deliverable: "Scroll position holds on chip and toggle clicks in a 50+ artist list; pressed-toggle Check meets 3:1 against #27272a; 32-character label with Check truncates at 375px" + verification: [] + human_judgment: true + rationale: "Scroll restoration, measured contrast, and truncation at width are only observable in a browser; deferred to end-of-phase UAT per the plan's human-check" +--- + +# Phase 25 Plan 08: Preference toggles and chip click-to-filter Summary + +The Watchlist gains `Show only` toggles for `Has muted events` and `Custom release types` (URL params `muted=1` / `custom=1`, AND-composed with name, tags, and sort), and each card's tag chip label becomes a one-click `Filter by {tag}` toggle that shares the toolbar's `tags` filter while the x remains the only detach. + +## Accomplishments + +- `watchlistView.ts` adds `DEFAULT_RELEASE_TYPES`, the `muted`/`custom` view fields (only `1` is honored), `hasMutedEvents`, and `hasCustomReleaseTypes` (set comparison). `selectVisible` ANDs both axes and `isFiltered` counts them, so `Clear filters` shows for a lone toggle. +- `WatchlistToolbar` renders a `Show only` fieldset between the tag filter and `Clear filters`: two default-height `Button`s, `outline` off and `secondary` on, `aria-pressed`, and a leading `Check` only when on. +- The route writes the flags with push, clears sticky cards, and schedules the count announcement. The existing canonicalization effect now also drops invalid `muted`/`custom` values with replace. +- `TagChips` focus now targets `[data-chip-remove]` (both confirmed and pending x buttons), so the new label buttons cannot shift focus on removal. All Phase 24 focus tests stayed green. +- Confirmed chips carry a `Filter by {tag}` button with `aria-pressed`; an active tag shows a `Check` and `ring-1 ring-inset ring-foreground/30`. Pending chips keep plain-text labels. +- The route passes `filterTagIds` and `toggleFilterTag` on `tagActions`; the label click reuses `handleTagsChange` and seeds `tagNamesRef` first. + +## Task Commits + +1. Task 1 (tracer): `d888568` feat(25-08): filter the Watchlist to muted events and custom release types +2. Task 2 refactor: `e9e8370` refactor(25-08): target chip focus by data-chip-remove and wire tag filter actions +3. Task 2 RED: `30cdd64` test(25-08): pin the chip label filter toggle and its separation from detach +4. Task 2 GREEN: `f7d460b` feat(25-08): let a card's tag chip label toggle the tag filter + +## Deviations from Plan + +### Auto-fixed Issues + +None. + +### Notes + +**1. [Ordering] Focus fix and action wiring landed as a behavior-neutral refactor commit before RED** +- The plan puts the `data-chip-remove` fix first with `TagChips.test.tsx` green before touching markup. Making `filterTagIds` and `toggleFilterTag` required on `TagActions` forced the route and existing test helpers to change at the same time, so that step became its own `refactor` commit. The full chip and route suites were green on it. + +**2. [Test harness] Tailwind class ordering** +- `prettier-plugin-tailwindcss` reorders the active-ring classes, so the test asserts each of `ring-1`, `ring-inset`, `ring-foreground/30` on the badge classList rather than one joined string. + +**3. [Test harness] Checkbox accessible names** +- The mute checkbox name includes its visible label, so the route test queries it with `/^Mute guest feature alerts/`. + +**4. [Unresolved by design] WLVW-05 edge row and sticky chip deselect** +- Per the plan's flagged assumptions, the WLVW-05 edge row stays unresolved; empty sets, set equality, and AND composition are pinned by unit tests. When a chip click deselects a tag on a card that was visible only because it was sticky, the card can leave the view and focus is left to the browser. Flagged for UI review. + +**Total deviations:** 0 auto-fixed, 4 notes. **Impact:** none on scope. + +## Verification + +- `corepack pnpm test` (full web suite with coverage): 30 files, 566 tests passed; coverage 95.18 / 90.57 / 95.75 / 97.18 +- `corepack pnpm run typecheck`: exit 0 +- `prettier --check "**/*.{ts,tsx}"`: clean +- Acceptance greps: `Has muted events` x1 and `Custom release types` x1 in the toolbar, `aria-pressed` present, `export function hasCustomReleaseTypes` x1, `data-chip-remove` x3, `Filter by ` x1, `toggleFilterTag` x2 in `TagChips.tsx` +- Pending human check: scroll position on 50+ artists, adding an artist under an active toggle, `Check` contrast against `#27272a`, and a 32-character tag with `Check` at 375px. Deferred to end-of-phase UAT. + +## Known Stubs + +None. + +## Threat Flags + +None. T-25-23 (label only calls `toggleFilterTag`; tests assert `attachTag`/`detachTag` are never called from it; distinct names and hover fills), T-25-24 (only `1` honored, anything else rewritten away with replace), and T-25-25 (tag names only as JSX text and escaped attributes) are mitigated as planned. + +## Self-Check: PASSED + +- FOUND commits d888568, e9e8370, 30cdd64, f7d460b +- FOUND modified files listed in key-files +- All task acceptance criteria re-run and passing From 4ec4cc761c7055c0065ac702a4d692040d0a749e Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 12:19:50 -0500 Subject: [PATCH 047/142] docs(25-08): update state and roadmap for completed plan --- .planning/REQUIREMENTS.md | 4 ++-- .planning/ROADMAP.md | 6 +++--- .planning/STATE.md | 16 +++++++++------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 8061b3b..b81d308 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -26,7 +26,7 @@ - [x] **WLVW-02**: User can sort by name (A–Z/Z–A) or date added (newest/oldest), with a stable tie-break - [x] **WLVW-03**: User can sort by latest release (the artist's newest own `new_release` event date that is not upcoming); artists with none sort last in both directions - [x] **WLVW-04**: User can filter the watchlist by one or more tags -- [ ] **WLVW-05**: User can filter to artists with muted event types or non-default release-type filters +- [x] **WLVW-05**: User can filter to artists with muted event types or non-default release-type filters - [x] **WLVW-06**: Search, sort, and filters combine; the view shows "N of M artists" and an empty state with a clear-filters action ### History @@ -93,7 +93,7 @@ Which phases cover which requirements. Updated during roadmap creation. | WLVW-02 | Phase 25 | Complete | | WLVW-03 | Phase 25 | Complete | | WLVW-04 | Phase 25 | Complete | -| WLVW-05 | Phase 25 | Pending | +| WLVW-05 | Phase 25 | Complete | | WLVW-06 | Phase 25 | Complete | | HIST-02 | Phase 25 | Complete | | BULK-01 | Phase 26 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 450d4b7..8a214ae 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -193,7 +193,7 @@ Plans: 4. The user can filter History by a tag alongside the existing filters. It lists events for artists carrying that tag, including artists since removed from the watchlist, and it never shows an event older than the retention window, either on the first page or via "load older". 5. Opening the Watchlist makes one request regardless of watchlist size. Searching, sorting, and filtering then happen instantly with no further requests. -**Plans:** 8/10 plans executed +**Plans:** 9/10 plans executed Plans: **Wave 1** @@ -218,7 +218,7 @@ Plans: **Wave 5** *(blocked on Wave 4 completion)* -- [ ] 25-08-PLAN.md — `Has muted events` / `Custom release types` toggles with full AND composition, and the card chip label as a filter toggle (D-08) +- [x] 25-08-PLAN.md — `Has muted events` / `Custom release types` toggles with full AND composition, and the card chip label as a filter toggle (D-08) **Wave 6** *(blocked on Wave 5 completion)* @@ -327,7 +327,7 @@ Phases execute in numeric order: 24 → 25 → 26 → 27 → 28 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 24. Artist Tags & Notes | 14/14 | Complete | 2026-10-06 | -| 25. Find & Filter — Watchlist and History | 8/10 | In Progress| | +| 25. Find & Filter — Watchlist and History | 9/10 | In Progress| | | 26. Bulk Edit & Remove | 0/? | Not started | - | | 27. Paste-a-List Bulk Add | 0/? | Not started | - | | 28. Tags on Discord Notifications | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index a15f98b..9fa9113 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,16 +5,16 @@ milestone_name: Watchlist Organization current_phase: 25 current_phase_name: Find & Filter — Watchlist and History status: executing -stopped_at: Completed 25-07-PLAN.md -last_updated: "2026-10-10T17:10:52.264Z" +stopped_at: Completed 25-08-PLAN.md +last_updated: "2026-10-10T17:19:42.160Z" last_activity: 2026-10-10 last_activity_desc: Phase 25 execution started -state_head: 139429c8c6f1298280f7e9fa31930cbe1069beb1 +state_head: 88b6b4039e6433364806895bd3e5894d4f228f9e progress: total_phases: 5 completed_phases: 1 total_plans: 24 - completed_plans: 22 + completed_plans: 23 percent: 20 --- @@ -30,7 +30,7 @@ See: .planning/PROJECT.md (updated 2026-10-06) ## Current Position Phase: 25 (Find & Filter — Watchlist and History) — EXECUTING -Plan: 9 of 10 +Plan: 10 of 10 Status: Ready to execute Last activity: 2026-10-10 — Phase 25 execution started @@ -170,6 +170,7 @@ Last activity: 2026-10-10 — Phase 25 execution started | Phase 25 P05 | 5 min | 2 tasks | 6 files | | Phase 25 P09 | 4 min | 2 tasks | 6 files | | Phase 25 P07 | 40 min | 3 tasks | 9 files | +| Phase 25 P08 | 25 min | 2 tasks | 8 files | ## Accumulated Context @@ -405,6 +406,7 @@ Recent decisions affecting current work: - [Phase 25]: 25-05: Latest-release sort compares raw text (2024 before 2024-05), nulls last in both directions, artist_id ascending tie-break in every mode - [Phase 25]: 25-09: History stale artist/tag ids are checked once per source per mount, only after a successful load; a failed fetch keeps the filter and the trigger reads Artist unavailable / Tag unavailable — A failed fetch is not proof an id is stale (UI-SPEC History Tag filter) - [Phase 25]: 25-07: Esc on a closed tag-filter popup is cancelled (escape-key reason) so it cannot clear every chip; tags URL param serializes commas as %2C +- [Phase 25]: 25-08: Custom release types is a set comparison against the default four; chip label click reuses handleTagsChange and the x remains the only detach ### Pending Todos @@ -498,8 +500,8 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-10T17:10:52.181Z -Stopped at: Completed 25-07-PLAN.md +Last session: 2026-10-10T17:19:42.041Z +Stopped at: Completed 25-08-PLAN.md Resume file: None ## Operator Next Steps From c38d832372a6c207840cd0a01b2a77ba854ca489 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 12:21:19 -0500 Subject: [PATCH 048/142] build(25-10): rebuild embedded SPA with watchlist toolbar, latest-release lines, and history tag control --- .../build/client/assets/CoverArt-hXVrj8Fw.js | 1 - .../assets/{api-Bh4xuY1_.js => api-k_NcBvGr.js} | 2 +- .../{badge-mjpdUX1d.js => badge-B492YG3Q.js} | 2 +- ...En7.js => createBaseUIEventDetails-wEMuC0RZ.js} | 2 +- ...client-DJJfRPCk.js => entry.client-oFNB33ee.js} | 6 +++--- .../build/client/assets/history-CiwTCwWj.js | 1 + .../build/client/assets/history-MfMHuFqK.js | 1 - .../build/client/assets/input-Dx1GFl8F.js | 1 - .../build/client/assets/input-IDkwig6l.js | 1 + ...runtime-DT4cu4GC.js => jsx-runtime-D0QO-s14.js} | 14 +++++++------- .../build/client/assets/manifest-645baaf8.js | 1 + .../build/client/assets/manifest-cf0f6d83.js | 1 - ...react-dom-I-EqbIrq.js => react-dom-B1AcAK82.js} | 2 +- .../build/client/assets/root-BhE8j8TA.css | 2 -- .../assets/{root-Be7OCgpV.js => root-BiKnkZEa.js} | 2 +- .../build/client/assets/root-CdD2OkTO.css | 2 ++ .../build/client/assets/select-BBue9CB_.js | 1 + .../build/client/assets/system-BOnhL60J.js | 1 - .../build/client/assets/system-DHkZsAhb.js | 1 + ...lert-B9xMLdUt.js => triangle-alert-Br4OJW5s.js} | 2 +- .../assets/useAnchoredPopupScrollLock-Bx0zR8zl.js | 1 - .../build/client/assets/useUrlParams-DI9KFxsS.js | 1 + .../build/client/assets/watchlist-BuIICAgv.js | 1 + .../build/client/assets/watchlist-DCNGWDcG.js | 1 - internal/webassets/build/client/index.html | 8 ++++---- 25 files changed, 29 insertions(+), 29 deletions(-) delete mode 100644 internal/webassets/build/client/assets/CoverArt-hXVrj8Fw.js rename internal/webassets/build/client/assets/{api-Bh4xuY1_.js => api-k_NcBvGr.js} (93%) rename internal/webassets/build/client/assets/{badge-mjpdUX1d.js => badge-B492YG3Q.js} (93%) rename internal/webassets/build/client/assets/{createBaseUIEventDetails-BTtuUEn7.js => createBaseUIEventDetails-wEMuC0RZ.js} (75%) rename internal/webassets/build/client/assets/{entry.client-DJJfRPCk.js => entry.client-oFNB33ee.js} (86%) create mode 100644 internal/webassets/build/client/assets/history-CiwTCwWj.js delete mode 100644 internal/webassets/build/client/assets/history-MfMHuFqK.js delete mode 100644 internal/webassets/build/client/assets/input-Dx1GFl8F.js create mode 100644 internal/webassets/build/client/assets/input-IDkwig6l.js rename internal/webassets/build/client/assets/{jsx-runtime-DT4cu4GC.js => jsx-runtime-D0QO-s14.js} (83%) create mode 100644 internal/webassets/build/client/assets/manifest-645baaf8.js delete mode 100644 internal/webassets/build/client/assets/manifest-cf0f6d83.js rename internal/webassets/build/client/assets/{react-dom-I-EqbIrq.js => react-dom-B1AcAK82.js} (71%) delete mode 100644 internal/webassets/build/client/assets/root-BhE8j8TA.css rename internal/webassets/build/client/assets/{root-Be7OCgpV.js => root-BiKnkZEa.js} (90%) create mode 100644 internal/webassets/build/client/assets/root-CdD2OkTO.css create mode 100644 internal/webassets/build/client/assets/select-BBue9CB_.js delete mode 100644 internal/webassets/build/client/assets/system-BOnhL60J.js create mode 100644 internal/webassets/build/client/assets/system-DHkZsAhb.js rename internal/webassets/build/client/assets/{triangle-alert-B9xMLdUt.js => triangle-alert-Br4OJW5s.js} (74%) delete mode 100644 internal/webassets/build/client/assets/useAnchoredPopupScrollLock-Bx0zR8zl.js create mode 100644 internal/webassets/build/client/assets/useUrlParams-DI9KFxsS.js create mode 100644 internal/webassets/build/client/assets/watchlist-BuIICAgv.js delete mode 100644 internal/webassets/build/client/assets/watchlist-DCNGWDcG.js diff --git a/internal/webassets/build/client/assets/CoverArt-hXVrj8Fw.js b/internal/webassets/build/client/assets/CoverArt-hXVrj8Fw.js deleted file mode 100644 index 60ed6d8..0000000 --- a/internal/webassets/build/client/assets/CoverArt-hXVrj8Fw.js +++ /dev/null @@ -1 +0,0 @@ -import{A as e,M as t,t as n}from"./jsx-runtime-DT4cu4GC.js";import{st as r,w as i}from"./api-Bh4xuY1_.js";var a=r(`music`,[[`path`,{d:`M9 18V5l12-2v13`,key:`1jmyc2`}],[`circle`,{cx:`6`,cy:`18`,r:`3`,key:`fqmcym`}],[`circle`,{cx:`18`,cy:`16`,r:`3`,key:`1hluhg`}]]),o=t(e(),1),s=n();function c({src:e,alt:t,size:n=96,className:r}){let[c,l]=(0,o.useState)(!1);(0,o.useEffect)(()=>{l(!1)},[e]);let u=!e||c,d={width:n,height:n};return u?(0,s.jsx)(`div`,{role:`img`,"aria-label":t,className:i(`flex shrink-0 items-center justify-center rounded-md bg-secondary text-muted-foreground`,r),style:d,children:(0,s.jsx)(a,{className:`h-1/3 w-1/3`,"aria-hidden":`true`})}):(0,s.jsx)(`img`,{src:e,alt:t,style:d,className:i(`shrink-0 rounded-md object-cover`,r),onError:()=>l(!0)})}export{c as t}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/api-Bh4xuY1_.js b/internal/webassets/build/client/assets/api-k_NcBvGr.js similarity index 93% rename from internal/webassets/build/client/assets/api-Bh4xuY1_.js rename to internal/webassets/build/client/assets/api-k_NcBvGr.js index 1342d16..f32910b 100644 --- a/internal/webassets/build/client/assets/api-Bh4xuY1_.js +++ b/internal/webassets/build/client/assets/api-k_NcBvGr.js @@ -1 +1 @@ -import{A as e,M as t,t as n}from"./jsx-runtime-DT4cu4GC.js";var r=t(e(),1),i=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),a=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),o=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(e,t,n)=>n?n.toUpperCase():t.toLowerCase()),s=e=>{let t=o(e);return t.charAt(0).toUpperCase()+t.slice(1)},c={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},l=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0;return!1},u=(0,r.createContext)({}),d=()=>(0,r.useContext)(u),f=(0,r.forwardRef)(({color:e,size:t,strokeWidth:n,absoluteStrokeWidth:a,className:o=``,children:s,iconNode:u,...f},p)=>{let{size:m=24,strokeWidth:h=2,absoluteStrokeWidth:g=!1,color:_=`currentColor`,className:v=``}=d()??{},y=a??g?Number(n??h)*24/Number(t??m):n??h;return(0,r.createElement)(`svg`,{ref:p,...c,width:t??m??c.width,height:t??m??c.height,stroke:e??_,strokeWidth:y,className:i(`lucide`,v,o),...!s&&!l(f)&&{"aria-hidden":`true`},...f},[...u.map(([e,t])=>(0,r.createElement)(e,t)),...Array.isArray(s)?s:[s]])}),p=(e,t)=>{let n=(0,r.forwardRef)(({className:n,...o},c)=>(0,r.createElement)(f,{ref:c,iconNode:t,className:i(`lucide-${a(s(e))}`,`lucide-${e}`,n),...o}));return n.displayName=s(e),n},m=p(`loader-circle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]);function h(){return typeof window<`u`}function g(e){return y(e)?(e.nodeName||``).toLowerCase():`#document`}function _(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function v(e){return((y(e)?e.ownerDocument:e.document)||window.document)?.documentElement}function y(e){return h()?e instanceof Node||e instanceof _(e).Node:!1}function b(e){return h()?e instanceof Element||e instanceof _(e).Element:!1}function x(e){return h()?e instanceof HTMLElement||e instanceof _(e).HTMLElement:!1}function S(e){return!h()||typeof ShadowRoot>`u`?!1:e instanceof ShadowRoot||e instanceof _(e).ShadowRoot}function C(e){let{overflow:t,overflowX:n,overflowY:r,display:i}=A(e);return/auto|scroll|overlay|hidden|clip/.test(t+r+n)&&i!==`inline`&&i!==`contents`}function w(e){return/^(table|td|th)$/.test(g(e))}function T(e){try{if(e.matches(`:popover-open`))return!0}catch{}try{return e.matches(`:modal`)}catch{return!1}}var ee=/transform|translate|scale|rotate|perspective|filter/,te=/paint|layout|strict|content/,E=e=>!!e&&e!==`none`,ne;function re(e){let t=b(e)?A(e):e;return E(t.transform)||E(t.translate)||E(t.scale)||E(t.rotate)||E(t.perspective)||!O()&&(E(t.backdropFilter)||E(t.filter))||ee.test(t.willChange||``)||te.test(t.contain||``)}function D(e){let t=j(e);for(;x(t)&&!k(t);){if(re(t))return t;if(T(t))return null;t=j(t)}return null}function O(){return ne??=typeof CSS<`u`&&CSS.supports&&CSS.supports(`-webkit-backdrop-filter`,`none`),ne}function k(e){return/^(html|body|#document)$/.test(g(e))}function A(e){return _(e).getComputedStyle(e)}function ie(e){return b(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function j(e){if(g(e)===`html`)return e;let t=e.assignedSlot||e.parentNode||S(e)&&e.host||v(e);return S(t)?t.host:t}function ae(e){let t=j(e);return k(t)?(e.ownerDocument||e).body:x(t)&&C(t)?t:ae(t)}function oe(e,t,n){t===void 0&&(t=[]),n===void 0&&(n=!0);let r=ae(e),i=r===e.ownerDocument?.body,a=_(r);if(i){let e=se(a);return t.concat(a,a.visualViewport||[],C(r)?r:[],e&&n?oe(e):[])}else return t.concat(r,oe(r,[],n))}function se(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}var M={...r},N={};function P(e,t){let n=r.useRef(N);return n.current===N&&(n.current=e(t)),n}var F=M.useInsertionEffect,ce=F&&F!==M.useLayoutEffect?F:e=>e();function I(e){let t=P(le).current;return t.next=e,ce(t.effect),t.trampoline}function le(){let e={next:void 0,callback:ue,trampoline:(...t)=>e.callback?.(...t),effect:()=>{e.callback=e.next}};return e}function ue(){}var L=typeof document<`u`?r.useLayoutEffect:()=>{};function R(e,t){if(e&&!t)return e;if(!e&&t)return t;if(e||t)return{...e,...t}}var z={};function de(e,t,n,r,i){if(!n&&!r&&!i&&!e)return pe(t);let a=pe(e);return t&&(a=B(a,t)),n&&(a=B(a,n)),r&&(a=B(a,r)),i&&(a=B(a,i)),a}function fe(e){if(e.length===0)return z;if(e.length===1)return pe(e[0]);let t=pe(e[0]);for(let n=1;n=65&&i<=90&&(typeof t==`function`||t===void 0)}function _e(e){return typeof e==`function`}function ve(e,t){return _e(e)?e(t):e??z}function ye(e,t){return t?e?(...n)=>{let r=n[0];if(Ce(r)){let i=r;xe(i);let a=t(...n);return i.baseUIHandlerPrevented||e?.(...n),a}let i=t(...n);return e?.(...n),i}:be(t):e}function be(e){return e&&((...t)=>{let n=t[0];return Ce(n)&&xe(n),e(...t)})}function xe(e){return e.preventBaseUIHandler=()=>{e.baseUIHandlerPrevented=!0},e}function Se(e,t){return t?e?t+` `+e:t:e}function Ce(e){return typeof e==`object`&&!!e&&`nativeEvent`in e}function we(e,t){return function(n,...r){let i=new URL(e);return i.searchParams.set(`code`,n.toString()),r.forEach(e=>i.searchParams.append(`args[]`,e)),`${t} error #${n}; visit ${i} for the full message.`}}var Te=we(`https://base-ui.com/production-error`,`Base UI`),Ee=r.createContext(void 0);function De(e=!1){let t=r.useContext(Ee);if(t===void 0&&!e)throw Error(Te(16));return t}function Oe(e){let{focusableWhenDisabled:t,disabled:n,composite:i=!1,tabIndex:a=0,isNativeButton:o}=e,s=i&&t!==!1,c=i&&t===!1;return{props:r.useMemo(()=>{let e={onKeyDown(e){n&&t&&e.key!==`Tab`&&e.preventDefault()}};return i||(e.tabIndex=a,!o&&n&&(e.tabIndex=t?a:-1)),(o&&(t||s)||!o&&n)&&(e[`aria-disabled`]=n),o&&(!t||c)&&(e.disabled=n),e},[i,n,t,s,c,o,a])}}function ke(e){return e?.ownerDocument||document}function Ae(e,t,{detail:n=0}={}){e.dispatchEvent(new(_(e)).PointerEvent(`click`,{bubbles:!0,cancelable:!0,composed:!0,detail:n,shiftKey:t.shiftKey,ctrlKey:t.ctrlKey,altKey:t.altKey,metaKey:t.metaKey}))}function je(e={}){let{disabled:t=!1,focusableWhenDisabled:n,tabIndex:i=0,native:a=!0,composite:o}=e,s=r.useRef(null),c=De(!0),l=o??c!==void 0,{props:u}=Oe({focusableWhenDisabled:n,disabled:t,composite:l,tabIndex:i,isNativeButton:a}),d=r.useCallback(()=>{let e=s.current;Me(e)&&l&&t&&u.disabled===void 0&&e.disabled&&(e.disabled=!1)},[t,u.disabled,l]);return L(d,[d]),{getButtonProps:r.useCallback((e={})=>{let{onClick:n,onMouseDown:r,onKeyUp:i,onKeyDown:o,onPointerDown:s,...c}=e;return de({onClick(e){if(t){e.preventDefault();return}n?.(e)},onMouseDown(e){t||r?.(e)},onKeyDown(e){if(t||(xe(e),o?.(e),e.baseUIHandlerPrevented))return;let n=e.target===e.currentTarget,r=e.currentTarget,i=Me(r),s=!a&&Ne(r),c=n&&(a?i:!s),u=e.key===`Enter`,d=e.key===` `,f=r.getAttribute(`role`),p=f?.startsWith(`menuitem`)||f===`option`||f===`gridcell`;if(n&&l&&d){if(e.defaultPrevented&&p)return;e.preventDefault(),(!a||i)&&(e.preventBaseUIHandler(),Ae(r,e));return}if(!c||a||!d&&!u){n&&s&&d&&e.preventDefault();return}e.defaultPrevented||(e.preventDefault(),u&&(e.preventBaseUIHandler(),Ae(r,e)))},onKeyUp(e){if(!t){if(xe(e),i?.(e),e.target===e.currentTarget&&a&&l&&Me(e.currentTarget)&&e.key===` `){e.preventDefault();return}e.baseUIHandlerPrevented||e.target===e.currentTarget&&!a&&!l&&!e.defaultPrevented&&e.key===` `&&(e.preventBaseUIHandler(),Ae(e.currentTarget,e))}},onPointerDown(e){if(t){e.preventDefault();return}s?.(e)}},a?{type:`button`}:{role:`button`},u,c)},[t,u,l,a]),buttonRef:I(e=>{s.current=e,d()})}}function Me(e){return x(e)&&e.tagName===`BUTTON`}function Ne(e){return x(e)&&e.tagName===`A`&&!!e.href}function Pe(e,t,n,r){let i=P(Ie).current;return Le(i,e,t,n,r)&&ze(i,[e,t,n,r]),i.callback}function Fe(e){let t=P(Ie).current;return Re(t,e)&&ze(t,e),t.callback}function Ie(){return{callback:null,cleanup:null,refs:[]}}function Le(e,t,n,r,i){return e.refs[0]!==t||e.refs[1]!==n||e.refs[2]!==r||e.refs[3]!==i}function Re(e,t){return e.refs.length!==t.length||e.refs.some((e,n)=>e!==t[n])}function ze(e,t){if(e.refs=t,t.every(e=>e==null)){e.callback=null;return}e.callback=n=>{if(e.cleanup&&=(e.cleanup(),null),n!=null){let r=Array(t.length).fill(null);for(let e=0;e{for(let e=0;e=e}function He(e){if(!r.isValidElement(e))return null;let t=e,n=t.props;return(Ve(19)?n?.ref:t.ref)??null}function Ue(){}var We=Object.freeze([]),V=Object.freeze({});function Ge(e,t){let n={};for(let r in e){let i=e[r];if(t?.hasOwnProperty(r)){let e=t[r](i);e!=null&&Object.assign(n,e);continue}i===!0?n[`data-${r.toLowerCase()}`]=``:i&&(n[`data-${r.toLowerCase()}`]=i.toString())}return n}function Ke(e,t){return typeof e==`function`?e(t):e}function qe(e,t){return typeof e==`function`?e(t):e}function Je(e,t,n={}){let r=t.render,i=Ye(t,n);return n.enabled===!1?null:Qe(e,r,i,n.state??V)}function Ye(e,t={}){let{className:n,style:r,render:i}=e,{state:a=V,ref:o,props:s,stateAttributesMapping:c,enabled:l=!0}=t,u=l?Ke(n,a):void 0,d=l?qe(r,a):void 0,f=l?Ge(a,c):V,p=l&&s?Xe(s):void 0,m=l?R(f,p)??{}:V;return typeof document<`u`&&(l?Array.isArray(o)?m.ref=Fe([m.ref,He(i),...o]):m.ref=Pe(m.ref,He(i),o):Pe(null,null)),l?(u!==void 0&&(m.className=Se(m.className,u)),d!==void 0&&(m.style=R(m.style,d)),m):V}function Xe(e){return Array.isArray(e)?fe(e):de(void 0,e)}var Ze=Symbol.for(`react.lazy`);function Qe(e,t,n,i){if(t){if(typeof t==`function`)return t(n,i);let e=de(n,t.props);e.ref=n.ref;let a=t;return a?.$$typeof===Ze&&(a=r.Children.toArray(t)[0]),r.cloneElement(a,e)}if(e&&typeof e==`string`)return $e(e,n);throw Error(Te(8))}function $e(e,t){return e===`button`?(0,r.createElement)(`button`,{type:`button`,...t,key:t.key}):e===`img`?(0,r.createElement)(`img`,{alt:``,...t,key:t.key}):r.createElement(e,t)}var et=r.forwardRef(function(e,t){let{render:n,className:r,disabled:i=!1,focusableWhenDisabled:a=!1,nativeButton:o=!0,style:s,...c}=e,{getButtonProps:l,buttonRef:u}=je({disabled:i,focusableWhenDisabled:a,native:o});return Je(`button`,e,{state:{disabled:i},ref:[t,u],props:[c,l]})});function tt(e){var t,n,r=``;if(typeof e==`string`||typeof e==`number`)r+=e;else if(typeof e==`object`)if(Array.isArray(e)){var i=e.length;for(t=0;ttypeof e==`boolean`?`${e}`:e===0?`0`:e,it=nt,at=(e,t)=>n=>{if(t?.variants==null)return it(e,n?.class,n?.className);let{variants:r,defaultVariants:i}=t,a=Object.keys(r).map(e=>{let t=n?.[e],a=i?.[e];if(t===null)return null;let o=rt(t)||rt(a);return r[e][o]}),o=n&&Object.entries(n).reduce((e,t)=>{let[n,r]=t;return r===void 0||(e[n]=r),e},{});return it(e,a,t?.compoundVariants?.reduce((e,t)=>{let{class:n,className:r,...a}=t;return Object.entries(a).every(e=>{let[t,n]=e;return Array.isArray(n)?n.includes({...i,...o}[t]):{...i,...o}[t]===n})?[...e,n,r]:e},[]),n?.class,n?.className)},ot=(e,t)=>{let n=Array(e.length+t.length);for(let t=0;t({classGroupId:e,validator:t}),ct=(e=new Map,t=null,n)=>({nextPart:e,validators:t,classGroupId:n}),lt=`-`,ut=[],dt=`arbitrary..`,ft=e=>{let t=ht(e),{conflictingClassGroups:n,conflictingClassGroupModifiers:r}=e;return{getClassGroupId:e=>{if(e.startsWith(`[`)&&e.endsWith(`]`))return mt(e);let n=e.split(lt);return pt(n,+(n[0]===``&&n.length>1),t)},getConflictingClassGroupIds:(e,t)=>{if(t){let t=r[e],i=n[e];return t?i?ot(i,t):t:i||ut}return n[e]||ut}}},pt=(e,t,n)=>{if(e.length-t===0)return n.classGroupId;let r=e[t],i=n.nextPart.get(r);if(i){let n=pt(e,t+1,i);if(n)return n}let a=n.validators;if(a===null)return;let o=t===0?e.join(lt):e.slice(t).join(lt),s=a.length;for(let e=0;ee.slice(1,-1).indexOf(`:`)===-1?void 0:(()=>{let t=e.slice(1,-1),n=t.indexOf(`:`),r=t.slice(0,n);return r?dt+r:void 0})(),ht=e=>{let{theme:t,classGroups:n}=e;return gt(n,t)},gt=(e,t)=>{let n=ct();for(let r in e){let i=e[r];_t(i,n,r,t)}return n},_t=(e,t,n,r)=>{let i=e.length;for(let a=0;a{if(typeof e==`string`){yt(e,t,n);return}if(typeof e==`function`){bt(e,t,n,r);return}xt(e,t,n,r)},yt=(e,t,n)=>{let r=e===``?t:St(t,e);r.classGroupId=n},bt=(e,t,n,r)=>{if(Ct(e)){_t(e(r),t,n,r);return}t.validators===null&&(t.validators=[]),t.validators.push(st(n,e))},xt=(e,t,n,r)=>{let i=Object.entries(e),a=i.length;for(let e=0;e{let n=e,r=t.split(lt),i=r.length;for(let e=0;e`isThemeGetter`in e&&e.isThemeGetter===!0,wt=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let t=0,n=Object.create(null),r=Object.create(null),i=(i,a)=>{n[i]=a,t++,t>e&&(t=0,r=n,n=Object.create(null))};return{get(e){let t=n[e];if(t!==void 0)return t;if((t=r[e])!==void 0)return i(e,t),t},set(e,t){e in n?n[e]=t:i(e,t)}}},Tt=`!`,Et=`:`,Dt=[],Ot=(e,t,n,r,i)=>({modifiers:e,hasImportantModifier:t,baseClassName:n,maybePostfixModifierPosition:r,isExternal:i}),kt=e=>{let{prefix:t,experimentalParseClassName:n}=e,r=e=>{let t=[],n=0,r=0,i=0,a,o=e.length;for(let s=0;si?a-i:void 0;return Ot(t,l,c,u)};if(t){let e=t+Et,n=r;r=t=>t.startsWith(e)?n(t.slice(e.length)):Ot(Dt,!1,t,void 0,!0)}if(n){let e=r;r=t=>n({className:t,parseClassName:e})}return r},At=e=>{let t=new Map;return e.orderSensitiveModifiers.forEach((e,n)=>{t.set(e,1e6+n)}),e=>{let n=[],r=[];for(let i=0;i0&&(r.sort(),n.push(...r),r=[]),n.push(a)):r.push(a)}return r.length>0&&(r.sort(),n.push(...r)),n}},jt=e=>({cache:wt(e.cacheSize),parseClassName:kt(e),sortModifiers:At(e),postfixLookupClassGroupIds:Mt(e),...ft(e)}),Mt=e=>{let t=Object.create(null),n=e.postfixLookupClassGroups;if(n)for(let e=0;e{let{parseClassName:n,getClassGroupId:r,getConflictingClassGroupIds:i,sortModifiers:a,postfixLookupClassGroupIds:o}=t,s=[],c=e.trim().split(Nt),l=``;for(let e=c.length-1;e>=0;--e){let t=c[e],{isExternal:u,modifiers:d,hasImportantModifier:f,baseClassName:p,maybePostfixModifierPosition:m}=n(t);if(u){l=t+(l.length>0?` `+l:l);continue}let h=!!m,g;if(h){g=r(p.substring(0,m));let e=g&&o[g]?r(p):void 0;e&&e!==g&&(g=e,h=!1)}else g=r(p);if(!g){if(!h){l=t+(l.length>0?` `+l:l);continue}if(g=r(p),!g){l=t+(l.length>0?` `+l:l);continue}h=!1}let _=d.length===0?``:d.length===1?d[0]:a(d).join(`:`),v=f?_+Tt:_,y=v+g;if(s.indexOf(y)>-1)continue;s.push(y);let b=i(g,h);for(let e=0;e0?` `+l:l)}return l},Ft=(...e)=>{let t=0,n,r,i=``;for(;t{if(typeof e==`string`)return e;let t,n=``;for(let r=0;r{let n,r,i,a,o=o=>(n=jt(t.reduce((e,t)=>t(e),e())),r=n.cache.get,i=n.cache.set,a=s,s(o)),s=e=>{let t=r(e);if(t)return t;let a=Pt(e,n);return i(e,a),a};return a=o,(...e)=>a(Ft(...e))},Rt=[],H=e=>{let t=t=>t[e]||Rt;return t.isThemeGetter=!0,t},zt=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,Bt=/^\((?:(\w[\w-]*):)?(.+)\)$/i,Vt=/^\d+(?:\.\d+)?\/\d+(?:\.\d+)?$/,Ht=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,Ut=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,Wt=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,Gt=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,Kt=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,U=e=>Vt.test(e),W=e=>!!e&&!Number.isNaN(Number(e)),G=e=>!!e&&Number.isInteger(Number(e)),qt=e=>e.endsWith(`%`)&&W(e.slice(0,-1)),K=e=>Ht.test(e),Jt=()=>!0,Yt=e=>Ut.test(e)&&!Wt.test(e),Xt=()=>!1,Zt=e=>Gt.test(e),Qt=e=>Kt.test(e),$t=e=>!q(e)&&!Y(e),en=e=>e.startsWith(`@container`)&&(e[10]===`/`&&e[11]!==void 0||e[11]===`s`&&e[16]!==void 0&&e.startsWith(`-size/`,10)||e[11]===`n`&&e[18]!==void 0&&e.startsWith(`-normal/`,10)),tn=e=>X(e,vn,Xt),q=e=>zt.test(e),J=e=>X(e,yn,Yt),nn=e=>X(e,bn,W),rn=e=>X(e,Sn,Jt),an=e=>X(e,xn,Xt),on=e=>X(e,gn,Xt),sn=e=>X(e,_n,Qt),cn=e=>X(e,Cn,Zt),Y=e=>Bt.test(e),ln=e=>Z(e,yn),un=e=>Z(e,xn),dn=e=>Z(e,gn),fn=e=>Z(e,vn),pn=e=>Z(e,_n),mn=e=>Z(e,Cn,!0),hn=e=>Z(e,Sn,!0),X=(e,t,n)=>{let r=zt.exec(e);return r?r[1]?t(r[1]):n(r[2]):!1},Z=(e,t,n=!1)=>{let r=Bt.exec(e);return r?r[1]?t(r[1]):n:!1},gn=e=>e===`position`||e===`percentage`,_n=e=>e===`image`||e===`url`,vn=e=>e===`length`||e===`size`||e===`bg-size`,yn=e=>e===`length`,bn=e=>e===`number`,xn=e=>e===`family-name`,Sn=e=>e===`number`||e===`weight`,Cn=e=>e===`shadow`,wn=Lt(()=>{let e=H(`color`),t=H(`font`),n=H(`text`),r=H(`font-weight`),i=H(`tracking`),a=H(`leading`),o=H(`breakpoint`),s=H(`container`),c=H(`spacing`),l=H(`radius`),u=H(`shadow`),d=H(`inset-shadow`),f=H(`text-shadow`),p=H(`drop-shadow`),m=H(`blur`),h=H(`perspective`),g=H(`aspect`),_=H(`ease`),v=H(`animate`),y=()=>[`auto`,`avoid`,`all`,`avoid-page`,`page`,`left`,`right`,`column`],b=()=>[`center`,`top`,`bottom`,`left`,`right`,`top-left`,`left-top`,`top-right`,`right-top`,`bottom-right`,`right-bottom`,`bottom-left`,`left-bottom`],x=()=>[...b(),Y,q],S=()=>[`auto`,`hidden`,`clip`,`visible`,`scroll`],C=()=>[`auto`,`contain`,`none`],w=()=>[Y,q,c],T=()=>[U,`full`,`auto`,...w()],ee=()=>[G,`none`,`subgrid`,Y,q],te=()=>[`auto`,{span:[`full`,G,Y,q]},G,Y,q],E=()=>[G,`auto`,Y,q],ne=()=>[`auto`,`min`,`max`,`fr`,Y,q],re=()=>[`start`,`end`,`center`,`between`,`around`,`evenly`,`stretch`,`baseline`,`center-safe`,`end-safe`],D=()=>[`start`,`end`,`center`,`stretch`,`center-safe`,`end-safe`],O=()=>[`auto`,...w()],k=()=>[U,`auto`,`full`,`dvw`,`dvh`,`lvw`,`lvh`,`svw`,`svh`,`min`,`max`,`fit`,...w()],A=()=>[U,`screen`,`full`,`dvw`,`lvw`,`svw`,`min`,`max`,`fit`,...w()],ie=()=>[U,`screen`,`full`,`lh`,`dvh`,`lvh`,`svh`,`min`,`max`,`fit`,...w()],j=()=>[e,Y,q],ae=()=>[...b(),dn,on,{position:[Y,q]}],oe=()=>[`no-repeat`,{repeat:[``,`x`,`y`,`space`,`round`]}],se=()=>[`auto`,`cover`,`contain`,fn,tn,{size:[Y,q]}],M=()=>[qt,ln,J],N=()=>[``,`none`,`full`,l,Y,q],P=()=>[``,W,ln,J],F=()=>[`solid`,`dashed`,`dotted`,`double`],ce=()=>[`normal`,`multiply`,`screen`,`overlay`,`darken`,`lighten`,`color-dodge`,`color-burn`,`hard-light`,`soft-light`,`difference`,`exclusion`,`hue`,`saturation`,`color`,`luminosity`],I=()=>[W,qt,dn,on],le=()=>[``,`none`,m,Y,q],ue=()=>[`none`,W,Y,q],L=()=>[`none`,W,Y,q],R=()=>[W,Y,q],z=()=>[U,`full`,...w()];return{cacheSize:500,theme:{animate:[`spin`,`ping`,`pulse`,`bounce`],aspect:[`video`],blur:[K],breakpoint:[K],color:[Jt],container:[K],"drop-shadow":[K],ease:[`in`,`out`,`in-out`],font:[$t],"font-weight":[`thin`,`extralight`,`light`,`normal`,`medium`,`semibold`,`bold`,`extrabold`,`black`],"inset-shadow":[K],leading:[`none`,`tight`,`snug`,`normal`,`relaxed`,`loose`],perspective:[`dramatic`,`near`,`normal`,`midrange`,`distant`,`none`],radius:[K],shadow:[K],spacing:[`px`,W],text:[K],"text-shadow":[K],tracking:[`tighter`,`tight`,`normal`,`wide`,`wider`,`widest`]},classGroups:{aspect:[{aspect:[`auto`,`square`,U,q,Y,g]}],container:[`container`],"container-type":[{"@container":[``,`normal`,`size`,Y,q]}],"container-named":[en],columns:[{columns:[W,q,Y,s]}],"break-after":[{"break-after":y()}],"break-before":[{"break-before":y()}],"break-inside":[{"break-inside":[`auto`,`avoid`,`avoid-page`,`avoid-column`]}],"box-decoration":[{"box-decoration":[`slice`,`clone`]}],box:[{box:[`border`,`content`]}],display:[`block`,`inline-block`,`inline`,`flex`,`inline-flex`,`table`,`inline-table`,`table-caption`,`table-cell`,`table-column`,`table-column-group`,`table-footer-group`,`table-header-group`,`table-row-group`,`table-row`,`flow-root`,`grid`,`inline-grid`,`contents`,`list-item`,`hidden`],sr:[`sr-only`,`not-sr-only`],float:[{float:[`right`,`left`,`none`,`start`,`end`]}],clear:[{clear:[`left`,`right`,`both`,`none`,`start`,`end`]}],isolation:[`isolate`,`isolation-auto`],"object-fit":[{object:[`contain`,`cover`,`fill`,`none`,`scale-down`]}],"object-position":[{object:x()}],overflow:[{overflow:S()}],"overflow-x":[{"overflow-x":S()}],"overflow-y":[{"overflow-y":S()}],overscroll:[{overscroll:C()}],"overscroll-x":[{"overscroll-x":C()}],"overscroll-y":[{"overscroll-y":C()}],position:[`static`,`fixed`,`absolute`,`relative`,`sticky`],inset:[{inset:T()}],"inset-x":[{"inset-x":T()}],"inset-y":[{"inset-y":T()}],start:[{"inset-s":T(),start:T()}],end:[{"inset-e":T(),end:T()}],"inset-bs":[{"inset-bs":T()}],"inset-be":[{"inset-be":T()}],top:[{top:T()}],right:[{right:T()}],bottom:[{bottom:T()}],left:[{left:T()}],visibility:[`visible`,`invisible`,`collapse`],z:[{z:[G,`auto`,Y,q]}],basis:[{basis:[U,`full`,`auto`,s,...w()]}],"flex-direction":[{flex:[`row`,`row-reverse`,`col`,`col-reverse`]}],"flex-wrap":[{flex:[`nowrap`,`wrap`,`wrap-reverse`]}],flex:[{flex:[W,U,`auto`,`initial`,`none`,q]}],grow:[{grow:[``,W,Y,q]}],shrink:[{shrink:[``,W,Y,q]}],order:[{order:[G,`first`,`last`,`none`,Y,q]}],"grid-cols":[{"grid-cols":ee()}],"col-start-end":[{col:te()}],"col-start":[{"col-start":E()}],"col-end":[{"col-end":E()}],"grid-rows":[{"grid-rows":ee()}],"row-start-end":[{row:te()}],"row-start":[{"row-start":E()}],"row-end":[{"row-end":E()}],"grid-flow":[{"grid-flow":[`row`,`col`,`dense`,`row-dense`,`col-dense`]}],"auto-cols":[{"auto-cols":ne()}],"auto-rows":[{"auto-rows":ne()}],gap:[{gap:w()}],"gap-x":[{"gap-x":w()}],"gap-y":[{"gap-y":w()}],"justify-content":[{justify:[...re(),`normal`]}],"justify-items":[{"justify-items":[...D(),`normal`]}],"justify-self":[{"justify-self":[`auto`,...D()]}],"align-content":[{content:[`normal`,...re()]}],"align-items":[{items:[...D(),{baseline:[``,`last`]}]}],"align-self":[{self:[`auto`,...D(),{baseline:[``,`last`]}]}],"place-content":[{"place-content":re()}],"place-items":[{"place-items":[...D(),`baseline`]}],"place-self":[{"place-self":[`auto`,...D()]}],p:[{p:w()}],px:[{px:w()}],py:[{py:w()}],ps:[{ps:w()}],pe:[{pe:w()}],pbs:[{pbs:w()}],pbe:[{pbe:w()}],pt:[{pt:w()}],pr:[{pr:w()}],pb:[{pb:w()}],pl:[{pl:w()}],m:[{m:O()}],mx:[{mx:O()}],my:[{my:O()}],ms:[{ms:O()}],me:[{me:O()}],mbs:[{mbs:O()}],mbe:[{mbe:O()}],mt:[{mt:O()}],mr:[{mr:O()}],mb:[{mb:O()}],ml:[{ml:O()}],"space-x":[{"space-x":w()}],"space-x-reverse":[`space-x-reverse`],"space-y":[{"space-y":w()}],"space-y-reverse":[`space-y-reverse`],size:[{size:k()}],"inline-size":[{inline:[`auto`,...A()]}],"min-inline-size":[{"min-inline":[`auto`,...A()]}],"max-inline-size":[{"max-inline":[`none`,...A()]}],"block-size":[{block:[`auto`,...ie()]}],"min-block-size":[{"min-block":[`auto`,...ie()]}],"max-block-size":[{"max-block":[`none`,...ie()]}],w:[{w:[s,`screen`,...k()]}],"min-w":[{"min-w":[s,`screen`,`none`,...k()]}],"max-w":[{"max-w":[s,`screen`,`none`,`prose`,{screen:[o]},...k()]}],h:[{h:[`screen`,`lh`,...k()]}],"min-h":[{"min-h":[`screen`,`lh`,`none`,...k()]}],"max-h":[{"max-h":[`screen`,`lh`,...k()]}],"font-size":[{text:[`base`,n,ln,J]}],"font-smoothing":[`antialiased`,`subpixel-antialiased`],"font-style":[`italic`,`not-italic`],"font-weight":[{font:[r,hn,rn]}],"font-stretch":[{"font-stretch":[`ultra-condensed`,`extra-condensed`,`condensed`,`semi-condensed`,`normal`,`semi-expanded`,`expanded`,`extra-expanded`,`ultra-expanded`,qt,q]}],"font-family":[{font:[un,an,t]}],"font-features":[{"font-features":[q]}],"fvn-normal":[`normal-nums`],"fvn-ordinal":[`ordinal`],"fvn-slashed-zero":[`slashed-zero`],"fvn-figure":[`lining-nums`,`oldstyle-nums`],"fvn-spacing":[`proportional-nums`,`tabular-nums`],"fvn-fraction":[`diagonal-fractions`,`stacked-fractions`],tracking:[{tracking:[i,Y,q]}],"line-clamp":[{"line-clamp":[W,`none`,Y,nn]}],leading:[{leading:[a,...w()]}],"list-image":[{"list-image":[`none`,Y,q]}],"list-style-position":[{list:[`inside`,`outside`]}],"list-style-type":[{list:[`disc`,`decimal`,`none`,Y,q]}],"text-alignment":[{text:[`left`,`center`,`right`,`justify`,`start`,`end`]}],"placeholder-color":[{placeholder:j()}],"text-color":[{text:j()}],"text-decoration":[`underline`,`overline`,`line-through`,`no-underline`],"text-decoration-style":[{decoration:[...F(),`wavy`]}],"text-decoration-thickness":[{decoration:[W,`from-font`,`auto`,Y,J]}],"text-decoration-color":[{decoration:j()}],"underline-offset":[{"underline-offset":[W,`auto`,Y,q]}],"text-transform":[`uppercase`,`lowercase`,`capitalize`,`normal-case`],"text-overflow":[`truncate`,`text-ellipsis`,`text-clip`],"text-wrap":[{text:[`wrap`,`nowrap`,`balance`,`pretty`]}],indent:[{indent:w()}],"tab-size":[{tab:[G,Y,q]}],"vertical-align":[{align:[`baseline`,`top`,`middle`,`bottom`,`text-top`,`text-bottom`,`sub`,`super`,Y,q]}],whitespace:[{whitespace:[`normal`,`nowrap`,`pre`,`pre-line`,`pre-wrap`,`break-spaces`]}],break:[{break:[`normal`,`words`,`all`,`keep`]}],wrap:[{wrap:[`break-word`,`anywhere`,`normal`]}],hyphens:[{hyphens:[`none`,`manual`,`auto`]}],content:[{content:[`none`,Y,q]}],"bg-attachment":[{bg:[`fixed`,`local`,`scroll`]}],"bg-clip":[{"bg-clip":[`border`,`padding`,`content`,`text`]}],"bg-origin":[{"bg-origin":[`border`,`padding`,`content`]}],"bg-position":[{bg:ae()}],"bg-repeat":[{bg:oe()}],"bg-size":[{bg:se()}],"bg-image":[{bg:[`none`,{linear:[{to:[`t`,`tr`,`r`,`br`,`b`,`bl`,`l`,`tl`]},G,Y,q],radial:[``,Y,q],conic:[G,Y,q]},pn,sn]}],"bg-color":[{bg:j()}],"gradient-from-pos":[{from:M()}],"gradient-via-pos":[{via:M()}],"gradient-to-pos":[{to:M()}],"gradient-from":[{from:j()}],"gradient-via":[{via:j()}],"gradient-to":[{to:j()}],rounded:[{rounded:N()}],"rounded-s":[{"rounded-s":N()}],"rounded-e":[{"rounded-e":N()}],"rounded-t":[{"rounded-t":N()}],"rounded-r":[{"rounded-r":N()}],"rounded-b":[{"rounded-b":N()}],"rounded-l":[{"rounded-l":N()}],"rounded-ss":[{"rounded-ss":N()}],"rounded-se":[{"rounded-se":N()}],"rounded-ee":[{"rounded-ee":N()}],"rounded-es":[{"rounded-es":N()}],"rounded-tl":[{"rounded-tl":N()}],"rounded-tr":[{"rounded-tr":N()}],"rounded-br":[{"rounded-br":N()}],"rounded-bl":[{"rounded-bl":N()}],"border-w":[{border:P()}],"border-w-x":[{"border-x":P()}],"border-w-y":[{"border-y":P()}],"border-w-s":[{"border-s":P()}],"border-w-e":[{"border-e":P()}],"border-w-bs":[{"border-bs":P()}],"border-w-be":[{"border-be":P()}],"border-w-t":[{"border-t":P()}],"border-w-r":[{"border-r":P()}],"border-w-b":[{"border-b":P()}],"border-w-l":[{"border-l":P()}],"divide-x":[{"divide-x":P()}],"divide-x-reverse":[`divide-x-reverse`],"divide-y":[{"divide-y":P()}],"divide-y-reverse":[`divide-y-reverse`],"border-style":[{border:[...F(),`hidden`,`none`]}],"divide-style":[{divide:[...F(),`hidden`,`none`]}],"border-color":[{border:j()}],"border-color-x":[{"border-x":j()}],"border-color-y":[{"border-y":j()}],"border-color-s":[{"border-s":j()}],"border-color-e":[{"border-e":j()}],"border-color-bs":[{"border-bs":j()}],"border-color-be":[{"border-be":j()}],"border-color-t":[{"border-t":j()}],"border-color-r":[{"border-r":j()}],"border-color-b":[{"border-b":j()}],"border-color-l":[{"border-l":j()}],"divide-color":[{divide:j()}],"outline-style":[{outline:[...F(),`none`,`hidden`]}],"outline-offset":[{"outline-offset":[W,Y,q]}],"outline-w":[{outline:[``,W,ln,J]}],"outline-color":[{outline:j()}],shadow:[{shadow:[``,`none`,u,mn,cn]}],"shadow-color":[{shadow:j()}],"inset-shadow":[{"inset-shadow":[`none`,d,mn,cn]}],"inset-shadow-color":[{"inset-shadow":j()}],"ring-w":[{ring:P()}],"ring-w-inset":[`ring-inset`],"ring-color":[{ring:j()}],"ring-offset-w":[{"ring-offset":[W,J]}],"ring-offset-color":[{"ring-offset":j()}],"inset-ring-w":[{"inset-ring":P()}],"inset-ring-color":[{"inset-ring":j()}],"text-shadow":[{"text-shadow":[`none`,f,mn,cn]}],"text-shadow-color":[{"text-shadow":j()}],opacity:[{opacity:[W,Y,q]}],"mix-blend":[{"mix-blend":[...ce(),`plus-darker`,`plus-lighter`]}],"bg-blend":[{"bg-blend":ce()}],"mask-clip":[{"mask-clip":[`border`,`padding`,`content`,`fill`,`stroke`,`view`]},`mask-no-clip`],"mask-composite":[{mask:[`add`,`subtract`,`intersect`,`exclude`]}],"mask-image-linear-pos":[{"mask-linear":[W]}],"mask-image-linear-from-pos":[{"mask-linear-from":I()}],"mask-image-linear-to-pos":[{"mask-linear-to":I()}],"mask-image-linear-from-color":[{"mask-linear-from":j()}],"mask-image-linear-to-color":[{"mask-linear-to":j()}],"mask-image-t-from-pos":[{"mask-t-from":I()}],"mask-image-t-to-pos":[{"mask-t-to":I()}],"mask-image-t-from-color":[{"mask-t-from":j()}],"mask-image-t-to-color":[{"mask-t-to":j()}],"mask-image-r-from-pos":[{"mask-r-from":I()}],"mask-image-r-to-pos":[{"mask-r-to":I()}],"mask-image-r-from-color":[{"mask-r-from":j()}],"mask-image-r-to-color":[{"mask-r-to":j()}],"mask-image-b-from-pos":[{"mask-b-from":I()}],"mask-image-b-to-pos":[{"mask-b-to":I()}],"mask-image-b-from-color":[{"mask-b-from":j()}],"mask-image-b-to-color":[{"mask-b-to":j()}],"mask-image-l-from-pos":[{"mask-l-from":I()}],"mask-image-l-to-pos":[{"mask-l-to":I()}],"mask-image-l-from-color":[{"mask-l-from":j()}],"mask-image-l-to-color":[{"mask-l-to":j()}],"mask-image-x-from-pos":[{"mask-x-from":I()}],"mask-image-x-to-pos":[{"mask-x-to":I()}],"mask-image-x-from-color":[{"mask-x-from":j()}],"mask-image-x-to-color":[{"mask-x-to":j()}],"mask-image-y-from-pos":[{"mask-y-from":I()}],"mask-image-y-to-pos":[{"mask-y-to":I()}],"mask-image-y-from-color":[{"mask-y-from":j()}],"mask-image-y-to-color":[{"mask-y-to":j()}],"mask-image-radial":[{"mask-radial":[Y,q]}],"mask-image-radial-from-pos":[{"mask-radial-from":I()}],"mask-image-radial-to-pos":[{"mask-radial-to":I()}],"mask-image-radial-from-color":[{"mask-radial-from":j()}],"mask-image-radial-to-color":[{"mask-radial-to":j()}],"mask-image-radial-shape":[{"mask-radial":[`circle`,`ellipse`]}],"mask-image-radial-size":[{"mask-radial":[{closest:[`side`,`corner`],farthest:[`side`,`corner`]}]}],"mask-image-radial-pos":[{"mask-radial-at":b()}],"mask-image-conic-pos":[{"mask-conic":[W]}],"mask-image-conic-from-pos":[{"mask-conic-from":I()}],"mask-image-conic-to-pos":[{"mask-conic-to":I()}],"mask-image-conic-from-color":[{"mask-conic-from":j()}],"mask-image-conic-to-color":[{"mask-conic-to":j()}],"mask-mode":[{mask:[`alpha`,`luminance`,`match`]}],"mask-origin":[{"mask-origin":[`border`,`padding`,`content`,`fill`,`stroke`,`view`]}],"mask-position":[{mask:ae()}],"mask-repeat":[{mask:oe()}],"mask-size":[{mask:se()}],"mask-type":[{"mask-type":[`alpha`,`luminance`]}],"mask-image":[{mask:[`none`,Y,q]}],filter:[{filter:[``,`none`,Y,q]}],blur:[{blur:le()}],brightness:[{brightness:[W,Y,q]}],contrast:[{contrast:[W,Y,q]}],"drop-shadow":[{"drop-shadow":[``,`none`,p,mn,cn]}],"drop-shadow-color":[{"drop-shadow":j()}],grayscale:[{grayscale:[``,W,Y,q]}],"hue-rotate":[{"hue-rotate":[W,Y,q]}],invert:[{invert:[``,W,Y,q]}],saturate:[{saturate:[W,Y,q]}],sepia:[{sepia:[``,W,Y,q]}],"backdrop-filter":[{"backdrop-filter":[``,`none`,Y,q]}],"backdrop-blur":[{"backdrop-blur":le()}],"backdrop-brightness":[{"backdrop-brightness":[W,Y,q]}],"backdrop-contrast":[{"backdrop-contrast":[W,Y,q]}],"backdrop-grayscale":[{"backdrop-grayscale":[``,W,Y,q]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[W,Y,q]}],"backdrop-invert":[{"backdrop-invert":[``,W,Y,q]}],"backdrop-opacity":[{"backdrop-opacity":[W,Y,q]}],"backdrop-saturate":[{"backdrop-saturate":[W,Y,q]}],"backdrop-sepia":[{"backdrop-sepia":[``,W,Y,q]}],"border-collapse":[{border:[`collapse`,`separate`]}],"border-spacing":[{"border-spacing":w()}],"border-spacing-x":[{"border-spacing-x":w()}],"border-spacing-y":[{"border-spacing-y":w()}],"table-layout":[{table:[`auto`,`fixed`]}],caption:[{caption:[`top`,`bottom`]}],transition:[{transition:[``,`all`,`colors`,`opacity`,`shadow`,`transform`,`none`,Y,q]}],"transition-behavior":[{transition:[`normal`,`discrete`]}],duration:[{duration:[W,`initial`,Y,q]}],ease:[{ease:[`linear`,`initial`,_,Y,q]}],delay:[{delay:[W,Y,q]}],animate:[{animate:[`none`,v,Y,q]}],backface:[{backface:[`hidden`,`visible`]}],perspective:[{perspective:[h,Y,q]}],"perspective-origin":[{"perspective-origin":x()}],rotate:[{rotate:ue()}],"rotate-x":[{"rotate-x":ue()}],"rotate-y":[{"rotate-y":ue()}],"rotate-z":[{"rotate-z":ue()}],scale:[{scale:L()}],"scale-x":[{"scale-x":L()}],"scale-y":[{"scale-y":L()}],"scale-z":[{"scale-z":L()}],"scale-3d":[`scale-3d`],skew:[{skew:R()}],"skew-x":[{"skew-x":R()}],"skew-y":[{"skew-y":R()}],transform:[{transform:[Y,q,``,`none`,`gpu`,`cpu`]}],"transform-origin":[{origin:x()}],"transform-style":[{transform:[`3d`,`flat`]}],translate:[{translate:z()}],"translate-x":[{"translate-x":z()}],"translate-y":[{"translate-y":z()}],"translate-z":[{"translate-z":z()}],"translate-none":[`translate-none`],zoom:[{zoom:[G,Y,q]}],accent:[{accent:j()}],appearance:[{appearance:[`none`,`auto`]}],"caret-color":[{caret:j()}],"color-scheme":[{scheme:[`normal`,`dark`,`light`,`light-dark`,`only-dark`,`only-light`]}],cursor:[{cursor:[`auto`,`default`,`pointer`,`wait`,`text`,`move`,`help`,`not-allowed`,`none`,`context-menu`,`progress`,`cell`,`crosshair`,`vertical-text`,`alias`,`copy`,`no-drop`,`grab`,`grabbing`,`all-scroll`,`col-resize`,`row-resize`,`n-resize`,`e-resize`,`s-resize`,`w-resize`,`ne-resize`,`nw-resize`,`se-resize`,`sw-resize`,`ew-resize`,`ns-resize`,`nesw-resize`,`nwse-resize`,`zoom-in`,`zoom-out`,Y,q]}],"field-sizing":[{"field-sizing":[`fixed`,`content`]}],"pointer-events":[{"pointer-events":[`auto`,`none`]}],resize:[{resize:[`none`,``,`y`,`x`]}],"scroll-behavior":[{scroll:[`auto`,`smooth`]}],"scrollbar-thumb-color":[{"scrollbar-thumb":j()}],"scrollbar-track-color":[{"scrollbar-track":j()}],"scrollbar-gutter":[{"scrollbar-gutter":[`auto`,`stable`,`both`]}],"scrollbar-w":[{scrollbar:[`auto`,`thin`,`none`]}],"scroll-m":[{"scroll-m":w()}],"scroll-mx":[{"scroll-mx":w()}],"scroll-my":[{"scroll-my":w()}],"scroll-ms":[{"scroll-ms":w()}],"scroll-me":[{"scroll-me":w()}],"scroll-mbs":[{"scroll-mbs":w()}],"scroll-mbe":[{"scroll-mbe":w()}],"scroll-mt":[{"scroll-mt":w()}],"scroll-mr":[{"scroll-mr":w()}],"scroll-mb":[{"scroll-mb":w()}],"scroll-ml":[{"scroll-ml":w()}],"scroll-p":[{"scroll-p":w()}],"scroll-px":[{"scroll-px":w()}],"scroll-py":[{"scroll-py":w()}],"scroll-ps":[{"scroll-ps":w()}],"scroll-pe":[{"scroll-pe":w()}],"scroll-pbs":[{"scroll-pbs":w()}],"scroll-pbe":[{"scroll-pbe":w()}],"scroll-pt":[{"scroll-pt":w()}],"scroll-pr":[{"scroll-pr":w()}],"scroll-pb":[{"scroll-pb":w()}],"scroll-pl":[{"scroll-pl":w()}],"snap-align":[{snap:[`start`,`end`,`center`,`align-none`]}],"snap-stop":[{snap:[`normal`,`always`]}],"snap-type":[{snap:[`none`,`x`,`y`,`both`]}],"snap-strictness":[{snap:[`mandatory`,`proximity`]}],touch:[{touch:[`auto`,`none`,`manipulation`]}],"touch-x":[{"touch-pan":[`x`,`left`,`right`]}],"touch-y":[{"touch-pan":[`y`,`up`,`down`]}],"touch-pz":[`touch-pinch-zoom`],select:[{select:[`none`,`text`,`all`,`auto`]}],"will-change":[{"will-change":[`auto`,`scroll`,`contents`,`transform`,Y,q]}],fill:[{fill:[`none`,...j()]}],"stroke-w":[{stroke:[W,ln,J,nn]}],stroke:[{stroke:[`none`,...j()]}],"forced-color-adjust":[{"forced-color-adjust":[`auto`,`none`]}]},conflictingClassGroups:{"container-named":[`container-type`],overflow:[`overflow-x`,`overflow-y`],overscroll:[`overscroll-x`,`overscroll-y`],inset:[`inset-x`,`inset-y`,`inset-bs`,`inset-be`,`start`,`end`,`top`,`right`,`bottom`,`left`],"inset-x":[`right`,`left`],"inset-y":[`top`,`bottom`],flex:[`basis`,`grow`,`shrink`],gap:[`gap-x`,`gap-y`],p:[`px`,`py`,`ps`,`pe`,`pbs`,`pbe`,`pt`,`pr`,`pb`,`pl`],px:[`pr`,`pl`],py:[`pt`,`pb`],m:[`mx`,`my`,`ms`,`me`,`mbs`,`mbe`,`mt`,`mr`,`mb`,`ml`],mx:[`mr`,`ml`],my:[`mt`,`mb`],size:[`w`,`h`],"font-size":[`leading`],"fvn-normal":[`fvn-ordinal`,`fvn-slashed-zero`,`fvn-figure`,`fvn-spacing`,`fvn-fraction`],"fvn-ordinal":[`fvn-normal`],"fvn-slashed-zero":[`fvn-normal`],"fvn-figure":[`fvn-normal`],"fvn-spacing":[`fvn-normal`],"fvn-fraction":[`fvn-normal`],"line-clamp":[`display`,`overflow`],rounded:[`rounded-s`,`rounded-e`,`rounded-t`,`rounded-r`,`rounded-b`,`rounded-l`,`rounded-ss`,`rounded-se`,`rounded-ee`,`rounded-es`,`rounded-tl`,`rounded-tr`,`rounded-br`,`rounded-bl`],"rounded-s":[`rounded-ss`,`rounded-es`],"rounded-e":[`rounded-se`,`rounded-ee`],"rounded-t":[`rounded-tl`,`rounded-tr`],"rounded-r":[`rounded-tr`,`rounded-br`],"rounded-b":[`rounded-br`,`rounded-bl`],"rounded-l":[`rounded-tl`,`rounded-bl`],"border-spacing":[`border-spacing-x`,`border-spacing-y`],"border-w":[`border-w-x`,`border-w-y`,`border-w-s`,`border-w-e`,`border-w-bs`,`border-w-be`,`border-w-t`,`border-w-r`,`border-w-b`,`border-w-l`],"border-w-x":[`border-w-r`,`border-w-l`],"border-w-y":[`border-w-t`,`border-w-b`],"border-color":[`border-color-x`,`border-color-y`,`border-color-s`,`border-color-e`,`border-color-bs`,`border-color-be`,`border-color-t`,`border-color-r`,`border-color-b`,`border-color-l`],"border-color-x":[`border-color-r`,`border-color-l`],"border-color-y":[`border-color-t`,`border-color-b`],translate:[`translate-x`,`translate-y`,`translate-none`],"translate-none":[`translate`,`translate-x`,`translate-y`,`translate-z`],"scroll-m":[`scroll-mx`,`scroll-my`,`scroll-ms`,`scroll-me`,`scroll-mbs`,`scroll-mbe`,`scroll-mt`,`scroll-mr`,`scroll-mb`,`scroll-ml`],"scroll-mx":[`scroll-mr`,`scroll-ml`],"scroll-my":[`scroll-mt`,`scroll-mb`],"scroll-p":[`scroll-px`,`scroll-py`,`scroll-ps`,`scroll-pe`,`scroll-pbs`,`scroll-pbe`,`scroll-pt`,`scroll-pr`,`scroll-pb`,`scroll-pl`],"scroll-px":[`scroll-pr`,`scroll-pl`],"scroll-py":[`scroll-pt`,`scroll-pb`],touch:[`touch-x`,`touch-y`,`touch-pz`],"touch-x":[`touch`],"touch-y":[`touch`],"touch-pz":[`touch`]},conflictingClassGroupModifiers:{"font-size":[`leading`]},postfixLookupClassGroups:[`container-type`],orderSensitiveModifiers:[`*`,`**`,`after`,`backdrop`,`before`,`details-content`,`file`,`first-letter`,`first-line`,`marker`,`placeholder`,`selection`]}});function Tn(...e){return wn(nt(e))}var En=n(),Dn=at(`group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,{variants:{variant:{default:`bg-primary text-primary-foreground hover:bg-primary/80`,outline:`border-border bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground`,secondary:`bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground`,ghost:`hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50`,destructive:`bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40`,link:`text-primary underline-offset-4 hover:underline`},size:{default:`h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5`,xs:`h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3`,sm:`h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2`,lg:`h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3`,icon:`size-9`,"icon-xs":`size-6 [&_svg:not([class*='size-'])]:size-3`,"icon-sm":`size-8`,"icon-lg":`size-10`}},defaultVariants:{variant:`default`,size:`default`}});function On({className:e,variant:t=`default`,size:n=`default`,...r}){return(0,En.jsx)(et,{"data-slot":`button`,className:Tn(Dn({variant:t,size:n,className:e})),...r})}var kn=`dt_gate_active`,An=`1`;function jn(){try{return typeof sessionStorage>`u`?!1:sessionStorage.getItem(kn)===An}catch{return!1}}function Mn(){try{if(typeof sessionStorage>`u`)return;sessionStorage.setItem(kn,An)}catch{}}var Nn=!0,Pn=jn(),Fn=new Set;function In(){for(let e of Fn)e()}var Q={isAuthed:()=>Nn,isGateActive:()=>Pn,markAuthenticated(){Nn=!0,Pn=!0,Mn(),In()},markUnauthenticated(){Nn=!1,Pn=!0,Mn(),In()},markGateActive(){Pn||(Pn=!0,Mn(),In())},subscribe(e){return Fn.add(e),()=>{Fn.delete(e)}}};function Ln(){return(0,r.useSyncExternalStore)(Q.subscribe,Q.isAuthed,Q.isAuthed)}function Rn(){return(0,r.useSyncExternalStore)(Q.subscribe,Q.isGateActive,Q.isGateActive)}var zn=`X-Instance-Gated`,Bn=`1`,Vn=class extends Error{status;body;code;constructor(e,t,n,r){super(t),this.name=`ApiError`,this.status=e,this.body=n,this.code=r}};async function $(e,t){let n=(t?.method??`GET`)===`GET`?t?.headers:{...t?.headers,"X-Requested-With":`drop-tracker`},r=await fetch(e,{...t,headers:n});if(r.headers.get(zn)===Bn&&Q.markGateActive(),r.status===401)throw Q.markUnauthenticated(),new Vn(401,`unauthenticated`);if(r.status!==204){if(!r.ok){let e=r.statusText,t,n;try{let i=await r.json();t=i,i.error&&(e=i.error),typeof i.code==`string`&&(n=i.code)}catch{}throw new Vn(r.status,e,t,n)}return await r.json()}}async function Hn(e){let t=new URLSearchParams;e?.artistId!=null&&t.set(`artist_id`,String(e.artistId)),e?.eventType&&t.set(`event_type`,e.eventType),e?.cursor!=null&&t.set(`cursor`,e.cursor);let n=t.toString();return $(`/events${n?`?${n}`:``}`)}async function Un(){return $(`/watchlist`)}async function Wn(e){return $(`/watchlist`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({mbid:e.mbid,name:e.name,deezer_id:e.deezerId,disambiguation:e.disambiguation,image_url:e.imageUrl,note:e.note})})}async function Gn(e,t){return $(`/watchlist/${e}/note`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({note:t})})}async function Kn(e,t){return $(`/watchlist/${e}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({release_types:t.releaseTypes,muted_event_types:t.mutedEventTypes})})}async function qn(e){await $(`/watchlist/${e}`,{method:`DELETE`})}async function Jn(e,t){await $(`/watchlist/${e}/tags/${t}`,{method:`DELETE`})}async function Yn(e,t){return $(`/watchlist/${e}/tags`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:t})})}async function Xn(){return $(`/tags`)}async function Zn(e){return $(`/tags/${e}`,{method:`DELETE`})}async function Qn(e,t){try{return{kind:`renamed`,tag:await $(`/tags/${e}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:t})})}}catch(e){if(e instanceof Vn&&e.status===409&&e.body){let t=e.body;if(t.target&&typeof t.carrier_count_after_merge==`number`)return{kind:`collision`,target:t.target,carrierCountAfterMerge:t.carrier_count_after_merge}}throw e}}async function $n(e,t){return $(`/tags/${e}/merge`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({into:t})})}async function er(e,t){return $(`/search?${new URLSearchParams({q:e}).toString()}`,{signal:t})}async function tr(e){await $(`/session`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({passphrase:e})})}async function nr(){await $(`/session`,{method:`DELETE`})}async function rr(){return $(`/status`)}async function ir(){return $(`/settings/notifications`)}async function ar(e){return $(`/settings/notifications`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({digest_enabled:e.digestEnabled,digest_cadence:e.digestCadence})})}export{k as $,Ve as A,M as B,On as C,We as D,Je as E,Te as F,g as G,D as H,de as I,j as J,ie as K,L,je as M,Ae as N,V as O,ke as P,x as Q,I as R,Rn as S,at as T,v as U,A as V,se as W,re as X,_ as Y,b as Z,ar as _,nr as a,O as at,Q as b,ir as c,Xn as d,y as et,Un as f,er as g,Qn as h,tr as i,T as it,Pe as j,Ue as k,rr as l,qn as m,Wn as n,S as nt,Zn as o,m as ot,$n as p,oe as q,Yn as r,w as rt,Jn as s,p as st,Vn as t,C as tt,Hn as u,Gn as v,Tn as w,Ln as x,Kn as y,P as z}; \ No newline at end of file +import{N as e,j as t,t as n}from"./jsx-runtime-D0QO-s14.js";var r=e(t(),1),i=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),a=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),o=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(e,t,n)=>n?n.toUpperCase():t.toLowerCase()),s=e=>{let t=o(e);return t.charAt(0).toUpperCase()+t.slice(1)},c={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},l=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0;return!1},u=(0,r.createContext)({}),d=()=>(0,r.useContext)(u),f=(0,r.forwardRef)(({color:e,size:t,strokeWidth:n,absoluteStrokeWidth:a,className:o=``,children:s,iconNode:u,...f},p)=>{let{size:m=24,strokeWidth:h=2,absoluteStrokeWidth:g=!1,color:_=`currentColor`,className:v=``}=d()??{},y=a??g?Number(n??h)*24/Number(t??m):n??h;return(0,r.createElement)(`svg`,{ref:p,...c,width:t??m??c.width,height:t??m??c.height,stroke:e??_,strokeWidth:y,className:i(`lucide`,v,o),...!s&&!l(f)&&{"aria-hidden":`true`},...f},[...u.map(([e,t])=>(0,r.createElement)(e,t)),...Array.isArray(s)?s:[s]])}),p=(e,t)=>{let n=(0,r.forwardRef)(({className:n,...o},c)=>(0,r.createElement)(f,{ref:c,iconNode:t,className:i(`lucide-${a(s(e))}`,`lucide-${e}`,n),...o}));return n.displayName=s(e),n},m=p(`loader-circle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]);function h(){return typeof window<`u`}function g(e){return y(e)?(e.nodeName||``).toLowerCase():`#document`}function _(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function v(e){return((y(e)?e.ownerDocument:e.document)||window.document)?.documentElement}function y(e){return h()?e instanceof Node||e instanceof _(e).Node:!1}function b(e){return h()?e instanceof Element||e instanceof _(e).Element:!1}function x(e){return h()?e instanceof HTMLElement||e instanceof _(e).HTMLElement:!1}function S(e){return!h()||typeof ShadowRoot>`u`?!1:e instanceof ShadowRoot||e instanceof _(e).ShadowRoot}function C(e){let{overflow:t,overflowX:n,overflowY:r,display:i}=A(e);return/auto|scroll|overlay|hidden|clip/.test(t+r+n)&&i!==`inline`&&i!==`contents`}function w(e){return/^(table|td|th)$/.test(g(e))}function T(e){try{if(e.matches(`:popover-open`))return!0}catch{}try{return e.matches(`:modal`)}catch{return!1}}var ee=/transform|translate|scale|rotate|perspective|filter/,te=/paint|layout|strict|content/,E=e=>!!e&&e!==`none`,ne;function re(e){let t=b(e)?A(e):e;return E(t.transform)||E(t.translate)||E(t.scale)||E(t.rotate)||E(t.perspective)||!O()&&(E(t.backdropFilter)||E(t.filter))||ee.test(t.willChange||``)||te.test(t.contain||``)}function D(e){let t=j(e);for(;x(t)&&!k(t);){if(re(t))return t;if(T(t))return null;t=j(t)}return null}function O(){return ne??=typeof CSS<`u`&&CSS.supports&&CSS.supports(`-webkit-backdrop-filter`,`none`),ne}function k(e){return/^(html|body|#document)$/.test(g(e))}function A(e){return _(e).getComputedStyle(e)}function ie(e){return b(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function j(e){if(g(e)===`html`)return e;let t=e.assignedSlot||e.parentNode||S(e)&&e.host||v(e);return S(t)?t.host:t}function ae(e){let t=j(e);return k(t)?(e.ownerDocument||e).body:x(t)&&C(t)?t:ae(t)}function oe(e,t,n){t===void 0&&(t=[]),n===void 0&&(n=!0);let r=ae(e),i=r===e.ownerDocument?.body,a=_(r);if(i){let e=se(a);return t.concat(a,a.visualViewport||[],C(r)?r:[],e&&n?oe(e):[])}else return t.concat(r,oe(r,[],n))}function se(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}var M={...r},N={};function P(e,t){let n=r.useRef(N);return n.current===N&&(n.current=e(t)),n}var F=M.useInsertionEffect,ce=F&&F!==M.useLayoutEffect?F:e=>e();function I(e){let t=P(le).current;return t.next=e,ce(t.effect),t.trampoline}function le(){let e={next:void 0,callback:ue,trampoline:(...t)=>e.callback?.(...t),effect:()=>{e.callback=e.next}};return e}function ue(){}var L=typeof document<`u`?r.useLayoutEffect:()=>{};function R(e,t){if(e&&!t)return e;if(!e&&t)return t;if(e||t)return{...e,...t}}var z={};function de(e,t,n,r,i){if(!n&&!r&&!i&&!e)return pe(t);let a=pe(e);return t&&(a=B(a,t)),n&&(a=B(a,n)),r&&(a=B(a,r)),i&&(a=B(a,i)),a}function fe(e){if(e.length===0)return z;if(e.length===1)return pe(e[0]);let t=pe(e[0]);for(let n=1;n=65&&i<=90&&(typeof t==`function`||t===void 0)}function _e(e){return typeof e==`function`}function ve(e,t){return _e(e)?e(t):e??z}function ye(e,t){return t?e?(...n)=>{let r=n[0];if(Ce(r)){let i=r;xe(i);let a=t(...n);return i.baseUIHandlerPrevented||e?.(...n),a}let i=t(...n);return e?.(...n),i}:be(t):e}function be(e){return e&&((...t)=>{let n=t[0];return Ce(n)&&xe(n),e(...t)})}function xe(e){return e.preventBaseUIHandler=()=>{e.baseUIHandlerPrevented=!0},e}function Se(e,t){return t?e?t+` `+e:t:e}function Ce(e){return typeof e==`object`&&!!e&&`nativeEvent`in e}function we(e,t){return function(n,...r){let i=new URL(e);return i.searchParams.set(`code`,n.toString()),r.forEach(e=>i.searchParams.append(`args[]`,e)),`${t} error #${n}; visit ${i} for the full message.`}}var Te=we(`https://base-ui.com/production-error`,`Base UI`),Ee=r.createContext(void 0);function De(e=!1){let t=r.useContext(Ee);if(t===void 0&&!e)throw Error(Te(16));return t}function Oe(e){let{focusableWhenDisabled:t,disabled:n,composite:i=!1,tabIndex:a=0,isNativeButton:o}=e,s=i&&t!==!1,c=i&&t===!1;return{props:r.useMemo(()=>{let e={onKeyDown(e){n&&t&&e.key!==`Tab`&&e.preventDefault()}};return i||(e.tabIndex=a,!o&&n&&(e.tabIndex=t?a:-1)),(o&&(t||s)||!o&&n)&&(e[`aria-disabled`]=n),o&&(!t||c)&&(e.disabled=n),e},[i,n,t,s,c,o,a])}}function ke(e){return e?.ownerDocument||document}function Ae(e,t,{detail:n=0}={}){e.dispatchEvent(new(_(e)).PointerEvent(`click`,{bubbles:!0,cancelable:!0,composed:!0,detail:n,shiftKey:t.shiftKey,ctrlKey:t.ctrlKey,altKey:t.altKey,metaKey:t.metaKey}))}function je(e={}){let{disabled:t=!1,focusableWhenDisabled:n,tabIndex:i=0,native:a=!0,composite:o}=e,s=r.useRef(null),c=De(!0),l=o??c!==void 0,{props:u}=Oe({focusableWhenDisabled:n,disabled:t,composite:l,tabIndex:i,isNativeButton:a}),d=r.useCallback(()=>{let e=s.current;Me(e)&&l&&t&&u.disabled===void 0&&e.disabled&&(e.disabled=!1)},[t,u.disabled,l]);return L(d,[d]),{getButtonProps:r.useCallback((e={})=>{let{onClick:n,onMouseDown:r,onKeyUp:i,onKeyDown:o,onPointerDown:s,...c}=e;return de({onClick(e){if(t){e.preventDefault();return}n?.(e)},onMouseDown(e){t||r?.(e)},onKeyDown(e){if(t||(xe(e),o?.(e),e.baseUIHandlerPrevented))return;let n=e.target===e.currentTarget,r=e.currentTarget,i=Me(r),s=!a&&Ne(r),c=n&&(a?i:!s),u=e.key===`Enter`,d=e.key===` `,f=r.getAttribute(`role`),p=f?.startsWith(`menuitem`)||f===`option`||f===`gridcell`;if(n&&l&&d){if(e.defaultPrevented&&p)return;e.preventDefault(),(!a||i)&&(e.preventBaseUIHandler(),Ae(r,e));return}if(!c||a||!d&&!u){n&&s&&d&&e.preventDefault();return}e.defaultPrevented||(e.preventDefault(),u&&(e.preventBaseUIHandler(),Ae(r,e)))},onKeyUp(e){if(!t){if(xe(e),i?.(e),e.target===e.currentTarget&&a&&l&&Me(e.currentTarget)&&e.key===` `){e.preventDefault();return}e.baseUIHandlerPrevented||e.target===e.currentTarget&&!a&&!l&&!e.defaultPrevented&&e.key===` `&&(e.preventBaseUIHandler(),Ae(e.currentTarget,e))}},onPointerDown(e){if(t){e.preventDefault();return}s?.(e)}},a?{type:`button`}:{role:`button`},u,c)},[t,u,l,a]),buttonRef:I(e=>{s.current=e,d()})}}function Me(e){return x(e)&&e.tagName===`BUTTON`}function Ne(e){return x(e)&&e.tagName===`A`&&!!e.href}function Pe(e,t,n,r){let i=P(Ie).current;return Le(i,e,t,n,r)&&ze(i,[e,t,n,r]),i.callback}function Fe(e){let t=P(Ie).current;return Re(t,e)&&ze(t,e),t.callback}function Ie(){return{callback:null,cleanup:null,refs:[]}}function Le(e,t,n,r,i){return e.refs[0]!==t||e.refs[1]!==n||e.refs[2]!==r||e.refs[3]!==i}function Re(e,t){return e.refs.length!==t.length||e.refs.some((e,n)=>e!==t[n])}function ze(e,t){if(e.refs=t,t.every(e=>e==null)){e.callback=null;return}e.callback=n=>{if(e.cleanup&&=(e.cleanup(),null),n!=null){let r=Array(t.length).fill(null);for(let e=0;e{for(let e=0;e=e}function He(e){if(!r.isValidElement(e))return null;let t=e,n=t.props;return(Ve(19)?n?.ref:t.ref)??null}function Ue(){}var We=Object.freeze([]),V=Object.freeze({});function Ge(e,t){let n={};for(let r in e){let i=e[r];if(t?.hasOwnProperty(r)){let e=t[r](i);e!=null&&Object.assign(n,e);continue}i===!0?n[`data-${r.toLowerCase()}`]=``:i&&(n[`data-${r.toLowerCase()}`]=i.toString())}return n}function Ke(e,t){return typeof e==`function`?e(t):e}function qe(e,t){return typeof e==`function`?e(t):e}function Je(e,t,n={}){let r=t.render,i=Ye(t,n);return n.enabled===!1?null:Qe(e,r,i,n.state??V)}function Ye(e,t={}){let{className:n,style:r,render:i}=e,{state:a=V,ref:o,props:s,stateAttributesMapping:c,enabled:l=!0}=t,u=l?Ke(n,a):void 0,d=l?qe(r,a):void 0,f=l?Ge(a,c):V,p=l&&s?Xe(s):void 0,m=l?R(f,p)??{}:V;return typeof document<`u`&&(l?Array.isArray(o)?m.ref=Fe([m.ref,He(i),...o]):m.ref=Pe(m.ref,He(i),o):Pe(null,null)),l?(u!==void 0&&(m.className=Se(m.className,u)),d!==void 0&&(m.style=R(m.style,d)),m):V}function Xe(e){return Array.isArray(e)?fe(e):de(void 0,e)}var Ze=Symbol.for(`react.lazy`);function Qe(e,t,n,i){if(t){if(typeof t==`function`)return t(n,i);let e=de(n,t.props);e.ref=n.ref;let a=t;return a?.$$typeof===Ze&&(a=r.Children.toArray(t)[0]),r.cloneElement(a,e)}if(e&&typeof e==`string`)return $e(e,n);throw Error(Te(8))}function $e(e,t){return e===`button`?(0,r.createElement)(`button`,{type:`button`,...t,key:t.key}):e===`img`?(0,r.createElement)(`img`,{alt:``,...t,key:t.key}):r.createElement(e,t)}var et=r.forwardRef(function(e,t){let{render:n,className:r,disabled:i=!1,focusableWhenDisabled:a=!1,nativeButton:o=!0,style:s,...c}=e,{getButtonProps:l,buttonRef:u}=je({disabled:i,focusableWhenDisabled:a,native:o});return Je(`button`,e,{state:{disabled:i},ref:[t,u],props:[c,l]})});function tt(e){var t,n,r=``;if(typeof e==`string`||typeof e==`number`)r+=e;else if(typeof e==`object`)if(Array.isArray(e)){var i=e.length;for(t=0;ttypeof e==`boolean`?`${e}`:e===0?`0`:e,it=nt,at=(e,t)=>n=>{if(t?.variants==null)return it(e,n?.class,n?.className);let{variants:r,defaultVariants:i}=t,a=Object.keys(r).map(e=>{let t=n?.[e],a=i?.[e];if(t===null)return null;let o=rt(t)||rt(a);return r[e][o]}),o=n&&Object.entries(n).reduce((e,t)=>{let[n,r]=t;return r===void 0||(e[n]=r),e},{});return it(e,a,t?.compoundVariants?.reduce((e,t)=>{let{class:n,className:r,...a}=t;return Object.entries(a).every(e=>{let[t,n]=e;return Array.isArray(n)?n.includes({...i,...o}[t]):{...i,...o}[t]===n})?[...e,n,r]:e},[]),n?.class,n?.className)},ot=(e,t)=>{let n=Array(e.length+t.length);for(let t=0;t({classGroupId:e,validator:t}),ct=(e=new Map,t=null,n)=>({nextPart:e,validators:t,classGroupId:n}),lt=`-`,ut=[],dt=`arbitrary..`,ft=e=>{let t=ht(e),{conflictingClassGroups:n,conflictingClassGroupModifiers:r}=e;return{getClassGroupId:e=>{if(e.startsWith(`[`)&&e.endsWith(`]`))return mt(e);let n=e.split(lt);return pt(n,+(n[0]===``&&n.length>1),t)},getConflictingClassGroupIds:(e,t)=>{if(t){let t=r[e],i=n[e];return t?i?ot(i,t):t:i||ut}return n[e]||ut}}},pt=(e,t,n)=>{if(e.length-t===0)return n.classGroupId;let r=e[t],i=n.nextPart.get(r);if(i){let n=pt(e,t+1,i);if(n)return n}let a=n.validators;if(a===null)return;let o=t===0?e.join(lt):e.slice(t).join(lt),s=a.length;for(let e=0;ee.slice(1,-1).indexOf(`:`)===-1?void 0:(()=>{let t=e.slice(1,-1),n=t.indexOf(`:`),r=t.slice(0,n);return r?dt+r:void 0})(),ht=e=>{let{theme:t,classGroups:n}=e;return gt(n,t)},gt=(e,t)=>{let n=ct();for(let r in e){let i=e[r];_t(i,n,r,t)}return n},_t=(e,t,n,r)=>{let i=e.length;for(let a=0;a{if(typeof e==`string`){yt(e,t,n);return}if(typeof e==`function`){bt(e,t,n,r);return}xt(e,t,n,r)},yt=(e,t,n)=>{let r=e===``?t:St(t,e);r.classGroupId=n},bt=(e,t,n,r)=>{if(Ct(e)){_t(e(r),t,n,r);return}t.validators===null&&(t.validators=[]),t.validators.push(st(n,e))},xt=(e,t,n,r)=>{let i=Object.entries(e),a=i.length;for(let e=0;e{let n=e,r=t.split(lt),i=r.length;for(let e=0;e`isThemeGetter`in e&&e.isThemeGetter===!0,wt=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let t=0,n=Object.create(null),r=Object.create(null),i=(i,a)=>{n[i]=a,t++,t>e&&(t=0,r=n,n=Object.create(null))};return{get(e){let t=n[e];if(t!==void 0)return t;if((t=r[e])!==void 0)return i(e,t),t},set(e,t){e in n?n[e]=t:i(e,t)}}},Tt=`!`,Et=`:`,Dt=[],Ot=(e,t,n,r,i)=>({modifiers:e,hasImportantModifier:t,baseClassName:n,maybePostfixModifierPosition:r,isExternal:i}),kt=e=>{let{prefix:t,experimentalParseClassName:n}=e,r=e=>{let t=[],n=0,r=0,i=0,a,o=e.length;for(let s=0;si?a-i:void 0;return Ot(t,l,c,u)};if(t){let e=t+Et,n=r;r=t=>t.startsWith(e)?n(t.slice(e.length)):Ot(Dt,!1,t,void 0,!0)}if(n){let e=r;r=t=>n({className:t,parseClassName:e})}return r},At=e=>{let t=new Map;return e.orderSensitiveModifiers.forEach((e,n)=>{t.set(e,1e6+n)}),e=>{let n=[],r=[];for(let i=0;i0&&(r.sort(),n.push(...r),r=[]),n.push(a)):r.push(a)}return r.length>0&&(r.sort(),n.push(...r)),n}},jt=e=>({cache:wt(e.cacheSize),parseClassName:kt(e),sortModifiers:At(e),postfixLookupClassGroupIds:Mt(e),...ft(e)}),Mt=e=>{let t=Object.create(null),n=e.postfixLookupClassGroups;if(n)for(let e=0;e{let{parseClassName:n,getClassGroupId:r,getConflictingClassGroupIds:i,sortModifiers:a,postfixLookupClassGroupIds:o}=t,s=[],c=e.trim().split(Nt),l=``;for(let e=c.length-1;e>=0;--e){let t=c[e],{isExternal:u,modifiers:d,hasImportantModifier:f,baseClassName:p,maybePostfixModifierPosition:m}=n(t);if(u){l=t+(l.length>0?` `+l:l);continue}let h=!!m,g;if(h){g=r(p.substring(0,m));let e=g&&o[g]?r(p):void 0;e&&e!==g&&(g=e,h=!1)}else g=r(p);if(!g){if(!h){l=t+(l.length>0?` `+l:l);continue}if(g=r(p),!g){l=t+(l.length>0?` `+l:l);continue}h=!1}let _=d.length===0?``:d.length===1?d[0]:a(d).join(`:`),v=f?_+Tt:_,y=v+g;if(s.indexOf(y)>-1)continue;s.push(y);let b=i(g,h);for(let e=0;e0?` `+l:l)}return l},Ft=(...e)=>{let t=0,n,r,i=``;for(;t{if(typeof e==`string`)return e;let t,n=``;for(let r=0;r{let n,r,i,a,o=o=>(n=jt(t.reduce((e,t)=>t(e),e())),r=n.cache.get,i=n.cache.set,a=s,s(o)),s=e=>{let t=r(e);if(t)return t;let a=Pt(e,n);return i(e,a),a};return a=o,(...e)=>a(Ft(...e))},Rt=[],H=e=>{let t=t=>t[e]||Rt;return t.isThemeGetter=!0,t},zt=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,Bt=/^\((?:(\w[\w-]*):)?(.+)\)$/i,Vt=/^\d+(?:\.\d+)?\/\d+(?:\.\d+)?$/,Ht=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,Ut=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,Wt=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,Gt=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,Kt=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,U=e=>Vt.test(e),W=e=>!!e&&!Number.isNaN(Number(e)),G=e=>!!e&&Number.isInteger(Number(e)),qt=e=>e.endsWith(`%`)&&W(e.slice(0,-1)),K=e=>Ht.test(e),Jt=()=>!0,Yt=e=>Ut.test(e)&&!Wt.test(e),Xt=()=>!1,Zt=e=>Gt.test(e),Qt=e=>Kt.test(e),$t=e=>!q(e)&&!Y(e),en=e=>e.startsWith(`@container`)&&(e[10]===`/`&&e[11]!==void 0||e[11]===`s`&&e[16]!==void 0&&e.startsWith(`-size/`,10)||e[11]===`n`&&e[18]!==void 0&&e.startsWith(`-normal/`,10)),tn=e=>X(e,vn,Xt),q=e=>zt.test(e),J=e=>X(e,yn,Yt),nn=e=>X(e,bn,W),rn=e=>X(e,Sn,Jt),an=e=>X(e,xn,Xt),on=e=>X(e,gn,Xt),sn=e=>X(e,_n,Qt),cn=e=>X(e,Cn,Zt),Y=e=>Bt.test(e),ln=e=>Z(e,yn),un=e=>Z(e,xn),dn=e=>Z(e,gn),fn=e=>Z(e,vn),pn=e=>Z(e,_n),mn=e=>Z(e,Cn,!0),hn=e=>Z(e,Sn,!0),X=(e,t,n)=>{let r=zt.exec(e);return r?r[1]?t(r[1]):n(r[2]):!1},Z=(e,t,n=!1)=>{let r=Bt.exec(e);return r?r[1]?t(r[1]):n:!1},gn=e=>e===`position`||e===`percentage`,_n=e=>e===`image`||e===`url`,vn=e=>e===`length`||e===`size`||e===`bg-size`,yn=e=>e===`length`,bn=e=>e===`number`,xn=e=>e===`family-name`,Sn=e=>e===`number`||e===`weight`,Cn=e=>e===`shadow`,wn=Lt(()=>{let e=H(`color`),t=H(`font`),n=H(`text`),r=H(`font-weight`),i=H(`tracking`),a=H(`leading`),o=H(`breakpoint`),s=H(`container`),c=H(`spacing`),l=H(`radius`),u=H(`shadow`),d=H(`inset-shadow`),f=H(`text-shadow`),p=H(`drop-shadow`),m=H(`blur`),h=H(`perspective`),g=H(`aspect`),_=H(`ease`),v=H(`animate`),y=()=>[`auto`,`avoid`,`all`,`avoid-page`,`page`,`left`,`right`,`column`],b=()=>[`center`,`top`,`bottom`,`left`,`right`,`top-left`,`left-top`,`top-right`,`right-top`,`bottom-right`,`right-bottom`,`bottom-left`,`left-bottom`],x=()=>[...b(),Y,q],S=()=>[`auto`,`hidden`,`clip`,`visible`,`scroll`],C=()=>[`auto`,`contain`,`none`],w=()=>[Y,q,c],T=()=>[U,`full`,`auto`,...w()],ee=()=>[G,`none`,`subgrid`,Y,q],te=()=>[`auto`,{span:[`full`,G,Y,q]},G,Y,q],E=()=>[G,`auto`,Y,q],ne=()=>[`auto`,`min`,`max`,`fr`,Y,q],re=()=>[`start`,`end`,`center`,`between`,`around`,`evenly`,`stretch`,`baseline`,`center-safe`,`end-safe`],D=()=>[`start`,`end`,`center`,`stretch`,`center-safe`,`end-safe`],O=()=>[`auto`,...w()],k=()=>[U,`auto`,`full`,`dvw`,`dvh`,`lvw`,`lvh`,`svw`,`svh`,`min`,`max`,`fit`,...w()],A=()=>[U,`screen`,`full`,`dvw`,`lvw`,`svw`,`min`,`max`,`fit`,...w()],ie=()=>[U,`screen`,`full`,`lh`,`dvh`,`lvh`,`svh`,`min`,`max`,`fit`,...w()],j=()=>[e,Y,q],ae=()=>[...b(),dn,on,{position:[Y,q]}],oe=()=>[`no-repeat`,{repeat:[``,`x`,`y`,`space`,`round`]}],se=()=>[`auto`,`cover`,`contain`,fn,tn,{size:[Y,q]}],M=()=>[qt,ln,J],N=()=>[``,`none`,`full`,l,Y,q],P=()=>[``,W,ln,J],F=()=>[`solid`,`dashed`,`dotted`,`double`],ce=()=>[`normal`,`multiply`,`screen`,`overlay`,`darken`,`lighten`,`color-dodge`,`color-burn`,`hard-light`,`soft-light`,`difference`,`exclusion`,`hue`,`saturation`,`color`,`luminosity`],I=()=>[W,qt,dn,on],le=()=>[``,`none`,m,Y,q],ue=()=>[`none`,W,Y,q],L=()=>[`none`,W,Y,q],R=()=>[W,Y,q],z=()=>[U,`full`,...w()];return{cacheSize:500,theme:{animate:[`spin`,`ping`,`pulse`,`bounce`],aspect:[`video`],blur:[K],breakpoint:[K],color:[Jt],container:[K],"drop-shadow":[K],ease:[`in`,`out`,`in-out`],font:[$t],"font-weight":[`thin`,`extralight`,`light`,`normal`,`medium`,`semibold`,`bold`,`extrabold`,`black`],"inset-shadow":[K],leading:[`none`,`tight`,`snug`,`normal`,`relaxed`,`loose`],perspective:[`dramatic`,`near`,`normal`,`midrange`,`distant`,`none`],radius:[K],shadow:[K],spacing:[`px`,W],text:[K],"text-shadow":[K],tracking:[`tighter`,`tight`,`normal`,`wide`,`wider`,`widest`]},classGroups:{aspect:[{aspect:[`auto`,`square`,U,q,Y,g]}],container:[`container`],"container-type":[{"@container":[``,`normal`,`size`,Y,q]}],"container-named":[en],columns:[{columns:[W,q,Y,s]}],"break-after":[{"break-after":y()}],"break-before":[{"break-before":y()}],"break-inside":[{"break-inside":[`auto`,`avoid`,`avoid-page`,`avoid-column`]}],"box-decoration":[{"box-decoration":[`slice`,`clone`]}],box:[{box:[`border`,`content`]}],display:[`block`,`inline-block`,`inline`,`flex`,`inline-flex`,`table`,`inline-table`,`table-caption`,`table-cell`,`table-column`,`table-column-group`,`table-footer-group`,`table-header-group`,`table-row-group`,`table-row`,`flow-root`,`grid`,`inline-grid`,`contents`,`list-item`,`hidden`],sr:[`sr-only`,`not-sr-only`],float:[{float:[`right`,`left`,`none`,`start`,`end`]}],clear:[{clear:[`left`,`right`,`both`,`none`,`start`,`end`]}],isolation:[`isolate`,`isolation-auto`],"object-fit":[{object:[`contain`,`cover`,`fill`,`none`,`scale-down`]}],"object-position":[{object:x()}],overflow:[{overflow:S()}],"overflow-x":[{"overflow-x":S()}],"overflow-y":[{"overflow-y":S()}],overscroll:[{overscroll:C()}],"overscroll-x":[{"overscroll-x":C()}],"overscroll-y":[{"overscroll-y":C()}],position:[`static`,`fixed`,`absolute`,`relative`,`sticky`],inset:[{inset:T()}],"inset-x":[{"inset-x":T()}],"inset-y":[{"inset-y":T()}],start:[{"inset-s":T(),start:T()}],end:[{"inset-e":T(),end:T()}],"inset-bs":[{"inset-bs":T()}],"inset-be":[{"inset-be":T()}],top:[{top:T()}],right:[{right:T()}],bottom:[{bottom:T()}],left:[{left:T()}],visibility:[`visible`,`invisible`,`collapse`],z:[{z:[G,`auto`,Y,q]}],basis:[{basis:[U,`full`,`auto`,s,...w()]}],"flex-direction":[{flex:[`row`,`row-reverse`,`col`,`col-reverse`]}],"flex-wrap":[{flex:[`nowrap`,`wrap`,`wrap-reverse`]}],flex:[{flex:[W,U,`auto`,`initial`,`none`,q]}],grow:[{grow:[``,W,Y,q]}],shrink:[{shrink:[``,W,Y,q]}],order:[{order:[G,`first`,`last`,`none`,Y,q]}],"grid-cols":[{"grid-cols":ee()}],"col-start-end":[{col:te()}],"col-start":[{"col-start":E()}],"col-end":[{"col-end":E()}],"grid-rows":[{"grid-rows":ee()}],"row-start-end":[{row:te()}],"row-start":[{"row-start":E()}],"row-end":[{"row-end":E()}],"grid-flow":[{"grid-flow":[`row`,`col`,`dense`,`row-dense`,`col-dense`]}],"auto-cols":[{"auto-cols":ne()}],"auto-rows":[{"auto-rows":ne()}],gap:[{gap:w()}],"gap-x":[{"gap-x":w()}],"gap-y":[{"gap-y":w()}],"justify-content":[{justify:[...re(),`normal`]}],"justify-items":[{"justify-items":[...D(),`normal`]}],"justify-self":[{"justify-self":[`auto`,...D()]}],"align-content":[{content:[`normal`,...re()]}],"align-items":[{items:[...D(),{baseline:[``,`last`]}]}],"align-self":[{self:[`auto`,...D(),{baseline:[``,`last`]}]}],"place-content":[{"place-content":re()}],"place-items":[{"place-items":[...D(),`baseline`]}],"place-self":[{"place-self":[`auto`,...D()]}],p:[{p:w()}],px:[{px:w()}],py:[{py:w()}],ps:[{ps:w()}],pe:[{pe:w()}],pbs:[{pbs:w()}],pbe:[{pbe:w()}],pt:[{pt:w()}],pr:[{pr:w()}],pb:[{pb:w()}],pl:[{pl:w()}],m:[{m:O()}],mx:[{mx:O()}],my:[{my:O()}],ms:[{ms:O()}],me:[{me:O()}],mbs:[{mbs:O()}],mbe:[{mbe:O()}],mt:[{mt:O()}],mr:[{mr:O()}],mb:[{mb:O()}],ml:[{ml:O()}],"space-x":[{"space-x":w()}],"space-x-reverse":[`space-x-reverse`],"space-y":[{"space-y":w()}],"space-y-reverse":[`space-y-reverse`],size:[{size:k()}],"inline-size":[{inline:[`auto`,...A()]}],"min-inline-size":[{"min-inline":[`auto`,...A()]}],"max-inline-size":[{"max-inline":[`none`,...A()]}],"block-size":[{block:[`auto`,...ie()]}],"min-block-size":[{"min-block":[`auto`,...ie()]}],"max-block-size":[{"max-block":[`none`,...ie()]}],w:[{w:[s,`screen`,...k()]}],"min-w":[{"min-w":[s,`screen`,`none`,...k()]}],"max-w":[{"max-w":[s,`screen`,`none`,`prose`,{screen:[o]},...k()]}],h:[{h:[`screen`,`lh`,...k()]}],"min-h":[{"min-h":[`screen`,`lh`,`none`,...k()]}],"max-h":[{"max-h":[`screen`,`lh`,...k()]}],"font-size":[{text:[`base`,n,ln,J]}],"font-smoothing":[`antialiased`,`subpixel-antialiased`],"font-style":[`italic`,`not-italic`],"font-weight":[{font:[r,hn,rn]}],"font-stretch":[{"font-stretch":[`ultra-condensed`,`extra-condensed`,`condensed`,`semi-condensed`,`normal`,`semi-expanded`,`expanded`,`extra-expanded`,`ultra-expanded`,qt,q]}],"font-family":[{font:[un,an,t]}],"font-features":[{"font-features":[q]}],"fvn-normal":[`normal-nums`],"fvn-ordinal":[`ordinal`],"fvn-slashed-zero":[`slashed-zero`],"fvn-figure":[`lining-nums`,`oldstyle-nums`],"fvn-spacing":[`proportional-nums`,`tabular-nums`],"fvn-fraction":[`diagonal-fractions`,`stacked-fractions`],tracking:[{tracking:[i,Y,q]}],"line-clamp":[{"line-clamp":[W,`none`,Y,nn]}],leading:[{leading:[a,...w()]}],"list-image":[{"list-image":[`none`,Y,q]}],"list-style-position":[{list:[`inside`,`outside`]}],"list-style-type":[{list:[`disc`,`decimal`,`none`,Y,q]}],"text-alignment":[{text:[`left`,`center`,`right`,`justify`,`start`,`end`]}],"placeholder-color":[{placeholder:j()}],"text-color":[{text:j()}],"text-decoration":[`underline`,`overline`,`line-through`,`no-underline`],"text-decoration-style":[{decoration:[...F(),`wavy`]}],"text-decoration-thickness":[{decoration:[W,`from-font`,`auto`,Y,J]}],"text-decoration-color":[{decoration:j()}],"underline-offset":[{"underline-offset":[W,`auto`,Y,q]}],"text-transform":[`uppercase`,`lowercase`,`capitalize`,`normal-case`],"text-overflow":[`truncate`,`text-ellipsis`,`text-clip`],"text-wrap":[{text:[`wrap`,`nowrap`,`balance`,`pretty`]}],indent:[{indent:w()}],"tab-size":[{tab:[G,Y,q]}],"vertical-align":[{align:[`baseline`,`top`,`middle`,`bottom`,`text-top`,`text-bottom`,`sub`,`super`,Y,q]}],whitespace:[{whitespace:[`normal`,`nowrap`,`pre`,`pre-line`,`pre-wrap`,`break-spaces`]}],break:[{break:[`normal`,`words`,`all`,`keep`]}],wrap:[{wrap:[`break-word`,`anywhere`,`normal`]}],hyphens:[{hyphens:[`none`,`manual`,`auto`]}],content:[{content:[`none`,Y,q]}],"bg-attachment":[{bg:[`fixed`,`local`,`scroll`]}],"bg-clip":[{"bg-clip":[`border`,`padding`,`content`,`text`]}],"bg-origin":[{"bg-origin":[`border`,`padding`,`content`]}],"bg-position":[{bg:ae()}],"bg-repeat":[{bg:oe()}],"bg-size":[{bg:se()}],"bg-image":[{bg:[`none`,{linear:[{to:[`t`,`tr`,`r`,`br`,`b`,`bl`,`l`,`tl`]},G,Y,q],radial:[``,Y,q],conic:[G,Y,q]},pn,sn]}],"bg-color":[{bg:j()}],"gradient-from-pos":[{from:M()}],"gradient-via-pos":[{via:M()}],"gradient-to-pos":[{to:M()}],"gradient-from":[{from:j()}],"gradient-via":[{via:j()}],"gradient-to":[{to:j()}],rounded:[{rounded:N()}],"rounded-s":[{"rounded-s":N()}],"rounded-e":[{"rounded-e":N()}],"rounded-t":[{"rounded-t":N()}],"rounded-r":[{"rounded-r":N()}],"rounded-b":[{"rounded-b":N()}],"rounded-l":[{"rounded-l":N()}],"rounded-ss":[{"rounded-ss":N()}],"rounded-se":[{"rounded-se":N()}],"rounded-ee":[{"rounded-ee":N()}],"rounded-es":[{"rounded-es":N()}],"rounded-tl":[{"rounded-tl":N()}],"rounded-tr":[{"rounded-tr":N()}],"rounded-br":[{"rounded-br":N()}],"rounded-bl":[{"rounded-bl":N()}],"border-w":[{border:P()}],"border-w-x":[{"border-x":P()}],"border-w-y":[{"border-y":P()}],"border-w-s":[{"border-s":P()}],"border-w-e":[{"border-e":P()}],"border-w-bs":[{"border-bs":P()}],"border-w-be":[{"border-be":P()}],"border-w-t":[{"border-t":P()}],"border-w-r":[{"border-r":P()}],"border-w-b":[{"border-b":P()}],"border-w-l":[{"border-l":P()}],"divide-x":[{"divide-x":P()}],"divide-x-reverse":[`divide-x-reverse`],"divide-y":[{"divide-y":P()}],"divide-y-reverse":[`divide-y-reverse`],"border-style":[{border:[...F(),`hidden`,`none`]}],"divide-style":[{divide:[...F(),`hidden`,`none`]}],"border-color":[{border:j()}],"border-color-x":[{"border-x":j()}],"border-color-y":[{"border-y":j()}],"border-color-s":[{"border-s":j()}],"border-color-e":[{"border-e":j()}],"border-color-bs":[{"border-bs":j()}],"border-color-be":[{"border-be":j()}],"border-color-t":[{"border-t":j()}],"border-color-r":[{"border-r":j()}],"border-color-b":[{"border-b":j()}],"border-color-l":[{"border-l":j()}],"divide-color":[{divide:j()}],"outline-style":[{outline:[...F(),`none`,`hidden`]}],"outline-offset":[{"outline-offset":[W,Y,q]}],"outline-w":[{outline:[``,W,ln,J]}],"outline-color":[{outline:j()}],shadow:[{shadow:[``,`none`,u,mn,cn]}],"shadow-color":[{shadow:j()}],"inset-shadow":[{"inset-shadow":[`none`,d,mn,cn]}],"inset-shadow-color":[{"inset-shadow":j()}],"ring-w":[{ring:P()}],"ring-w-inset":[`ring-inset`],"ring-color":[{ring:j()}],"ring-offset-w":[{"ring-offset":[W,J]}],"ring-offset-color":[{"ring-offset":j()}],"inset-ring-w":[{"inset-ring":P()}],"inset-ring-color":[{"inset-ring":j()}],"text-shadow":[{"text-shadow":[`none`,f,mn,cn]}],"text-shadow-color":[{"text-shadow":j()}],opacity:[{opacity:[W,Y,q]}],"mix-blend":[{"mix-blend":[...ce(),`plus-darker`,`plus-lighter`]}],"bg-blend":[{"bg-blend":ce()}],"mask-clip":[{"mask-clip":[`border`,`padding`,`content`,`fill`,`stroke`,`view`]},`mask-no-clip`],"mask-composite":[{mask:[`add`,`subtract`,`intersect`,`exclude`]}],"mask-image-linear-pos":[{"mask-linear":[W]}],"mask-image-linear-from-pos":[{"mask-linear-from":I()}],"mask-image-linear-to-pos":[{"mask-linear-to":I()}],"mask-image-linear-from-color":[{"mask-linear-from":j()}],"mask-image-linear-to-color":[{"mask-linear-to":j()}],"mask-image-t-from-pos":[{"mask-t-from":I()}],"mask-image-t-to-pos":[{"mask-t-to":I()}],"mask-image-t-from-color":[{"mask-t-from":j()}],"mask-image-t-to-color":[{"mask-t-to":j()}],"mask-image-r-from-pos":[{"mask-r-from":I()}],"mask-image-r-to-pos":[{"mask-r-to":I()}],"mask-image-r-from-color":[{"mask-r-from":j()}],"mask-image-r-to-color":[{"mask-r-to":j()}],"mask-image-b-from-pos":[{"mask-b-from":I()}],"mask-image-b-to-pos":[{"mask-b-to":I()}],"mask-image-b-from-color":[{"mask-b-from":j()}],"mask-image-b-to-color":[{"mask-b-to":j()}],"mask-image-l-from-pos":[{"mask-l-from":I()}],"mask-image-l-to-pos":[{"mask-l-to":I()}],"mask-image-l-from-color":[{"mask-l-from":j()}],"mask-image-l-to-color":[{"mask-l-to":j()}],"mask-image-x-from-pos":[{"mask-x-from":I()}],"mask-image-x-to-pos":[{"mask-x-to":I()}],"mask-image-x-from-color":[{"mask-x-from":j()}],"mask-image-x-to-color":[{"mask-x-to":j()}],"mask-image-y-from-pos":[{"mask-y-from":I()}],"mask-image-y-to-pos":[{"mask-y-to":I()}],"mask-image-y-from-color":[{"mask-y-from":j()}],"mask-image-y-to-color":[{"mask-y-to":j()}],"mask-image-radial":[{"mask-radial":[Y,q]}],"mask-image-radial-from-pos":[{"mask-radial-from":I()}],"mask-image-radial-to-pos":[{"mask-radial-to":I()}],"mask-image-radial-from-color":[{"mask-radial-from":j()}],"mask-image-radial-to-color":[{"mask-radial-to":j()}],"mask-image-radial-shape":[{"mask-radial":[`circle`,`ellipse`]}],"mask-image-radial-size":[{"mask-radial":[{closest:[`side`,`corner`],farthest:[`side`,`corner`]}]}],"mask-image-radial-pos":[{"mask-radial-at":b()}],"mask-image-conic-pos":[{"mask-conic":[W]}],"mask-image-conic-from-pos":[{"mask-conic-from":I()}],"mask-image-conic-to-pos":[{"mask-conic-to":I()}],"mask-image-conic-from-color":[{"mask-conic-from":j()}],"mask-image-conic-to-color":[{"mask-conic-to":j()}],"mask-mode":[{mask:[`alpha`,`luminance`,`match`]}],"mask-origin":[{"mask-origin":[`border`,`padding`,`content`,`fill`,`stroke`,`view`]}],"mask-position":[{mask:ae()}],"mask-repeat":[{mask:oe()}],"mask-size":[{mask:se()}],"mask-type":[{"mask-type":[`alpha`,`luminance`]}],"mask-image":[{mask:[`none`,Y,q]}],filter:[{filter:[``,`none`,Y,q]}],blur:[{blur:le()}],brightness:[{brightness:[W,Y,q]}],contrast:[{contrast:[W,Y,q]}],"drop-shadow":[{"drop-shadow":[``,`none`,p,mn,cn]}],"drop-shadow-color":[{"drop-shadow":j()}],grayscale:[{grayscale:[``,W,Y,q]}],"hue-rotate":[{"hue-rotate":[W,Y,q]}],invert:[{invert:[``,W,Y,q]}],saturate:[{saturate:[W,Y,q]}],sepia:[{sepia:[``,W,Y,q]}],"backdrop-filter":[{"backdrop-filter":[``,`none`,Y,q]}],"backdrop-blur":[{"backdrop-blur":le()}],"backdrop-brightness":[{"backdrop-brightness":[W,Y,q]}],"backdrop-contrast":[{"backdrop-contrast":[W,Y,q]}],"backdrop-grayscale":[{"backdrop-grayscale":[``,W,Y,q]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[W,Y,q]}],"backdrop-invert":[{"backdrop-invert":[``,W,Y,q]}],"backdrop-opacity":[{"backdrop-opacity":[W,Y,q]}],"backdrop-saturate":[{"backdrop-saturate":[W,Y,q]}],"backdrop-sepia":[{"backdrop-sepia":[``,W,Y,q]}],"border-collapse":[{border:[`collapse`,`separate`]}],"border-spacing":[{"border-spacing":w()}],"border-spacing-x":[{"border-spacing-x":w()}],"border-spacing-y":[{"border-spacing-y":w()}],"table-layout":[{table:[`auto`,`fixed`]}],caption:[{caption:[`top`,`bottom`]}],transition:[{transition:[``,`all`,`colors`,`opacity`,`shadow`,`transform`,`none`,Y,q]}],"transition-behavior":[{transition:[`normal`,`discrete`]}],duration:[{duration:[W,`initial`,Y,q]}],ease:[{ease:[`linear`,`initial`,_,Y,q]}],delay:[{delay:[W,Y,q]}],animate:[{animate:[`none`,v,Y,q]}],backface:[{backface:[`hidden`,`visible`]}],perspective:[{perspective:[h,Y,q]}],"perspective-origin":[{"perspective-origin":x()}],rotate:[{rotate:ue()}],"rotate-x":[{"rotate-x":ue()}],"rotate-y":[{"rotate-y":ue()}],"rotate-z":[{"rotate-z":ue()}],scale:[{scale:L()}],"scale-x":[{"scale-x":L()}],"scale-y":[{"scale-y":L()}],"scale-z":[{"scale-z":L()}],"scale-3d":[`scale-3d`],skew:[{skew:R()}],"skew-x":[{"skew-x":R()}],"skew-y":[{"skew-y":R()}],transform:[{transform:[Y,q,``,`none`,`gpu`,`cpu`]}],"transform-origin":[{origin:x()}],"transform-style":[{transform:[`3d`,`flat`]}],translate:[{translate:z()}],"translate-x":[{"translate-x":z()}],"translate-y":[{"translate-y":z()}],"translate-z":[{"translate-z":z()}],"translate-none":[`translate-none`],zoom:[{zoom:[G,Y,q]}],accent:[{accent:j()}],appearance:[{appearance:[`none`,`auto`]}],"caret-color":[{caret:j()}],"color-scheme":[{scheme:[`normal`,`dark`,`light`,`light-dark`,`only-dark`,`only-light`]}],cursor:[{cursor:[`auto`,`default`,`pointer`,`wait`,`text`,`move`,`help`,`not-allowed`,`none`,`context-menu`,`progress`,`cell`,`crosshair`,`vertical-text`,`alias`,`copy`,`no-drop`,`grab`,`grabbing`,`all-scroll`,`col-resize`,`row-resize`,`n-resize`,`e-resize`,`s-resize`,`w-resize`,`ne-resize`,`nw-resize`,`se-resize`,`sw-resize`,`ew-resize`,`ns-resize`,`nesw-resize`,`nwse-resize`,`zoom-in`,`zoom-out`,Y,q]}],"field-sizing":[{"field-sizing":[`fixed`,`content`]}],"pointer-events":[{"pointer-events":[`auto`,`none`]}],resize:[{resize:[`none`,``,`y`,`x`]}],"scroll-behavior":[{scroll:[`auto`,`smooth`]}],"scrollbar-thumb-color":[{"scrollbar-thumb":j()}],"scrollbar-track-color":[{"scrollbar-track":j()}],"scrollbar-gutter":[{"scrollbar-gutter":[`auto`,`stable`,`both`]}],"scrollbar-w":[{scrollbar:[`auto`,`thin`,`none`]}],"scroll-m":[{"scroll-m":w()}],"scroll-mx":[{"scroll-mx":w()}],"scroll-my":[{"scroll-my":w()}],"scroll-ms":[{"scroll-ms":w()}],"scroll-me":[{"scroll-me":w()}],"scroll-mbs":[{"scroll-mbs":w()}],"scroll-mbe":[{"scroll-mbe":w()}],"scroll-mt":[{"scroll-mt":w()}],"scroll-mr":[{"scroll-mr":w()}],"scroll-mb":[{"scroll-mb":w()}],"scroll-ml":[{"scroll-ml":w()}],"scroll-p":[{"scroll-p":w()}],"scroll-px":[{"scroll-px":w()}],"scroll-py":[{"scroll-py":w()}],"scroll-ps":[{"scroll-ps":w()}],"scroll-pe":[{"scroll-pe":w()}],"scroll-pbs":[{"scroll-pbs":w()}],"scroll-pbe":[{"scroll-pbe":w()}],"scroll-pt":[{"scroll-pt":w()}],"scroll-pr":[{"scroll-pr":w()}],"scroll-pb":[{"scroll-pb":w()}],"scroll-pl":[{"scroll-pl":w()}],"snap-align":[{snap:[`start`,`end`,`center`,`align-none`]}],"snap-stop":[{snap:[`normal`,`always`]}],"snap-type":[{snap:[`none`,`x`,`y`,`both`]}],"snap-strictness":[{snap:[`mandatory`,`proximity`]}],touch:[{touch:[`auto`,`none`,`manipulation`]}],"touch-x":[{"touch-pan":[`x`,`left`,`right`]}],"touch-y":[{"touch-pan":[`y`,`up`,`down`]}],"touch-pz":[`touch-pinch-zoom`],select:[{select:[`none`,`text`,`all`,`auto`]}],"will-change":[{"will-change":[`auto`,`scroll`,`contents`,`transform`,Y,q]}],fill:[{fill:[`none`,...j()]}],"stroke-w":[{stroke:[W,ln,J,nn]}],stroke:[{stroke:[`none`,...j()]}],"forced-color-adjust":[{"forced-color-adjust":[`auto`,`none`]}]},conflictingClassGroups:{"container-named":[`container-type`],overflow:[`overflow-x`,`overflow-y`],overscroll:[`overscroll-x`,`overscroll-y`],inset:[`inset-x`,`inset-y`,`inset-bs`,`inset-be`,`start`,`end`,`top`,`right`,`bottom`,`left`],"inset-x":[`right`,`left`],"inset-y":[`top`,`bottom`],flex:[`basis`,`grow`,`shrink`],gap:[`gap-x`,`gap-y`],p:[`px`,`py`,`ps`,`pe`,`pbs`,`pbe`,`pt`,`pr`,`pb`,`pl`],px:[`pr`,`pl`],py:[`pt`,`pb`],m:[`mx`,`my`,`ms`,`me`,`mbs`,`mbe`,`mt`,`mr`,`mb`,`ml`],mx:[`mr`,`ml`],my:[`mt`,`mb`],size:[`w`,`h`],"font-size":[`leading`],"fvn-normal":[`fvn-ordinal`,`fvn-slashed-zero`,`fvn-figure`,`fvn-spacing`,`fvn-fraction`],"fvn-ordinal":[`fvn-normal`],"fvn-slashed-zero":[`fvn-normal`],"fvn-figure":[`fvn-normal`],"fvn-spacing":[`fvn-normal`],"fvn-fraction":[`fvn-normal`],"line-clamp":[`display`,`overflow`],rounded:[`rounded-s`,`rounded-e`,`rounded-t`,`rounded-r`,`rounded-b`,`rounded-l`,`rounded-ss`,`rounded-se`,`rounded-ee`,`rounded-es`,`rounded-tl`,`rounded-tr`,`rounded-br`,`rounded-bl`],"rounded-s":[`rounded-ss`,`rounded-es`],"rounded-e":[`rounded-se`,`rounded-ee`],"rounded-t":[`rounded-tl`,`rounded-tr`],"rounded-r":[`rounded-tr`,`rounded-br`],"rounded-b":[`rounded-br`,`rounded-bl`],"rounded-l":[`rounded-tl`,`rounded-bl`],"border-spacing":[`border-spacing-x`,`border-spacing-y`],"border-w":[`border-w-x`,`border-w-y`,`border-w-s`,`border-w-e`,`border-w-bs`,`border-w-be`,`border-w-t`,`border-w-r`,`border-w-b`,`border-w-l`],"border-w-x":[`border-w-r`,`border-w-l`],"border-w-y":[`border-w-t`,`border-w-b`],"border-color":[`border-color-x`,`border-color-y`,`border-color-s`,`border-color-e`,`border-color-bs`,`border-color-be`,`border-color-t`,`border-color-r`,`border-color-b`,`border-color-l`],"border-color-x":[`border-color-r`,`border-color-l`],"border-color-y":[`border-color-t`,`border-color-b`],translate:[`translate-x`,`translate-y`,`translate-none`],"translate-none":[`translate`,`translate-x`,`translate-y`,`translate-z`],"scroll-m":[`scroll-mx`,`scroll-my`,`scroll-ms`,`scroll-me`,`scroll-mbs`,`scroll-mbe`,`scroll-mt`,`scroll-mr`,`scroll-mb`,`scroll-ml`],"scroll-mx":[`scroll-mr`,`scroll-ml`],"scroll-my":[`scroll-mt`,`scroll-mb`],"scroll-p":[`scroll-px`,`scroll-py`,`scroll-ps`,`scroll-pe`,`scroll-pbs`,`scroll-pbe`,`scroll-pt`,`scroll-pr`,`scroll-pb`,`scroll-pl`],"scroll-px":[`scroll-pr`,`scroll-pl`],"scroll-py":[`scroll-pt`,`scroll-pb`],touch:[`touch-x`,`touch-y`,`touch-pz`],"touch-x":[`touch`],"touch-y":[`touch`],"touch-pz":[`touch`]},conflictingClassGroupModifiers:{"font-size":[`leading`]},postfixLookupClassGroups:[`container-type`],orderSensitiveModifiers:[`*`,`**`,`after`,`backdrop`,`before`,`details-content`,`file`,`first-letter`,`first-line`,`marker`,`placeholder`,`selection`]}});function Tn(...e){return wn(nt(e))}var En=n(),Dn=at(`group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,{variants:{variant:{default:`bg-primary text-primary-foreground hover:bg-primary/80`,outline:`border-border bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground`,secondary:`bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground`,ghost:`hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50`,destructive:`bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40`,link:`text-primary underline-offset-4 hover:underline`},size:{default:`h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5`,xs:`h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3`,sm:`h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2`,lg:`h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3`,icon:`size-9`,"icon-xs":`size-6 [&_svg:not([class*='size-'])]:size-3`,"icon-sm":`size-8`,"icon-lg":`size-10`}},defaultVariants:{variant:`default`,size:`default`}});function On({className:e,variant:t=`default`,size:n=`default`,...r}){return(0,En.jsx)(et,{"data-slot":`button`,className:Tn(Dn({variant:t,size:n,className:e})),...r})}var kn=`dt_gate_active`,An=`1`;function jn(){try{return typeof sessionStorage>`u`?!1:sessionStorage.getItem(kn)===An}catch{return!1}}function Mn(){try{if(typeof sessionStorage>`u`)return;sessionStorage.setItem(kn,An)}catch{}}var Nn=!0,Pn=jn(),Fn=new Set;function In(){for(let e of Fn)e()}var Q={isAuthed:()=>Nn,isGateActive:()=>Pn,markAuthenticated(){Nn=!0,Pn=!0,Mn(),In()},markUnauthenticated(){Nn=!1,Pn=!0,Mn(),In()},markGateActive(){Pn||(Pn=!0,Mn(),In())},subscribe(e){return Fn.add(e),()=>{Fn.delete(e)}}};function Ln(){return(0,r.useSyncExternalStore)(Q.subscribe,Q.isAuthed,Q.isAuthed)}function Rn(){return(0,r.useSyncExternalStore)(Q.subscribe,Q.isGateActive,Q.isGateActive)}var zn=`X-Instance-Gated`,Bn=`1`,Vn=class extends Error{status;body;code;constructor(e,t,n,r){super(t),this.name=`ApiError`,this.status=e,this.body=n,this.code=r}};async function $(e,t){let n=(t?.method??`GET`)===`GET`?t?.headers:{...t?.headers,"X-Requested-With":`drop-tracker`},r=await fetch(e,{...t,headers:n});if(r.headers.get(zn)===Bn&&Q.markGateActive(),r.status===401)throw Q.markUnauthenticated(),new Vn(401,`unauthenticated`);if(r.status!==204){if(!r.ok){let e=r.statusText,t,n;try{let i=await r.json();t=i,i.error&&(e=i.error),typeof i.code==`string`&&(n=i.code)}catch{}throw new Vn(r.status,e,t,n)}return await r.json()}}async function Hn(e){let t=new URLSearchParams;e?.artistId!=null&&t.set(`artist_id`,String(e.artistId)),e?.eventType&&t.set(`event_type`,e.eventType),e?.tagId!=null&&t.set(`tag_id`,String(e.tagId)),e?.cursor!=null&&t.set(`cursor`,e.cursor);let n=t.toString();return $(`/events${n?`?${n}`:``}`)}async function Un(){return $(`/watchlist`)}async function Wn(e){return $(`/watchlist`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({mbid:e.mbid,name:e.name,deezer_id:e.deezerId,disambiguation:e.disambiguation,image_url:e.imageUrl,note:e.note})})}async function Gn(e,t){return $(`/watchlist/${e}/note`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({note:t})})}async function Kn(e,t){return $(`/watchlist/${e}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({release_types:t.releaseTypes,muted_event_types:t.mutedEventTypes})})}async function qn(e){await $(`/watchlist/${e}`,{method:`DELETE`})}async function Jn(e,t){await $(`/watchlist/${e}/tags/${t}`,{method:`DELETE`})}async function Yn(e,t){return $(`/watchlist/${e}/tags`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:t})})}async function Xn(){return $(`/tags`)}async function Zn(e){return $(`/tags/${e}`,{method:`DELETE`})}async function Qn(e,t){try{return{kind:`renamed`,tag:await $(`/tags/${e}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:t})})}}catch(e){if(e instanceof Vn&&e.status===409&&e.body){let t=e.body;if(t.target&&typeof t.carrier_count_after_merge==`number`)return{kind:`collision`,target:t.target,carrierCountAfterMerge:t.carrier_count_after_merge}}throw e}}async function $n(e,t){return $(`/tags/${e}/merge`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({into:t})})}async function er(e,t){return $(`/search?${new URLSearchParams({q:e}).toString()}`,{signal:t})}async function tr(e){await $(`/session`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({passphrase:e})})}async function nr(){await $(`/session`,{method:`DELETE`})}async function rr(){return $(`/status`)}async function ir(){return $(`/settings/notifications`)}async function ar(e){return $(`/settings/notifications`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({digest_enabled:e.digestEnabled,digest_cadence:e.digestCadence})})}export{k as $,Ve as A,M as B,On as C,We as D,Je as E,Te as F,g as G,D as H,de as I,j as J,ie as K,L,je as M,Ae as N,V as O,ke as P,x as Q,I as R,Rn as S,at as T,v as U,A as V,se as W,re as X,_ as Y,b as Z,ar as _,nr as a,O as at,Q as b,ir as c,Xn as d,y as et,Un as f,er as g,Qn as h,tr as i,T as it,Pe as j,Ue as k,rr as l,qn as m,Wn as n,S as nt,Zn as o,m as ot,$n as p,oe as q,Yn as r,w as rt,Jn as s,p as st,Vn as t,C as tt,Hn as u,Gn as v,Tn as w,Ln as x,Kn as y,P as z}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/badge-mjpdUX1d.js b/internal/webassets/build/client/assets/badge-B492YG3Q.js similarity index 93% rename from internal/webassets/build/client/assets/badge-mjpdUX1d.js rename to internal/webassets/build/client/assets/badge-B492YG3Q.js index 3e1d388..1540a45 100644 --- a/internal/webassets/build/client/assets/badge-mjpdUX1d.js +++ b/internal/webassets/build/client/assets/badge-B492YG3Q.js @@ -1 +1 @@ -import{t as e}from"./jsx-runtime-DT4cu4GC.js";import{E as t,I as n,T as r,w as i}from"./api-Bh4xuY1_.js";var a=e();function o({heading:e,body:t,action:n}){return(0,a.jsxs)(`div`,{className:`flex flex-col items-center gap-4 rounded-md bg-card px-6 py-16 text-center`,children:[(0,a.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[(0,a.jsx)(`h2`,{className:`text-heading font-semibold text-foreground`,children:e}),(0,a.jsx)(`p`,{className:`text-body text-muted-foreground`,children:t})]}),n]})}function s({className:e,...t}){return(0,a.jsx)(`div`,{"data-slot":`skeleton`,className:i(`animate-pulse rounded-xl bg-muted`,e),...t})}function c(e){return t(e.defaultTagName??`div`,e,e)}var l=r(`group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!`,{variants:{variant:{default:`bg-primary text-primary-foreground [a]:hover:bg-primary/80`,secondary:`bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80`,destructive:`bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20`,outline:`border-border bg-input/30 text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground`,ghost:`hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50`,link:`text-primary underline-offset-4 hover:underline`}},defaultVariants:{variant:`default`}});function u({className:e,variant:t=`default`,render:r,...a}){return c({defaultTagName:`span`,props:n({className:i(l({variant:t}),e)},a),render:r,state:{slot:`badge`,variant:t}})}export{s as n,o as r,u as t}; \ No newline at end of file +import{t as e}from"./jsx-runtime-D0QO-s14.js";import{E as t,I as n,T as r,w as i}from"./api-k_NcBvGr.js";var a=e();function o({heading:e,body:t,action:n}){return(0,a.jsxs)(`div`,{className:`flex flex-col items-center gap-4 rounded-md bg-card px-6 py-16 text-center`,children:[(0,a.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[(0,a.jsx)(`h2`,{className:`text-heading font-semibold text-foreground`,children:e}),(0,a.jsx)(`p`,{className:`text-body text-muted-foreground`,children:t})]}),n]})}function s({className:e,...t}){return(0,a.jsx)(`div`,{"data-slot":`skeleton`,className:i(`animate-pulse rounded-xl bg-muted`,e),...t})}function c(e){return t(e.defaultTagName??`div`,e,e)}var l=r(`group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!`,{variants:{variant:{default:`bg-primary text-primary-foreground [a]:hover:bg-primary/80`,secondary:`bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80`,destructive:`bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20`,outline:`border-border bg-input/30 text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground`,ghost:`hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50`,link:`text-primary underline-offset-4 hover:underline`}},defaultVariants:{variant:`default`}});function u({className:e,variant:t=`default`,render:r,...a}){return c({defaultTagName:`span`,props:n({className:i(l({variant:t}),e)},a),render:r,state:{slot:`badge`,variant:t}})}export{s as n,o as r,u as t}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/createBaseUIEventDetails-BTtuUEn7.js b/internal/webassets/build/client/assets/createBaseUIEventDetails-wEMuC0RZ.js similarity index 75% rename from internal/webassets/build/client/assets/createBaseUIEventDetails-BTtuUEn7.js rename to internal/webassets/build/client/assets/createBaseUIEventDetails-wEMuC0RZ.js index 946b893..be1bb3c 100644 --- a/internal/webassets/build/client/assets/createBaseUIEventDetails-BTtuUEn7.js +++ b/internal/webassets/build/client/assets/createBaseUIEventDetails-wEMuC0RZ.js @@ -1 +1 @@ -import{A as e,M as t}from"./jsx-runtime-DT4cu4GC.js";import{B as n,F as r,L as i,O as a,R as o,Z as s,k as c,nt as l,z as u}from"./api-Bh4xuY1_.js";var d=t(e(),1),f={badInput:!1,customError:!1,patternMismatch:!1,rangeOverflow:!1,rangeUnderflow:!1,stepMismatch:!1,tooLong:!1,tooShort:!1,typeMismatch:!1,valid:null,valueMissing:!1},p={valid:null,touched:!1,dirty:!1,filled:!1,focused:!1},m={disabled:!1,...p},h={valid(e){return e===null?null:e?{"data-valid":``}:{"data-invalid":``}}},g={invalid:void 0,name:void 0,validityData:{state:f,errors:[],error:``,value:``,initialValue:null},setValidityData:c,disabled:void 0,setTouched:c,setDirty:c,setFilled:c,setFocused:c,validationMode:`onSubmit`,shouldValidateOnChange:()=>!1,state:m,registerFieldControl:c,validation:{getValidationProps:(e,t=a)=>t,inputRef:{current:null},registeredInputs:new Map,registerInput:c,getInputControl:()=>null,commit:async()=>{},change:c}},_=d.createContext(g);function v(e=!0){let t=d.useContext(_);if(t.setValidityData===c&&!e)throw Error(r(28));return t}var y=d.createContext({elementRef:{current:null},formRef:{current:{fields:new Map}},errors:{},clearErrors:c,validationMode:`onSubmit`,submitAttemptedRef:{current:!1}});function b(){return d.useContext(y)}var x=0;function S(e,t=`mui`){let[n,r]=d.useState(e),i=e||n;return d.useEffect(()=>{n??(x+=1,r(`${t}-${x}`))},[n,t]),i}var C=n.useId;function w(e,t){if(C!==void 0){let n=C();return e??(t?`${t}-${n}`:n)}return S(e,t)}function T(e){return w(e,`base-ui`)}var E=d.createContext({controlId:void 0,registerControlId:c,labelId:void 0,setLabelId:c,messageIds:[],setMessageIds:c,getDescriptionProps:e=>e});function D(){return d.useContext(E)}function O(e={}){let{id:t,implicit:n=!1,controlRef:r}=e,{controlId:a,registerControlId:l}=D(),f=T(t),p=n?a:void 0,m=u(()=>Symbol()),h=d.useRef(!1),g=d.useRef(t!=null),_=o(()=>{!h.current||l===c||(h.current=!1,l(m.current,void 0))});return i(()=>{if(l===c)return;let e;if(n){let n=r?.current;e=s(n)&&n.closest(`label`)!=null?t??null:p??f}else if(t!=null)g.current=!0,e=t;else if(g.current)e=f;else{_();return}if(e===void 0){_();return}h.current=!0,l(m.current,e)},[t,r,p,l,n,f,m,_]),d.useEffect(()=>_,[_]),a??f}function k(e){let t=e.activeElement;for(;t?.shadowRoot?.activeElement!=null;)t=t.shadowRoot.activeElement;return t}function A(e,t){if(!e||!t)return!1;let n=t.getRootNode?.();if(e.contains(t))return!0;if(n&&l(n)){let n=t;for(;n;){if(e===n)return!0;n=n.parentNode||n.host}}return!1}function j(e){return`composedPath`in e?e.composedPath()[0]:e.target}function M({controlled:e,default:t,name:n,state:r=`value`}){let{current:i}=d.useRef(e!==void 0),[a,o]=d.useState(t);return[i?e:a,d.useCallback(e=>{i||o(e)},[])]}function N(e,t,n,r,a=!0,o){let{registerFieldControl:s}=v(),c=u(()=>Symbol());i(()=>{let i=c.current;if(!a){s(i,void 0);return}s(i,{controlRef:e,getValue:r,id:t,name:o,value:n})},[e,a,r,t,o,s,c,n]),i(()=>{let e=c.current;return()=>{s(e,void 0)}},[s,c])}var P=`none`,F=`trigger-press`,I=`trigger-hover`,L=`outside-press`,R=`item-press`,z=`close-press`,B=`clear-press`,V=`input-change`,H=`input-clear`,U=`input-press`,W=`focus-out`,G=`escape-key`,K=`list-navigation`,q=`keyboard`,J=`pointer`,Y=`cancel-open`,X=`imperative-action`,Z=`window-resize`;function Q(e,t,n,r){let i=!1,o=!1,s=r??a;return{reason:e,event:t??new Event(`base-ui`),cancel(){i=!0},allowPropagation(){o=!0},get isCanceled(){return i},get isPropagationAllowed(){return o},trigger:n,...s}}function $(e,t,n){let r=n??a;return{reason:e,event:t??new Event(`base-ui`),...r}}export{b as A,k as C,D,O as E,h as F,_ as M,v as N,T as O,p as P,M as S,j as T,J as _,z as a,Z as b,X as c,U as d,R as f,L as g,P as h,B as i,g as j,w as k,V as l,K as m,$ as n,G as o,q as p,Y as r,W as s,Q as t,H as u,I as v,A as w,N as x,F as y}; \ No newline at end of file +import{N as e,j as t}from"./jsx-runtime-D0QO-s14.js";import{B as n,F as r,L as i,O as a,R as o,Z as s,k as c,nt as l,z as u}from"./api-k_NcBvGr.js";var d=e(t(),1),f={badInput:!1,customError:!1,patternMismatch:!1,rangeOverflow:!1,rangeUnderflow:!1,stepMismatch:!1,tooLong:!1,tooShort:!1,typeMismatch:!1,valid:null,valueMissing:!1},p={valid:null,touched:!1,dirty:!1,filled:!1,focused:!1},m={disabled:!1,...p},h={valid(e){return e===null?null:e?{"data-valid":``}:{"data-invalid":``}}},g={invalid:void 0,name:void 0,validityData:{state:f,errors:[],error:``,value:``,initialValue:null},setValidityData:c,disabled:void 0,setTouched:c,setDirty:c,setFilled:c,setFocused:c,validationMode:`onSubmit`,shouldValidateOnChange:()=>!1,state:m,registerFieldControl:c,validation:{getValidationProps:(e,t=a)=>t,inputRef:{current:null},registeredInputs:new Map,registerInput:c,getInputControl:()=>null,commit:async()=>{},change:c}},_=d.createContext(g);function v(e=!0){let t=d.useContext(_);if(t.setValidityData===c&&!e)throw Error(r(28));return t}var y=d.createContext({elementRef:{current:null},formRef:{current:{fields:new Map}},errors:{},clearErrors:c,validationMode:`onSubmit`,submitAttemptedRef:{current:!1}});function b(){return d.useContext(y)}var x=0;function S(e,t=`mui`){let[n,r]=d.useState(e),i=e||n;return d.useEffect(()=>{n??(x+=1,r(`${t}-${x}`))},[n,t]),i}var C=n.useId;function w(e,t){if(C!==void 0){let n=C();return e??(t?`${t}-${n}`:n)}return S(e,t)}function T(e){return w(e,`base-ui`)}var E=d.createContext({controlId:void 0,registerControlId:c,labelId:void 0,setLabelId:c,messageIds:[],setMessageIds:c,getDescriptionProps:e=>e});function D(){return d.useContext(E)}function O(e={}){let{id:t,implicit:n=!1,controlRef:r}=e,{controlId:a,registerControlId:l}=D(),f=T(t),p=n?a:void 0,m=u(()=>Symbol()),h=d.useRef(!1),g=d.useRef(t!=null),_=o(()=>{!h.current||l===c||(h.current=!1,l(m.current,void 0))});return i(()=>{if(l===c)return;let e;if(n){let n=r?.current;e=s(n)&&n.closest(`label`)!=null?t??null:p??f}else if(t!=null)g.current=!0,e=t;else if(g.current)e=f;else{_();return}if(e===void 0){_();return}h.current=!0,l(m.current,e)},[t,r,p,l,n,f,m,_]),d.useEffect(()=>_,[_]),a??f}function k(e){let t=e.activeElement;for(;t?.shadowRoot?.activeElement!=null;)t=t.shadowRoot.activeElement;return t}function A(e,t){if(!e||!t)return!1;let n=t.getRootNode?.();if(e.contains(t))return!0;if(n&&l(n)){let n=t;for(;n;){if(e===n)return!0;n=n.parentNode||n.host}}return!1}function j(e){return`composedPath`in e?e.composedPath()[0]:e.target}function M({controlled:e,default:t,name:n,state:r=`value`}){let{current:i}=d.useRef(e!==void 0),[a,o]=d.useState(t);return[i?e:a,d.useCallback(e=>{i||o(e)},[])]}function N(e,t,n,r,a=!0,o){let{registerFieldControl:s}=v(),c=u(()=>Symbol());i(()=>{let i=c.current;if(!a){s(i,void 0);return}s(i,{controlRef:e,getValue:r,id:t,name:o,value:n})},[e,a,r,t,o,s,c,n]),i(()=>{let e=c.current;return()=>{s(e,void 0)}},[s,c])}var P=`none`,F=`trigger-press`,I=`trigger-hover`,L=`outside-press`,R=`item-press`,z=`close-press`,B=`clear-press`,V=`chip-remove-press`,H=`input-change`,U=`input-clear`,W=`input-press`,G=`focus-out`,K=`escape-key`,q=`list-navigation`,J=`keyboard`,Y=`pointer`,X=`cancel-open`,Z=`imperative-action`,Q=`window-resize`;function $(e,t,n,r){let i=!1,o=!1,s=r??a;return{reason:e,event:t??new Event(`base-ui`),cancel(){i=!0},allowPropagation(){o=!0},get isCanceled(){return i},get isPropagationAllowed(){return o},trigger:n,...s}}function ee(e,t,n){let r=n??a;return{reason:e,event:t??new Event(`base-ui`),...r}}export{w as A,M as C,O as D,j as E,p as F,h as I,g as M,_ as N,D as O,v as P,N as S,A as T,L as _,B as a,F as b,G as c,U as d,W as f,P as g,q as h,V as i,b as j,T as k,Z as l,J as m,ee as n,z as o,R as p,X as r,K as s,$ as t,H as u,Y as v,k as w,Q as x,I as y}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/entry.client-DJJfRPCk.js b/internal/webassets/build/client/assets/entry.client-oFNB33ee.js similarity index 86% rename from internal/webassets/build/client/assets/entry.client-DJJfRPCk.js rename to internal/webassets/build/client/assets/entry.client-oFNB33ee.js index 5448706..5d85486 100644 --- a/internal/webassets/build/client/assets/entry.client-DJJfRPCk.js +++ b/internal/webassets/build/client/assets/entry.client-oFNB33ee.js @@ -1,9 +1,9 @@ -import{A as e,D as t,E as n,M as r,S as i,T as a,_ as o,b as s,g as c,h as l,j as u,l as d,m as f,n as p,o as m,p as h,t as g,u as _,v,w as y,x as b,y as x}from"./jsx-runtime-DT4cu4GC.js";import{t as ee}from"./react-dom-I-EqbIrq.js";var te=u((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,ee||(ee=!0,ae());else{var t=n(l);t!==null&&C(x,t.startTime-e)}}var ee=!1,te=-1,ne=5,re=-1;function ie(){return g?!0:!(e.unstable_now()-ret&&ie());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&C(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?ae():ee=!1}}}var ae;if(typeof y==`function`)ae=function(){y(S)};else if(typeof MessageChannel<`u`){var oe=new MessageChannel,se=oe.port2;oe.port1.onmessage=S,ae=function(){se.postMessage(null)}}else ae=function(){_(S,0)};function C(t,n){te=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(te),te=-1):h=!0,C(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,ee||(ee=!0,ae()))),r},e.unstable_shouldYield=ie,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),ne=u(((e,t)=>{t.exports=te()})),re=u((t=>{var n=ne(),r=e(),i=ee();function a(e){var t=`https://react.dev/errors/`+e;if(1me||(e.current=pe[me],pe[me]=null,me--)}function D(e,t){me++,pe[me]=e.current,e.current=t}var ge=he(null),_e=he(null),ve=he(null),ye=he(null);function be(e,t){switch(D(ve,t),D(_e,e),D(ge,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}E(ge),D(ge,e)}function xe(){E(ge),E(_e),E(ve)}function Se(e){e.memoizedState!==null&&D(ye,e);var t=ge.current,n=Hd(t,e.type);t!==n&&(D(_e,e),D(ge,n))}function Ce(e){_e.current===e&&(E(ge),E(_e)),ye.current===e&&(E(ye),Qf._currentValue=fe)}var we,Te;function Ee(e){if(we===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);we=t&&t[1]||``,Te=-1{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,ee||(ee=!0,ae());else{var t=n(l);t!==null&&C(x,t.startTime-e)}}var ee=!1,te=-1,ne=5,re=-1;function ie(){return g?!0:!(e.unstable_now()-ret&&ie());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&C(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?ae():ee=!1}}}var ae;if(typeof y==`function`)ae=function(){y(S)};else if(typeof MessageChannel<`u`){var oe=new MessageChannel,se=oe.port2;oe.port1.onmessage=S,ae=function(){se.postMessage(null)}}else ae=function(){_(S,0)};function C(t,n){te=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(te),te=-1):h=!0,C(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,ee||(ee=!0,ae()))),r},e.unstable_shouldYield=ie,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),ne=n(((e,t)=>{t.exports=te()})),re=n((e=>{var t=ne(),n=u(),r=ee();function i(e){var t=`https://react.dev/errors/`+e;if(1me||(e.current=pe[me],pe[me]=null,me--)}function D(e,t){me++,pe[me]=e.current,e.current=t}var ge=he(null),_e=he(null),ve=he(null),ye=he(null);function be(e,t){switch(D(ve,t),D(_e,e),D(ge,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}E(ge),D(ge,e)}function xe(){E(ge),E(_e),E(ve)}function Se(e){e.memoizedState!==null&&D(ye,e);var t=ge.current,n=Hd(t,e.type);t!==n&&(D(_e,e),D(ge,n))}function Ce(e){_e.current===e&&(E(ge),E(_e)),ye.current===e&&(E(ye),Qf._currentValue=fe)}var we,Te;function Ee(e){if(we===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);we=t&&t[1]||``,Te=-1)`:-1i||c[r]!==l[i]){var u=` `+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{De=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?Ee(n):``}function ke(e,t){switch(e.tag){case 26:case 27:case 5:return Ee(e.type);case 16:return Ee(`Lazy`);case 13:return e.child!==t&&t!==null?Ee(`Suspense Fallback`):Ee(`Suspense`);case 19:return Ee(`SuspenseList`);case 0:case 15:return Oe(e.type,!1);case 11:return Oe(e.type.render,!1);case 1:return Oe(e.type,!0);case 31:return Ee(`Activity`);default:return``}}function Ae(e){try{var t=``,n=null;do t+=ke(e,n),n=e,e=e.return;while(e);return t}catch(e){return` Error generating stack: `+e.message+` -`+e.stack}}var je=Object.prototype.hasOwnProperty,Me=n.unstable_scheduleCallback,Ne=n.unstable_cancelCallback,Pe=n.unstable_shouldYield,Fe=n.unstable_requestPaint,Ie=n.unstable_now,Le=n.unstable_getCurrentPriorityLevel,Re=n.unstable_ImmediatePriority,ze=n.unstable_UserBlockingPriority,Be=n.unstable_NormalPriority,Ve=n.unstable_LowPriority,He=n.unstable_IdlePriority,Ue=n.log,We=n.unstable_setDisableYieldValue,Ge=null,Ke=null;function qe(e){if(typeof Ue==`function`&&We(e),Ke&&typeof Ke.setStrictMode==`function`)try{Ke.setStrictMode(Ge,e)}catch{}}var Je=Math.clz32?Math.clz32:Ze,Ye=Math.log,Xe=Math.LN2;function Ze(e){return e>>>=0,e===0?32:31-(Ye(e)/Xe|0)|0}var Qe=256,$e=262144,et=4194304;function tt(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function nt(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=tt(n))):i=tt(o):i=tt(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=tt(n))):i=tt(o)):i=tt(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function rt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function it(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function at(){var e=et;return et<<=1,!(et&62914560)&&(et=4194304),e}function ot(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function st(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function ct(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),bn=!1;if(yn)try{var xn={};Object.defineProperty(xn,"passive",{get:function(){bn=!0}}),window.addEventListener(`test`,xn,xn),window.removeEventListener(`test`,xn,xn)}catch{bn=!1}var Sn=null,Cn=null,wn=null;function Tn(){if(wn)return wn;var e,t=Cn,n=t.length,r,i=`value`in Sn?Sn.value:Sn.textContent,a=i.length;for(e=0;e=rr),or=` `,sr=!1;function cr(e,t){switch(e){case`keyup`:return tr.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function lr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var ur=!1;function dr(e,t){switch(e){case`compositionend`:return lr(t);case`keypress`:return t.which===32?(sr=!0,or):null;case`textInput`:return e=t.data,e===or&&sr?null:e;default:return null}}function fr(e,t){if(ur)return e===`compositionend`||!nr&&cr(e,t)?(e=Tn(),wn=Cn=Sn=null,ur=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Pr(n)}}function Ir(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ir(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Lr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Kt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Kt(e.document)}return t}function Rr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var zr=yn&&`documentMode`in document&&11>=document.documentMode,Br=null,Vr=null,Hr=null,Ur=!1;function Wr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ur||Br==null||Br!==Kt(r)||(r=Br,`selectionStart`in r&&Rr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Hr&&Nr(Hr,r)||(Hr=r,r=Ed(Vr,`onSelect`),0>=o,i-=o,Ii=1<<32-Je(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),j&&Ri(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(i,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(i,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(i,h),j&&Ri(i,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(i,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return j&&Ri(i,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,i,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(i,e)}),j&&Ri(i,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===_&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case h:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===_){if(r.tag===7){n(e,r.sibling),c=i(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===ae&&Fa(l)===r.type){n(e,r.sibling),c=i(r,o.props),Ha(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===_?(c=Ci(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=Si(o.type,o.key,o.props,null,e.mode,c),Ha(c,o),c.return=e,e=c)}return s(e);case g:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=i(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=Ei(o,e.mode,c),c.return=e,e=c}return s(e);case ae:return o=Fa(o),b(e,r,o,c)}if(de(o))return v(e,r,o,c);if(ce(o)){if(l=ce(o),typeof l!=`function`)throw Error(a(150));return o=l.call(o),y(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Va(o),c);if(o.$$typeof===x)return b(e,r,ua(e,o),c);Ua(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=i(r,o),c.return=e,e=c):(n(e,r),c=wi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Ba=0;var i=b(e,t,n,r);return za=null,i}catch(t){if(t===ka||t===ja)throw t;var a=vi(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ga=Wa(!0),Ka=Wa(!1),qa=!1;function Ja(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ya(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Xa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Za(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,W&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=hi(e),mi(e,null,n),t}return di(e,r,t,n),hi(e)}function Qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ut(e,n)}}function $a(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var eo=!1;function to(){if(eo){var e=ba;if(e!==null)throw e}}function no(e,t,n,r){eo=!1;var i=e.updateQueue;qa=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,m=f!==s.lane;if(m?(q&f)===f:(r&f)===f){f!==0&&f===ya&&(eo=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=p({},d,f);break a;case 2:qa=!0}}f=s.callback,f!==null&&(e.flags|=64,m&&(e.flags|=8192),m=i.callbacks,m===null?i.callbacks=[f]:m.push(f))}else m={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=m,c=d):u=u.next=m,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;m=s,s=m.next,m.next=null,i.lastBaseUpdate=m,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Kl|=o,e.lanes=o,e.memoizedState=d}}function ro(e,t){if(typeof e!=`function`)throw Error(a(191,e));e.call(t)}function io(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=w.T,s={};w.T=s,zs(e,!1,t,n);try{var c=i(),l=w.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Rs(e,t,Ca(c,r),pu(e)):Rs(e,t,r,pu(e))}catch(n){Rs(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{T.p=a,o!==null&&s.types!==null&&(o.types=s.types),w.T=o}}function Os(){}function ks(e,t,n,r){if(e.tag!==5)throw Error(a(476));var i=As(e).queue;Ds(e,i,t,fe,n===null?Os:function(){return js(e),n(r)})}function As(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:fe,baseState:fe,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Bo,lastRenderedState:fe},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Bo,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function js(e){var t=As(e);t.next===null&&(t=e.alternate.memoizedState),Rs(e,t.next.queue,{},pu())}function Ms(){return la(Qf)}function Ns(){return R().memoizedState}function Ps(){return R().memoizedState}function Fs(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Xa(n);var r=Za(t,e,n);r!==null&&(hu(r,t,n),Qa(r,t,n)),t={cache:ha()},e.payload=t;return}t=t.return}}function Is(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Bs(e)?Vs(t,n):(n=fi(e,t,n,r),n!==null&&(hu(n,e,r),Hs(n,t,r)))}function Ls(e,t,n){Rs(e,t,n,pu())}function Rs(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Bs(e))Vs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Mr(s,o))return di(e,t,i,0),G===null&&ui(),!1}catch{}if(n=fi(e,t,i,r),n!==null)return hu(n,e,r),Hs(n,t,r),!0}return!1}function zs(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Bs(e)){if(t)throw Error(a(479))}else t=fi(e,n,r,2),t!==null&&hu(t,e,2)}function Bs(e){var t=e.alternate;return e===P||t!==null&&t===P}function Vs(e,t){xo=bo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Hs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ut(e,n)}}var Us={readContext:la,use:Ro,useCallback:L,useContext:L,useEffect:L,useImperativeHandle:L,useLayoutEffect:L,useInsertionEffect:L,useMemo:L,useReducer:L,useRef:L,useState:L,useDebugValue:L,useDeferredValue:L,useTransition:L,useSyncExternalStore:L,useId:L,useHostTransitionStatus:L,useFormState:L,useActionState:L,useOptimistic:L,useMemoCache:L,useCacheRefresh:L};Us.useEffectEvent=L;var Ws={readContext:la,use:Ro,useCallback:function(e,t){return Fo().memoizedState=[e,t===void 0?null:t],e},useContext:la,useEffect:ms,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),fs(4194308,4,bs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return fs(4194308,4,e,t)},useInsertionEffect:function(e,t){fs(4,2,e,t)},useMemo:function(e,t){var n=Fo();t=t===void 0?null:t;var r=e();if(So){qe(!0);try{e()}finally{qe(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Fo();if(n!==void 0){var i=n(t);if(So){qe(!0);try{n(t)}finally{qe(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Is.bind(null,P,e),[r.memoizedState,e]},useRef:function(e){var t=Fo();return e={current:e},t.memoizedState=e},useState:function(e){e=Xo(e);var t=e.queue,n=Ls.bind(null,P,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:Ss,useDeferredValue:function(e,t){return Ts(Fo(),e,t)},useTransition:function(){var e=Xo(!1);return e=Ds.bind(null,P,e.queue,!0,!1),Fo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=P,i=Fo();if(j){if(n===void 0)throw Error(a(407));n=n()}else{if(n=t(),G===null)throw Error(a(349));q&127||Go(r,t,n)}i.memoizedState=n;var o={value:n,getSnapshot:t};return i.queue=o,ms(qo.bind(null,r,o,e),[e]),r.flags|=2048,us(9,{destroy:void 0},Ko.bind(null,r,o,n,t),null),n},useId:function(){var e=Fo(),t=G.identifierPrefix;if(j){var n=Li,r=Ii;n=(r&~(1<<32-Je(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=Co++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(i,{is:r.is}):s.createElement(i)}}o[_t]=t,o[vt]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,i,r),i){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Lc(t)}}return B(t),Rc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Lc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(a(166));if(e=ve.current,Yi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,i=k,i!==null)switch(i.tag){case 27:case 5:r=i.memoizedProps}e[_t]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Ki(t,!0)}else e=Bd(e).createTextNode(r),e[_t]=t,t.stateNode=e}return B(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Yi(t),n!==null){if(e===null){if(!r)throw Error(a(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(a(557));e[_t]=t}else Xi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;B(t),e=!1}else n=Zi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(_o(t),t):(_o(t),null);if(t.flags&128)throw Error(a(558))}return B(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(i=Yi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!i)throw Error(a(318));if(i=t.memoizedState,i=i===null?null:i.dehydrated,!i)throw Error(a(317));i[_t]=t}else Xi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;B(t),i=!1}else i=Zi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=i),i=!0;if(!i)return t.flags&256?(_o(t),t):(_o(t),null)}return _o(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,i=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(i=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==i&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Bc(t,t.updateQueue),B(t),null);case 4:return xe(),e===null&&Sd(t.stateNode.containerInfo),B(t),null;case 10:return ra(t.type),B(t),null;case 19:if(E(N),r=t.memoizedState,r===null)return B(t),null;if(i=(t.flags&128)!=0,o=r.rendering,o===null)if(i)Vc(r,!1);else{if(Y!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=vo(e),o!==null){for(t.flags|=128,Vc(r,!1),e=o.updateQueue,t.updateQueue=e,Bc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)xi(n,e),n=n.sibling;return D(N,N.current&1|2),j&&Ri(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Ie()>nu&&(t.flags|=128,i=!0,Vc(r,!1),t.lanes=4194304)}else{if(!i)if(e=vo(o),e!==null){if(t.flags|=128,i=!0,e=e.updateQueue,t.updateQueue=e,Bc(t,e),Vc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!j)return B(t),null}else 2*Ie()-r.renderingStartTime>nu&&n!==536870912&&(t.flags|=128,i=!0,Vc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(B(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Ie(),e.sibling=null,n=N.current,D(N,i?n&1|2:n&1),j&&Ri(t,r.treeForkCount),e);case 22:case 23:return _o(t),lo(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(B(t),t.subtreeFlags&6&&(t.flags|=8192)):B(t),n=t.updateQueue,n!==null&&Bc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&E(Ta),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),ra(M),B(t),null;case 25:return null;case 30:return null}throw Error(a(156,t.tag))}function Uc(e,t){switch(Vi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ra(M),xe(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Ce(t),null;case 31:if(t.memoizedState!==null){if(_o(t),t.alternate===null)throw Error(a(340));Xi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(_o(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(a(340));Xi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return E(N),null;case 4:return xe(),null;case 10:return ra(t.type),null;case 22:case 23:return _o(t),lo(),e!==null&&E(Ta),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return ra(M),null;case 25:return null;default:return null}}function Wc(e,t){switch(Vi(t),t.tag){case 3:ra(M),xe();break;case 26:case 27:case 5:Ce(t);break;case 4:xe();break;case 31:t.memoizedState!==null&&_o(t);break;case 13:_o(t);break;case 19:E(N);break;case 10:ra(t.type);break;case 22:case 23:_o(t),lo(),e!==null&&E(Ta);break;case 24:ra(M)}}function Gc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Kc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function qc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{io(t,n)}catch(t){Z(e,e.return,t)}}}function Jc(e,t,n){n.props=Zs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Yc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function Xc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Zc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Qc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[vt]=t}catch(t){Z(e,e.return,t)}}function $c(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function el(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||$c(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function tl(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=un));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(tl(e,t,n),e=e.sibling;e!==null;)tl(e,t,n),e=e.sibling}function nl(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(nl(e,t,n),e=e.sibling;e!==null;)nl(e,t,n),e=e.sibling}function rl(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[_t]=e,t[vt]=n}catch(t){Z(e,e.return,t)}}var il=!1,V=!1,al=!1,ol=typeof WeakSet==`function`?WeakSet:Set,H=null;function sl(e,t){if(e=e.containerInfo,Rd=sp,e=Lr(e),Rr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var i=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||i!==0&&f.nodeType!==3||(c=s+i),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===i&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,H=t;H!==null;)if(t=H,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,H=e;else for(;H!==null;){switch(t=H,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[_t]=e,O(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,i).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Fr(s,h),v=Fr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,w.T=null,n=lu,lu=null;var o=au,s=su;if(X=0,ou=au=null,su=0,W&6)throw Error(a(331));var c=W;if(W|=4,Il(o.current),Ol(o,o.current,s,n),W=c,id(0,!1),Ke&&typeof Ke.onPostCommitFiberRoot==`function`)try{Ke.onPostCommitFiberRoot(Ge,o)}catch{}return!0}finally{T.p=i,w.T=r,Vu(e,t)}}function Wu(e,t,n){t=Oi(n,t),t=rc(e.stateNode,t,2),e=Za(e,t,2),e!==null&&(st(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(iu===null||!iu.has(r))){e=Oi(n,e),n=ic(2),r=Za(t,n,2),r!==null&&(ac(n,r,t,e),st(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Bl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Wl=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,G===e&&(q&n)===n&&(Y===4||Y===3&&(q&62914560)===q&&300>Ie()-eu?!(W&2)&&Su(e,0):Jl|=n,Xl===q&&(Xl=0)),rd(e)}function qu(e,t){t===0&&(t=at()),e=pi(e,t),e!==null&&(st(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,i=e.memoizedState;i!==null&&(n=i.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(a(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return Me(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Je(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=q,a=nt(r,r===G?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||rt(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Ie(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}X!==0&&X!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Jt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),O(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Jt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Jt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Jt(n.imageSizes)+`"]`)):i+=`[href="`+Jt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=p({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),O(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Jt(r)+`"][href="`+Jt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=p({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),O(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=kt(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=p({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);O(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=kt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=p({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),O(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=kt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=p({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),O(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var i=(i=ve.current)?gf(i):null;if(!i)throw Error(a(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=kt(i).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=kt(i).hoistableStyles,s=o.get(e);if(s||(i=i.ownerDocument||i,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=i.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(i,e,n,s.state))),t&&r===null)throw Error(a(528,``));return s}if(t&&r!==null)throw Error(a(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=kt(i).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(a(444,e))}}function Af(e){return`href="`+Jt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return p({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),O(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Jt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Jt(n.href)+`"]`);if(r)return t.instance=r,O(r),r;var i=p({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),O(r),Pd(r,`style`,i),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:i=Af(n.href);var o=e.querySelector(jf(i));if(o)return t.state.loading|=4,t.instance=o,O(o),o;r=Mf(n),(i=mf.get(i))&&Rf(r,i),o=(e.ownerDocument||e).createElement(`link`),O(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(i=e.querySelector(Ff(o)))?(t.instance=i,O(i),i):(r=n,(i=mf.get(o))&&(r=p({},n),zf(r,i)),e=e.ownerDocument||e,i=e.createElement(`script`),O(i),Pd(i,`link`,r),e.head.appendChild(i),t.instance=i);case`void`:return null;default:throw Error(a(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,O(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),O(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=re()}))(),S=r(e(),1);[...m],new TextEncoder,S.Component;function ae({state:e,routes:t,getRouteInfo:r,location:i,basename:o,isSpaMode:s}){let c={...e,loaderData:{...e.loaderData}},l=a(t,i,o);if(l)for(let e of l){let t=e.route.id,i=r(t);n(t,i.clientLoader,i.hasLoader,s)&&(i.hasHydrateFallback||!i.hasLoader)?delete c.loaderData[t]:i.hasLoader||(c.loaderData[t]=null)}return c}var oe=r(ee(),1);function se(e){return S.createElement(_,{flushSync:oe.flushSync,...e})}var C=null,ce=null;function le(){if(!C&&window.__reactRouterContext&&window.__reactRouterManifest&&window.__reactRouterRouteModules){if(window.__reactRouterManifest.sri===!0){let e=document.querySelector(`script[rr-importmap]`);if(e?.textContent)try{window.__reactRouterManifest.sri=JSON.parse(e.textContent).integrity}catch(e){console.error(`Failed to parse import map`,e)}}C={context:window.__reactRouterContext,manifest:window.__reactRouterManifest,routeModules:window.__reactRouterRouteModules,stateDecodingPromise:void 0,router:void 0,routerInitialized:!1}}}function ue({getContext:e,instrumentations:t}){if(le(),!C)throw Error("You must be using the SSR features of React Router in order to skip passing a `router` prop to ``");let n=C;if(!C.stateDecodingPromise){let e=C.context.stream;i(e,`No stream found for single fetch decoding`),C.context.stream=void 0,C.stateDecodingPromise=v(e,window).then(e=>{C.context.state=e.value,n.stateDecodingPromise.value=!0}).catch(e=>{n.stateDecodingPromise.error=e})}if(C.stateDecodingPromise.error)throw C.stateDecodingPromise.error;if(!C.stateDecodingPromise.value)throw C.stateDecodingPromise;let r=f(C.manifest.routes,C.routeModules,C.context.state,C.context.ssr,C.context.isSpaMode),a;if(C.context.isSpaMode){let{loaderData:e}=C.context.state;C.manifest.routes.root?.hasLoader&&e&&`root`in e&&(a={loaderData:{root:e.root}})}else a=ae({state:C.context.state,routes:r,getRouteInfo:e=>({clientLoader:C.routeModules[e]?.clientLoader,hasLoader:C.manifest.routes[e]?.hasLoader===!0,hasHydrateFallback:C.routeModules[e]?.HydrateFallback!=null}),location:window.location,basename:window.__reactRouterContext?.basename,isSpaMode:C.context.isSpaMode});window.history.state&&window.history.state.masked&&window.history.replaceState({...window.history.state,masked:void 0},``);let c=o({routes:r,history:h(),basename:C.context.basename,getContext:e,hydrationData:a,hydrationRouteProperties:b,instrumentations:t,mapRouteProperties:y,future:{v8_passThroughRequests:C.context.future.v8_passThroughRequests},dataStrategy:s(()=>c,C.manifest,C.routeModules,C.context.ssr,C.context.basename,C.context.future.v8_trailingSlashAwareDataRequests),patchRoutesOnNavigation:x(()=>c,C.manifest,C.routeModules,C.context.ssr,C.context.routeDiscovery,C.context.isSpaMode,C.context.basename)});return C.router=c,c.state.initialized&&(C.routerInitialized=!0,c.initialize()),c.createRoutesForHMR=l,window.__reactRouterDataRouter=c,c}function de(e){ce||=ue({getContext:e.getContext,instrumentations:e.instrumentations});let[n,r]=S.useState(void 0);S.useEffect(()=>{},[]),S.useEffect(()=>{},[n]);let[a,o]=S.useState(ce.state.location);return S.useLayoutEffect(()=>{C&&C.router&&!C.routerInitialized&&(C.routerInitialized=!0,C.router.initialize())},[]),S.useLayoutEffect(()=>{if(C&&C.router)return C.router.subscribe(e=>{e.location!==a&&o(e.location)})},[a]),i(C,`ssrInfo unavailable for HydratedRouter`),t(ce,C.manifest,C.routeModules,C.context.ssr,C.context.routeDiscovery,C.context.isSpaMode),S.createElement(S.Fragment,null,S.createElement(p.Provider,{value:{manifest:C.manifest,routeModules:C.routeModules,future:C.context.future,criticalCss:n,ssr:C.context.ssr,isSpaMode:C.context.isSpaMode,routeDiscovery:C.context.routeDiscovery}},S.createElement(d,{location:a},S.createElement(se,{router:ce,useTransitions:e.useTransitions,onError:e.onError}))),S.createElement(S.Fragment,null))}c();var w=g();(0,S.startTransition)(()=>{(0,ie.hydrateRoot)(document,(0,w.jsx)(S.StrictMode,{children:(0,w.jsx)(de,{})}))}); \ No newline at end of file +`+e.stack}}var je=Object.prototype.hasOwnProperty,Me=t.unstable_scheduleCallback,Ne=t.unstable_cancelCallback,Pe=t.unstable_shouldYield,Fe=t.unstable_requestPaint,Ie=t.unstable_now,Le=t.unstable_getCurrentPriorityLevel,Re=t.unstable_ImmediatePriority,ze=t.unstable_UserBlockingPriority,Be=t.unstable_NormalPriority,Ve=t.unstable_LowPriority,He=t.unstable_IdlePriority,Ue=t.log,We=t.unstable_setDisableYieldValue,Ge=null,Ke=null;function qe(e){if(typeof Ue==`function`&&We(e),Ke&&typeof Ke.setStrictMode==`function`)try{Ke.setStrictMode(Ge,e)}catch{}}var Je=Math.clz32?Math.clz32:Ze,Ye=Math.log,Xe=Math.LN2;function Ze(e){return e>>>=0,e===0?32:31-(Ye(e)/Xe|0)|0}var Qe=256,$e=262144,et=4194304;function tt(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function nt(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=tt(n))):i=tt(o):i=tt(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=tt(n))):i=tt(o)):i=tt(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function rt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function it(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function at(){var e=et;return et<<=1,!(et&62914560)&&(et=4194304),e}function ot(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function st(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function ct(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),bn=!1;if(yn)try{var xn={};Object.defineProperty(xn,"passive",{get:function(){bn=!0}}),window.addEventListener(`test`,xn,xn),window.removeEventListener(`test`,xn,xn)}catch{bn=!1}var Sn=null,Cn=null,wn=null;function Tn(){if(wn)return wn;var e,t=Cn,n=t.length,r,i=`value`in Sn?Sn.value:Sn.textContent,a=i.length;for(e=0;e=rr),or=` `,sr=!1;function cr(e,t){switch(e){case`keyup`:return tr.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function lr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var ur=!1;function dr(e,t){switch(e){case`compositionend`:return lr(t);case`keypress`:return t.which===32?(sr=!0,or):null;case`textInput`:return e=t.data,e===or&&sr?null:e;default:return null}}function fr(e,t){if(ur)return e===`compositionend`||!nr&&cr(e,t)?(e=Tn(),wn=Cn=Sn=null,ur=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Pr(n)}}function Ir(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ir(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Lr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Kt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Kt(e.document)}return t}function Rr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var zr=yn&&`documentMode`in document&&11>=document.documentMode,Br=null,Vr=null,Hr=null,Ur=!1;function Wr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ur||Br==null||Br!==Kt(r)||(r=Br,`selectionStart`in r&&Rr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Hr&&Nr(Hr,r)||(Hr=r,r=Ed(Vr,`onSelect`),0>=o,i-=o,Ii=1<<32-Je(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),j&&Ri(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),j&&Ri(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return j&&Ri(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),j&&Ri(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===_&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case h:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===_){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===ae&&Fa(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ha(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===_?(c=Ci(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=Si(o.type,o.key,o.props,null,e.mode,c),Ha(c,o),c.return=e,e=c)}return s(e);case g:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=Ei(o,e.mode,c),c.return=e,e=c}return s(e);case ae:return o=Fa(o),b(e,r,o,c)}if(de(o))return v(e,r,o,c);if(ce(o)){if(l=ce(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),y(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Va(o),c);if(o.$$typeof===x)return b(e,r,ua(e,o),c);Ua(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=wi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Ba=0;var i=b(e,t,n,r);return za=null,i}catch(t){if(t===ka||t===ja)throw t;var a=vi(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ga=Wa(!0),Ka=Wa(!1),qa=!1;function Ja(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ya(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Xa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Za(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,W&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=hi(e),mi(e,null,n),t}return di(e,r,t,n),hi(e)}function Qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ut(e,n)}}function $a(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var eo=!1;function to(){if(eo){var e=ba;if(e!==null)throw e}}function no(e,t,n,r){eo=!1;var i=e.updateQueue;qa=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,m=f!==s.lane;if(m?(q&f)===f:(r&f)===f){f!==0&&f===ya&&(eo=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=p({},d,f);break a;case 2:qa=!0}}f=s.callback,f!==null&&(e.flags|=64,m&&(e.flags|=8192),m=i.callbacks,m===null?i.callbacks=[f]:m.push(f))}else m={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=m,c=d):u=u.next=m,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;m=s,s=m.next,m.next=null,i.lastBaseUpdate=m,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Kl|=o,e.lanes=o,e.memoizedState=d}}function ro(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function io(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=w.T,s={};w.T=s,zs(e,!1,t,n);try{var c=i(),l=w.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Rs(e,t,Ca(c,r),pu(e)):Rs(e,t,r,pu(e))}catch(n){Rs(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{T.p=a,o!==null&&s.types!==null&&(o.types=s.types),w.T=o}}function Os(){}function ks(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=As(e).queue;Ds(e,a,t,fe,n===null?Os:function(){return js(e),n(r)})}function As(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:fe,baseState:fe,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Bo,lastRenderedState:fe},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Bo,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function js(e){var t=As(e);t.next===null&&(t=e.alternate.memoizedState),Rs(e,t.next.queue,{},pu())}function Ms(){return la(Qf)}function Ns(){return R().memoizedState}function Ps(){return R().memoizedState}function Fs(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Xa(n);var r=Za(t,e,n);r!==null&&(hu(r,t,n),Qa(r,t,n)),t={cache:ha()},e.payload=t;return}t=t.return}}function Is(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Bs(e)?Vs(t,n):(n=fi(e,t,n,r),n!==null&&(hu(n,e,r),Hs(n,t,r)))}function Ls(e,t,n){Rs(e,t,n,pu())}function Rs(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Bs(e))Vs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Mr(s,o))return di(e,t,i,0),G===null&&ui(),!1}catch{}if(n=fi(e,t,i,r),n!==null)return hu(n,e,r),Hs(n,t,r),!0}return!1}function zs(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Bs(e)){if(t)throw Error(i(479))}else t=fi(e,n,r,2),t!==null&&hu(t,e,2)}function Bs(e){var t=e.alternate;return e===P||t!==null&&t===P}function Vs(e,t){xo=bo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Hs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ut(e,n)}}var Us={readContext:la,use:Ro,useCallback:L,useContext:L,useEffect:L,useImperativeHandle:L,useLayoutEffect:L,useInsertionEffect:L,useMemo:L,useReducer:L,useRef:L,useState:L,useDebugValue:L,useDeferredValue:L,useTransition:L,useSyncExternalStore:L,useId:L,useHostTransitionStatus:L,useFormState:L,useActionState:L,useOptimistic:L,useMemoCache:L,useCacheRefresh:L};Us.useEffectEvent=L;var Ws={readContext:la,use:Ro,useCallback:function(e,t){return Fo().memoizedState=[e,t===void 0?null:t],e},useContext:la,useEffect:ms,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),fs(4194308,4,bs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return fs(4194308,4,e,t)},useInsertionEffect:function(e,t){fs(4,2,e,t)},useMemo:function(e,t){var n=Fo();t=t===void 0?null:t;var r=e();if(So){qe(!0);try{e()}finally{qe(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Fo();if(n!==void 0){var i=n(t);if(So){qe(!0);try{n(t)}finally{qe(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Is.bind(null,P,e),[r.memoizedState,e]},useRef:function(e){var t=Fo();return e={current:e},t.memoizedState=e},useState:function(e){e=Xo(e);var t=e.queue,n=Ls.bind(null,P,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:Ss,useDeferredValue:function(e,t){return Ts(Fo(),e,t)},useTransition:function(){var e=Xo(!1);return e=Ds.bind(null,P,e.queue,!0,!1),Fo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=P,a=Fo();if(j){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),G===null)throw Error(i(349));q&127||Go(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,ms(qo.bind(null,r,o,e),[e]),r.flags|=2048,us(9,{destroy:void 0},Ko.bind(null,r,o,n,t),null),n},useId:function(){var e=Fo(),t=G.identifierPrefix;if(j){var n=Li,r=Ii;n=(r&~(1<<32-Je(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=Co++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[_t]=t,o[vt]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Lc(t)}}return B(t),Rc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Lc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=ve.current,Yi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=k,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[_t]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Ki(t,!0)}else e=Bd(e).createTextNode(r),e[_t]=t,t.stateNode=e}return B(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Yi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[_t]=t}else Xi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;B(t),e=!1}else n=Zi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(_o(t),t):(_o(t),null);if(t.flags&128)throw Error(i(558))}return B(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Yi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[_t]=t}else Xi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;B(t),a=!1}else a=Zi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(_o(t),t):(_o(t),null)}return _o(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Bc(t,t.updateQueue),B(t),null);case 4:return xe(),e===null&&Sd(t.stateNode.containerInfo),B(t),null;case 10:return ra(t.type),B(t),null;case 19:if(E(N),r=t.memoizedState,r===null)return B(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Vc(r,!1);else{if(Y!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=vo(e),o!==null){for(t.flags|=128,Vc(r,!1),e=o.updateQueue,t.updateQueue=e,Bc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)xi(n,e),n=n.sibling;return D(N,N.current&1|2),j&&Ri(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Ie()>nu&&(t.flags|=128,a=!0,Vc(r,!1),t.lanes=4194304)}else{if(!a)if(e=vo(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Bc(t,e),Vc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!j)return B(t),null}else 2*Ie()-r.renderingStartTime>nu&&n!==536870912&&(t.flags|=128,a=!0,Vc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(B(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Ie(),e.sibling=null,n=N.current,D(N,a?n&1|2:n&1),j&&Ri(t,r.treeForkCount),e);case 22:case 23:return _o(t),lo(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(B(t),t.subtreeFlags&6&&(t.flags|=8192)):B(t),n=t.updateQueue,n!==null&&Bc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&E(Ta),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),ra(M),B(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Uc(e,t){switch(Vi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ra(M),xe(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Ce(t),null;case 31:if(t.memoizedState!==null){if(_o(t),t.alternate===null)throw Error(i(340));Xi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(_o(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Xi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return E(N),null;case 4:return xe(),null;case 10:return ra(t.type),null;case 22:case 23:return _o(t),lo(),e!==null&&E(Ta),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return ra(M),null;case 25:return null;default:return null}}function Wc(e,t){switch(Vi(t),t.tag){case 3:ra(M),xe();break;case 26:case 27:case 5:Ce(t);break;case 4:xe();break;case 31:t.memoizedState!==null&&_o(t);break;case 13:_o(t);break;case 19:E(N);break;case 10:ra(t.type);break;case 22:case 23:_o(t),lo(),e!==null&&E(Ta);break;case 24:ra(M)}}function Gc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Kc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function qc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{io(t,n)}catch(t){Z(e,e.return,t)}}}function Jc(e,t,n){n.props=Zs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Yc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function Xc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Zc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Qc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[vt]=t}catch(t){Z(e,e.return,t)}}function $c(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function el(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||$c(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function tl(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=un));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(tl(e,t,n),e=e.sibling;e!==null;)tl(e,t,n),e=e.sibling}function nl(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(nl(e,t,n),e=e.sibling;e!==null;)nl(e,t,n),e=e.sibling}function rl(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[_t]=e,t[vt]=n}catch(t){Z(e,e.return,t)}}var il=!1,V=!1,al=!1,ol=typeof WeakSet==`function`?WeakSet:Set,H=null;function sl(e,t){if(e=e.containerInfo,Rd=sp,e=Lr(e),Rr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,H=t;H!==null;)if(t=H,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,H=e;else for(;H!==null;){switch(t=H,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[_t]=e,O(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Fr(s,h),v=Fr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,w.T=null,n=lu,lu=null;var o=au,s=su;if(X=0,ou=au=null,su=0,W&6)throw Error(i(331));var c=W;if(W|=4,Il(o.current),Ol(o,o.current,s,n),W=c,id(0,!1),Ke&&typeof Ke.onPostCommitFiberRoot==`function`)try{Ke.onPostCommitFiberRoot(Ge,o)}catch{}return!0}finally{T.p=a,w.T=r,Vu(e,t)}}function Wu(e,t,n){t=Oi(n,t),t=rc(e.stateNode,t,2),e=Za(e,t,2),e!==null&&(st(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(iu===null||!iu.has(r))){e=Oi(n,e),n=ic(2),r=Za(t,n,2),r!==null&&(ac(n,r,t,e),st(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Bl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Wl=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,G===e&&(q&n)===n&&(Y===4||Y===3&&(q&62914560)===q&&300>Ie()-eu?!(W&2)&&Su(e,0):Jl|=n,Xl===q&&(Xl=0)),rd(e)}function qu(e,t){t===0&&(t=at()),e=pi(e,t),e!==null&&(st(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return Me(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Je(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=q,a=nt(r,r===G?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||rt(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Ie(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}X!==0&&X!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Jt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),O(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Jt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Jt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Jt(n.imageSizes)+`"]`)):i+=`[href="`+Jt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=p({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),O(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Jt(r)+`"][href="`+Jt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=p({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),O(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=kt(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=p({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);O(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=kt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=p({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),O(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=kt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=p({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),O(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=ve.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=kt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=kt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=kt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Jt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return p({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),O(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Jt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Jt(n.href)+`"]`);if(r)return t.instance=r,O(r),r;var a=p({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),O(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,O(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),O(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,O(a),a):(r=n,(a=mf.get(o))&&(r=p({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),O(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,O(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),O(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=re()}))(),S=r(u(),1);[...m],new TextEncoder,S.Component;function ae({state:e,routes:n,getRouteInfo:r,location:i,basename:o,isSpaMode:s}){let c={...e,loaderData:{...e.loaderData}},l=a(n,i,o);if(l)for(let e of l){let n=e.route.id,i=r(n);t(n,i.clientLoader,i.hasLoader,s)&&(i.hasHydrateFallback||!i.hasLoader)?delete c.loaderData[n]:i.hasLoader||(c.loaderData[n]=null)}return c}var oe=r(ee(),1);function se(e){return S.createElement(_,{flushSync:oe.flushSync,...e})}var C=null,ce=null;function le(){if(!C&&window.__reactRouterContext&&window.__reactRouterManifest&&window.__reactRouterRouteModules){if(window.__reactRouterManifest.sri===!0){let e=document.querySelector(`script[rr-importmap]`);if(e?.textContent)try{window.__reactRouterManifest.sri=JSON.parse(e.textContent).integrity}catch(e){console.error(`Failed to parse import map`,e)}}C={context:window.__reactRouterContext,manifest:window.__reactRouterManifest,routeModules:window.__reactRouterRouteModules,stateDecodingPromise:void 0,router:void 0,routerInitialized:!1}}}function ue({getContext:e,instrumentations:t}){if(le(),!C)throw Error("You must be using the SSR features of React Router in order to skip passing a `router` prop to ``");let n=C;if(!C.stateDecodingPromise){let e=C.context.stream;i(e,`No stream found for single fetch decoding`),C.context.stream=void 0,C.stateDecodingPromise=v(e,window).then(e=>{C.context.state=e.value,n.stateDecodingPromise.value=!0}).catch(e=>{n.stateDecodingPromise.error=e})}if(C.stateDecodingPromise.error)throw C.stateDecodingPromise.error;if(!C.stateDecodingPromise.value)throw C.stateDecodingPromise;let r=f(C.manifest.routes,C.routeModules,C.context.state,C.context.ssr,C.context.isSpaMode),a;if(C.context.isSpaMode){let{loaderData:e}=C.context.state;C.manifest.routes.root?.hasLoader&&e&&`root`in e&&(a={loaderData:{root:e.root}})}else a=ae({state:C.context.state,routes:r,getRouteInfo:e=>({clientLoader:C.routeModules[e]?.clientLoader,hasLoader:C.manifest.routes[e]?.hasLoader===!0,hasHydrateFallback:C.routeModules[e]?.HydrateFallback!=null}),location:window.location,basename:window.__reactRouterContext?.basename,isSpaMode:C.context.isSpaMode});window.history.state&&window.history.state.masked&&window.history.replaceState({...window.history.state,masked:void 0},``);let c=o({routes:r,history:h(),basename:C.context.basename,getContext:e,hydrationData:a,hydrationRouteProperties:b,instrumentations:t,mapRouteProperties:y,future:{v8_passThroughRequests:C.context.future.v8_passThroughRequests},dataStrategy:s(()=>c,C.manifest,C.routeModules,C.context.ssr,C.context.basename,C.context.future.v8_trailingSlashAwareDataRequests),patchRoutesOnNavigation:x(()=>c,C.manifest,C.routeModules,C.context.ssr,C.context.routeDiscovery,C.context.isSpaMode,C.context.basename)});return C.router=c,c.state.initialized&&(C.routerInitialized=!0,c.initialize()),c.createRoutesForHMR=l,window.__reactRouterDataRouter=c,c}function de(t){ce||=ue({getContext:t.getContext,instrumentations:t.instrumentations});let[n,r]=S.useState(void 0);S.useEffect(()=>{},[]),S.useEffect(()=>{},[n]);let[a,o]=S.useState(ce.state.location);return S.useLayoutEffect(()=>{C&&C.router&&!C.routerInitialized&&(C.routerInitialized=!0,C.router.initialize())},[]),S.useLayoutEffect(()=>{if(C&&C.router)return C.router.subscribe(e=>{e.location!==a&&o(e.location)})},[a]),i(C,`ssrInfo unavailable for HydratedRouter`),e(ce,C.manifest,C.routeModules,C.context.ssr,C.context.routeDiscovery,C.context.isSpaMode),S.createElement(S.Fragment,null,S.createElement(p.Provider,{value:{manifest:C.manifest,routeModules:C.routeModules,future:C.context.future,criticalCss:n,ssr:C.context.ssr,isSpaMode:C.context.isSpaMode,routeDiscovery:C.context.routeDiscovery}},S.createElement(d,{location:a},S.createElement(se,{router:ce,useTransitions:t.useTransitions,onError:t.onError}))),S.createElement(S.Fragment,null))}c();var w=g();(0,S.startTransition)(()=>{(0,ie.hydrateRoot)(document,(0,w.jsx)(S.StrictMode,{children:(0,w.jsx)(de,{})}))}); \ No newline at end of file diff --git a/internal/webassets/build/client/assets/history-CiwTCwWj.js b/internal/webassets/build/client/assets/history-CiwTCwWj.js new file mode 100644 index 0000000..a31c8f7 --- /dev/null +++ b/internal/webassets/build/client/assets/history-CiwTCwWj.js @@ -0,0 +1 @@ +import{N as e,j as t,k as n,t as r}from"./jsx-runtime-D0QO-s14.js";import{C as i,d as a,f as o,ot as s,u as c}from"./api-k_NcBvGr.js";import{i as l,r as u,t as d}from"./useUrlParams-DI9KFxsS.js";import{n as f,r as p,t as m}from"./badge-B492YG3Q.js";var h=e(t(),1),g=r(),_=[{value:`new_release`,label:`New release`},{value:`guest_feature`,label:`Guest feature`},{value:`deluxe_change`,label:`Deluxe change`}],v=`flex h-9 max-w-64 min-w-40 items-center justify-between gap-2 rounded-md border border-input bg-input/30 px-3 text-body text-foreground outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50`,y=`cursor-pointer px-3 py-1.5 text-body text-popover-foreground data-[highlighted=true]:bg-accent`;function b({label:e,options:t,value:n,onSelect:r,triggerLabel:i,triggerRef:a}){let[o,s]=(0,h.useState)(!1),[c,l]=(0,h.useState)(0),u=(0,h.useRef)(null),d=(0,h.useRef)(null),f=a??d,p=(0,h.useId)(),m=t.findIndex(e=>e.value===``?n===null:e.value===n),_=t[m===-1?0:m],b=e=>`${p}-option-${e}`,x=i??_?.label;(0,h.useEffect)(()=>{o&&document.getElementById(b(c))?.scrollIntoView?.({block:`nearest`})},[o,c]),(0,h.useEffect)(()=>{if(!o)return;function e(e){u.current?.contains(e.target)||s(!1)}return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[o]);function S(e){l(e<0?0:e),s(!0)}function C(e){let n=t[e];n&&(r(n.value===``?null:n.value),s(!1),f.current?.focus())}function w(e){if(!o){(e.key===`ArrowDown`||e.key===`ArrowUp`||e.key===`Enter`||e.key===` `)&&(e.preventDefault(),S(m===-1?0:m));return}e.key===`ArrowDown`?(e.preventDefault(),l(e=>Math.min(e+1,t.length-1))):e.key===`ArrowUp`?(e.preventDefault(),l(e=>Math.max(e-1,0))):e.key===`Enter`||e.key===` `?(e.preventDefault(),C(c)):e.key===`Escape`&&(e.preventDefault(),s(!1),f.current?.focus())}return(0,g.jsxs)(`div`,{ref:u,className:`relative`,children:[(0,g.jsxs)(`button`,{ref:f,type:`button`,role:`combobox`,"aria-expanded":o,"aria-haspopup":`listbox`,"aria-controls":p,"aria-label":e,"aria-activedescendant":o?b(c):void 0,className:v,onClick:()=>o?s(!1):S(m===-1?0:m),onKeyDown:w,children:[(0,g.jsx)(`span`,{className:`truncate`,title:x,children:x}),(0,g.jsx)(`span`,{"aria-hidden":`true`,className:`text-muted-foreground`,children:`▾`})]}),o&&(0,g.jsx)(`ul`,{id:p,role:`listbox`,"aria-label":e,className:`absolute top-full left-0 z-50 mt-1 max-h-72 min-w-full overflow-y-auto rounded-md border border-input bg-popover text-popover-foreground shadow-md`,children:t.map((e,t)=>(0,g.jsx)(`li`,{id:b(t),role:`option`,"aria-selected":e.value===``?n===null:e.value===n,"data-highlighted":t===c,className:y,onMouseEnter:()=>l(t),onClick:()=>C(t),children:e.label},e.value===``?`__all__`:e.value))})]})}function x({value:e,onChange:t,tags:n,tagsStatus:r,tagTriggerRef:i,onArtistsLoaded:a}){let[s,c]=(0,h.useState)([]),[l,d]=(0,h.useState)(`loading`),f=(0,h.useRef)(a);f.current=a,(0,h.useEffect)(()=>{let e=!1;return o().then(t=>{e||(c(t),d(`loaded`),f.current?.(t))}).catch(()=>{e||(c([]),d(`error`))}),()=>{e=!0}},[]);let p=[{value:``,label:`All artists`},...s.map(e=>({value:String(e.artist_id),label:e.name}))],m;e.artistId!==null&&l===`loading`?m=`Loading artists…`:e.artistId!==null&&l===`error`&&(m=`Artist unavailable`);let v=[{value:``,label:`All event types`},..._],y;e.tagId!==null&&r===`loading`?y=`Loading tags…`:e.tagId!==null&&r===`error`&&(y=`Tag unavailable`);let x=[{value:``,label:`All tags`},...u(n).map(e=>({value:String(e.id),label:e.name}))];return(0,g.jsxs)(`div`,{className:`flex flex-wrap gap-4`,children:[(0,g.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Artist`,(0,g.jsx)(b,{label:`Artist`,options:p,value:e.artistId===null?null:String(e.artistId),triggerLabel:m,onSelect:n=>t({...e,artistId:n===null?null:Number(n)})})]}),(0,g.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Event type`,(0,g.jsx)(b,{label:`Event type`,options:v,value:e.eventType,onSelect:n=>t({...e,eventType:n})})]}),(0,g.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Tag`,(0,g.jsx)(b,{label:`Tag`,options:x,value:e.tagId===null?null:String(e.tagId),triggerLabel:y,triggerRef:i,onSelect:n=>t({...e,tagId:n===null?null:Number(n)})})]})]})}var S={new_release:{label:`New release`,emoji:`🆕`,color:`var(--color-event-new-release)`},guest_feature:{label:`Guest feature`,emoji:`🎤`,color:`var(--color-event-guest-feature)`},deluxe_change:{label:`Deluxe change`,emoji:`💿`,color:`var(--color-event-deluxe-change)`}},C={label:`Unknown`,emoji:`❔`,color:`var(--color-muted-foreground)`};function w({event:e}){let t=S[e.event_type]??C;return(0,g.jsxs)(`div`,{className:`flex gap-4 rounded-2xl bg-card p-4 ring-1 ring-foreground/10`,children:[(0,g.jsx)(l,{src:e.cover_art_url,alt:e.title,size:96,className:`rounded-md`}),(0,g.jsxs)(`div`,{className:`flex min-w-0 flex-1 flex-col gap-2`,children:[(0,g.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[(0,g.jsx)(`h3`,{className:`line-clamp-2 text-heading font-semibold text-foreground`,title:e.title,children:e.title}),(0,g.jsxs)(m,{variant:`secondary`,className:`shrink-0 gap-1 text-background`,style:{backgroundColor:t.color},children:[(0,g.jsx)(`span`,{"aria-hidden":`true`,children:t.emoji}),t.label]})]}),(0,g.jsx)(`p`,{className:`text-body text-muted-foreground`,children:e.artist_name}),(0,g.jsx)(T,{event:e})]})]})}function T({event:e}){switch(e.event_type){case`new_release`:return(0,g.jsx)(E,{event:e});case`guest_feature`:return(0,g.jsx)(k,{event:e});case`deluxe_change`:return(0,g.jsx)(A,{event:e});default:return null}}function E({event:e}){let t=e.release_date??`Release date unknown`;return(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[e.release_type?`${e.release_type} · `:``,t]})}function D(e){return e.source===`musicbrainz`?`https://musicbrainz.org/recording/${encodeURIComponent(e.external_id)}`:e.source===`deezer`?`https://www.deezer.com/track/${encodeURIComponent(e.external_id)}`:null}function O(e){let t=e.watched_artist_name;return!t||t===e.artist_name?null:t}function k({event:e}){let t=D(e),n=e.release_date??`Release date unknown`,r=O(e);return(0,g.jsxs)(g.Fragment,{children:[t?(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[`Featured on`,` `,(0,g.jsx)(`a`,{href:t,target:`_blank`,rel:`noreferrer`,className:`underline underline-offset-2 hover:text-foreground`,children:e.title}),r&&` — ${r} is on your watchlist`]}):(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[e.title,r&&` — ${r} is on your watchlist`]}),(0,g.jsx)(`p`,{className:`text-label text-muted-foreground`,children:n})]})}function A({event:e}){let t=e.release_date??`Release date unknown`,n=e.track_count??`?`;return e.previous_track_count==null?(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[t,` · `,n,` tracks`]}):(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[t,` · `,e.previous_track_count,` → `,n,` tracks`]})}var j=[`new_release`,`guest_feature`,`deluxe_change`],M=/^[1-9]\d*$/;function N(e){if(e===null||!M.test(e))return null;let t=Number(e);return Number.isSafeInteger(t)?t:null}function P(e){let t=e.get(`type`);return{artistId:N(e.get(`artist`)),eventType:j.find(e=>e===t)??null,tagId:N(e.get(`tag`))}}function F(e){let t=P(e),n=new URLSearchParams(e),r=[[`artist`,t.artistId===null?null:String(t.artistId)],[`type`,t.eventType],[`tag`,t.tagId===null?null:String(t.tagId)]];for(let[e,t]of r)t===null?n.delete(e):n.set(e,t);return n}function I(){return(0,g.jsxs)(`div`,{className:`flex gap-4 rounded-2xl bg-card p-4 ring-1 ring-foreground/10`,children:[(0,g.jsx)(f,{className:`h-24 w-24 shrink-0 rounded-md`}),(0,g.jsxs)(`div`,{className:`flex flex-1 flex-col gap-2 py-1`,children:[(0,g.jsx)(f,{className:`h-5 w-2/3`}),(0,g.jsx)(f,{className:`h-4 w-1/3`}),(0,g.jsx)(f,{className:`h-4 w-1/2`})]})]})}var L=3;function R(e,t,n){n===null?e.delete(t):e.set(t,String(n))}function z(e,t,n){return e?{heading:`Older than your retention window`,body:`There's release history for this view — it's just outside your retention window. Nothing was deleted.`}:t?{heading:`No events for “${t.name}”`,body:n?`Nothing matches “${t.name}” with the current artist and event-type filters.`:`None of the artists tagged “${t.name}” have release activity in your retention window.`}:n?{heading:`No matching events`,body:`Try a different artist or event type.`}:{heading:`No release activity yet`,body:`Add an artist to your watchlist to start tracking new releases, features, and deluxe editions.`}}function B(e,t){return c({artistId:e.artistId??void 0,eventType:e.eventType??void 0,tagId:e.tagId??void 0,cursor:t??void 0})}var V=n(function(){let{params:e,write:t}=d(),n=e.toString(),r=(0,h.useMemo)(()=>P(e),[n]),[o,c]=(0,h.useState)({status:`loading`,tags:[]}),[l,u]=(0,h.useState)([]),[f,m]=(0,h.useState)(null),[_,v]=(0,h.useState)(!1),[y,b]=(0,h.useState)(!0),[S,C]=(0,h.useState)(!1),[T,E]=(0,h.useState)(null),[D,O]=(0,h.useState)(null),[k,A]=(0,h.useState)(0),j=(0,h.useRef)(null),M=(0,h.useRef)(!1),N=(0,h.useRef)(!1),V=(0,h.useRef)(r);V.current=r,(0,h.useEffect)(()=>{let r=F(e);r.toString()!==n&&t(e=>{for(let t of[`artist`,`type`,`tag`]){let n=r.get(t);n===null?e.delete(t):e.set(t,n)}},{replace:!0})},[n,t]),(0,h.useEffect)(()=>{o.status!==`loaded`||M.current||(M.current=!0,r.tagId!==null&&!o.tags.some(e=>e.id===r.tagId)&&t(e=>e.delete(`tag`),{replace:!0}))},[o,r.tagId,t]);let H=e=>{if(N.current)return;N.current=!0;let n=V.current.artistId;n!==null&&!e.some(e=>e.artist_id===n)&&t(e=>e.delete(`artist`),{replace:!0})};(0,h.useEffect)(()=>{let e=!1;return a().then(t=>{e||c({status:`loaded`,tags:t})}).catch(()=>{e||c({status:`error`,tags:[]})}),()=>{e=!0}},[]),(0,h.useEffect)(()=>{let e=!1;return b(!0),E(null),O(null),u([]),m(null),v(!1),B(r,null).then(t=>{e||(u(t.events),m(t.next_cursor),v(t.has_older_events))}).catch(()=>{e||E(`Couldn't load release history.`)}).finally(()=>{e||b(!1)}),()=>{e=!0}},[r.artistId,r.eventType,r.tagId,k]);let U=()=>{f==null||S||(C(!0),O(null),B(r,f).then(e=>{u(t=>{let n=new Set(t.map(e=>e.id)),r=e.events.filter(e=>!n.has(e.id));return[...t,...r]}),m(e.next_cursor),v(e.has_older_events)}).catch(()=>{O(`Couldn't load release history.`)}).finally(()=>{C(!1)}))},W=()=>{A(e=>e+1)},G=r.artistId!=null||r.eventType!=null,K=r.tagId===null?null:{name:o.tags.find(e=>e.id===r.tagId)?.name??`this tag`},q=e=>{t(t=>{R(t,`artist`,e.artistId),R(t,`type`,e.eventType),R(t,`tag`,e.tagId)})},J=()=>{t(e=>e.delete(`tag`)),j.current?.focus()};return(0,g.jsxs)(`div`,{className:`flex flex-col gap-6 p-8`,children:[(0,g.jsx)(`h1`,{className:`text-display font-semibold text-foreground`,children:`History`}),(0,g.jsx)(x,{value:r,onChange:q,tags:o.tags,tagsStatus:o.status,tagTriggerRef:j,onArtistsLoaded:H}),T&&(0,g.jsx)(p,{heading:`Couldn't load release history.`,body:`Please try again.`,action:(0,g.jsx)(i,{variant:`secondary`,onClick:W,children:`Retry`})}),!T&&y&&(0,g.jsx)(`div`,{className:`flex flex-col gap-4`,children:Array.from({length:L}).map((e,t)=>(0,g.jsx)(I,{},t))}),!T&&!y&&l.length===0&&(()=>{let{heading:e,body:t}=z(_,K,G);return(0,g.jsx)(p,{heading:e,body:t,action:K&&!_?(0,g.jsx)(i,{variant:`secondary`,onClick:J,children:`Show all tags`}):void 0})})(),!T&&!y&&l.length>0&&(0,g.jsxs)(`div`,{className:`flex flex-col gap-4`,children:[l.map(e=>(0,g.jsx)(w,{event:e},e.id)),S&&Array.from({length:L}).map((e,t)=>(0,g.jsx)(I,{},`append-skeleton-${t}`)),D&&(0,g.jsx)(`p`,{className:`text-label text-destructive`,children:D}),f!=null&&(0,g.jsx)(i,{onClick:U,disabled:S,"aria-busy":S,className:`self-center`,children:S?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(s,{className:`size-4 animate-spin`,"aria-hidden":`true`}),`Loading…`]}):`Load more`})]})]})});export{V as default}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/history-MfMHuFqK.js b/internal/webassets/build/client/assets/history-MfMHuFqK.js deleted file mode 100644 index 0742ba2..0000000 --- a/internal/webassets/build/client/assets/history-MfMHuFqK.js +++ /dev/null @@ -1 +0,0 @@ -import{A as e,M as t,O as n,t as r}from"./jsx-runtime-DT4cu4GC.js";import{C as i,f as a,ot as o,u as s}from"./api-Bh4xuY1_.js";import{t as c}from"./CoverArt-hXVrj8Fw.js";import{n as l,r as u,t as d}from"./badge-mjpdUX1d.js";var f=t(e(),1),p=r(),m=[{value:`new_release`,label:`New release`},{value:`guest_feature`,label:`Guest feature`},{value:`deluxe_change`,label:`Deluxe change`}],h=`flex h-9 min-w-40 items-center justify-between gap-2 rounded-md border border-input bg-input/30 px-3 text-body text-foreground outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50`,g=`cursor-pointer px-3 py-1.5 text-body text-popover-foreground data-[highlighted=true]:bg-accent`;function _({label:e,options:t,value:n,onSelect:r}){let[i,a]=(0,f.useState)(!1),[o,s]=(0,f.useState)(0),c=(0,f.useRef)(null),l=(0,f.useRef)(null),u=(0,f.useId)(),d=t.findIndex(e=>e.value===``?n===null:e.value===n),m=t[d===-1?0:d],_=e=>`${u}-option-${e}`;(0,f.useEffect)(()=>{if(!i)return;function e(e){c.current?.contains(e.target)||a(!1)}return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[i]);function v(e){s(e<0?0:e),a(!0)}function y(e){let n=t[e];n&&(r(n.value===``?null:n.value),a(!1),l.current?.focus())}function b(e){if(!i){(e.key===`ArrowDown`||e.key===`ArrowUp`||e.key===`Enter`||e.key===` `)&&(e.preventDefault(),v(d===-1?0:d));return}e.key===`ArrowDown`?(e.preventDefault(),s(e=>Math.min(e+1,t.length-1))):e.key===`ArrowUp`?(e.preventDefault(),s(e=>Math.max(e-1,0))):e.key===`Enter`||e.key===` `?(e.preventDefault(),y(o)):e.key===`Escape`&&(e.preventDefault(),a(!1),l.current?.focus())}return(0,p.jsxs)(`div`,{ref:c,className:`relative`,children:[(0,p.jsxs)(`button`,{ref:l,type:`button`,role:`combobox`,"aria-expanded":i,"aria-haspopup":`listbox`,"aria-controls":u,"aria-label":e,"aria-activedescendant":i?_(o):void 0,className:h,onClick:()=>i?a(!1):v(d===-1?0:d),onKeyDown:b,children:[(0,p.jsx)(`span`,{children:m?.label}),(0,p.jsx)(`span`,{"aria-hidden":`true`,className:`text-muted-foreground`,children:`▾`})]}),i&&(0,p.jsx)(`ul`,{id:u,role:`listbox`,"aria-label":e,className:`absolute top-full left-0 z-50 mt-1 min-w-full overflow-hidden rounded-md border border-input bg-popover text-popover-foreground shadow-md`,children:t.map((e,t)=>(0,p.jsx)(`li`,{id:_(t),role:`option`,"aria-selected":e.value===``?n===null:e.value===n,"data-highlighted":t===o,className:g,onMouseEnter:()=>s(t),onClick:()=>y(t),children:e.label},e.value===``?`__all__`:e.value))})]})}function v({value:e,onChange:t}){let[n,r]=(0,f.useState)([]);(0,f.useEffect)(()=>{let e=!1;return a().then(t=>{e||r(t)}).catch(()=>{e||r([])}),()=>{e=!0}},[]);let i=[{value:``,label:`All artists`},...n.map(e=>({value:String(e.artist_id),label:e.name}))],o=[{value:``,label:`All event types`},...m];return(0,p.jsxs)(`div`,{className:`flex flex-wrap gap-4`,children:[(0,p.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Artist`,(0,p.jsx)(_,{label:`Artist`,options:i,value:e.artistId===null?null:String(e.artistId),onSelect:n=>t({...e,artistId:n===null?null:Number(n)})})]}),(0,p.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Event type`,(0,p.jsx)(_,{label:`Event type`,options:o,value:e.eventType,onSelect:n=>t({...e,eventType:n})})]})]})}var y={new_release:{label:`New release`,emoji:`🆕`,color:`var(--color-event-new-release)`},guest_feature:{label:`Guest feature`,emoji:`🎤`,color:`var(--color-event-guest-feature)`},deluxe_change:{label:`Deluxe change`,emoji:`💿`,color:`var(--color-event-deluxe-change)`}},b={label:`Unknown`,emoji:`❔`,color:`var(--color-muted-foreground)`};function x({event:e}){let t=y[e.event_type]??b;return(0,p.jsxs)(`div`,{className:`flex gap-4 rounded-2xl bg-card p-4 ring-1 ring-foreground/10`,children:[(0,p.jsx)(c,{src:e.cover_art_url,alt:e.title,size:96,className:`rounded-md`}),(0,p.jsxs)(`div`,{className:`flex min-w-0 flex-1 flex-col gap-2`,children:[(0,p.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[(0,p.jsx)(`h3`,{className:`line-clamp-2 text-heading font-semibold text-foreground`,title:e.title,children:e.title}),(0,p.jsxs)(d,{variant:`secondary`,className:`shrink-0 gap-1 text-background`,style:{backgroundColor:t.color},children:[(0,p.jsx)(`span`,{"aria-hidden":`true`,children:t.emoji}),t.label]})]}),(0,p.jsx)(`p`,{className:`text-body text-muted-foreground`,children:e.artist_name}),(0,p.jsx)(S,{event:e})]})]})}function S({event:e}){switch(e.event_type){case`new_release`:return(0,p.jsx)(C,{event:e});case`guest_feature`:return(0,p.jsx)(E,{event:e});case`deluxe_change`:return(0,p.jsx)(D,{event:e});default:return null}}function C({event:e}){let t=e.release_date??`Release date unknown`;return(0,p.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[e.release_type?`${e.release_type} · `:``,t]})}function w(e){return e.source===`musicbrainz`?`https://musicbrainz.org/recording/${encodeURIComponent(e.external_id)}`:e.source===`deezer`?`https://www.deezer.com/track/${encodeURIComponent(e.external_id)}`:null}function T(e){let t=e.watched_artist_name;return!t||t===e.artist_name?null:t}function E({event:e}){let t=w(e),n=e.release_date??`Release date unknown`,r=T(e);return(0,p.jsxs)(p.Fragment,{children:[t?(0,p.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[`Featured on`,` `,(0,p.jsx)(`a`,{href:t,target:`_blank`,rel:`noreferrer`,className:`underline underline-offset-2 hover:text-foreground`,children:e.title}),r&&` — ${r} is on your watchlist`]}):(0,p.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[e.title,r&&` — ${r} is on your watchlist`]}),(0,p.jsx)(`p`,{className:`text-label text-muted-foreground`,children:n})]})}function D({event:e}){let t=e.release_date??`Release date unknown`,n=e.track_count??`?`;return e.previous_track_count==null?(0,p.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[t,` · `,n,` tracks`]}):(0,p.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[t,` · `,e.previous_track_count,` → `,n,` tracks`]})}function O(){return(0,p.jsxs)(`div`,{className:`flex gap-4 rounded-2xl bg-card p-4 ring-1 ring-foreground/10`,children:[(0,p.jsx)(l,{className:`h-24 w-24 shrink-0 rounded-md`}),(0,p.jsxs)(`div`,{className:`flex flex-1 flex-col gap-2 py-1`,children:[(0,p.jsx)(l,{className:`h-5 w-2/3`}),(0,p.jsx)(l,{className:`h-4 w-1/3`}),(0,p.jsx)(l,{className:`h-4 w-1/2`})]})]})}var k=3;function A(e,t){return e?{heading:`Older than your retention window`,body:`There's release history for this view — it's just outside your retention window. Nothing was deleted.`}:t?{heading:`No matching events`,body:`Try a different artist or event type.`}:{heading:`No release activity yet`,body:`Add an artist to your watchlist to start tracking new releases, features, and deluxe editions.`}}function j(e,t){return s({artistId:e.artistId??void 0,eventType:e.eventType??void 0,cursor:t??void 0})}var M=n(function(){let[e,t]=(0,f.useState)({artistId:null,eventType:null}),[n,r]=(0,f.useState)([]),[a,s]=(0,f.useState)(null),[c,l]=(0,f.useState)(!1),[d,m]=(0,f.useState)(!0),[h,g]=(0,f.useState)(!1),[_,y]=(0,f.useState)(null),[b,S]=(0,f.useState)(null),[C,w]=(0,f.useState)(0);(0,f.useEffect)(()=>{let t=!1;return m(!0),y(null),S(null),r([]),s(null),l(!1),j(e,null).then(e=>{t||(r(e.events),s(e.next_cursor),l(e.has_older_events))}).catch(()=>{t||y(`Couldn't load release history.`)}).finally(()=>{t||m(!1)}),()=>{t=!0}},[e.artistId,e.eventType,C]);let T=()=>{a==null||h||(g(!0),S(null),j(e,a).then(e=>{r(t=>{let n=new Set(t.map(e=>e.id)),r=e.events.filter(e=>!n.has(e.id));return[...t,...r]}),s(e.next_cursor),l(e.has_older_events)}).catch(()=>{S(`Couldn't load release history.`)}).finally(()=>{g(!1)}))},E=()=>{w(e=>e+1)},D=e.artistId!=null||e.eventType!=null;return(0,p.jsxs)(`div`,{className:`flex flex-col gap-6 p-8`,children:[(0,p.jsx)(`h1`,{className:`text-display font-semibold text-foreground`,children:`History`}),(0,p.jsx)(v,{value:e,onChange:t}),_&&(0,p.jsx)(u,{heading:`Couldn't load release history.`,body:`Please try again.`,action:(0,p.jsx)(i,{variant:`secondary`,onClick:E,children:`Retry`})}),!_&&d&&(0,p.jsx)(`div`,{className:`flex flex-col gap-4`,children:Array.from({length:k}).map((e,t)=>(0,p.jsx)(O,{},t))}),!_&&!d&&n.length===0&&(()=>{let{heading:e,body:t}=A(c,D);return(0,p.jsx)(u,{heading:e,body:t})})(),!_&&!d&&n.length>0&&(0,p.jsxs)(`div`,{className:`flex flex-col gap-4`,children:[n.map(e=>(0,p.jsx)(x,{event:e},e.id)),h&&Array.from({length:k}).map((e,t)=>(0,p.jsx)(O,{},`append-skeleton-${t}`)),b&&(0,p.jsx)(`p`,{className:`text-label text-destructive`,children:b}),a!=null&&(0,p.jsx)(i,{onClick:T,disabled:h,"aria-busy":h,className:`self-center`,children:h?(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(o,{className:`size-4 animate-spin`,"aria-hidden":`true`}),`Loading…`]}):`Load more`})]})]})});export{M as default}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/input-Dx1GFl8F.js b/internal/webassets/build/client/assets/input-Dx1GFl8F.js deleted file mode 100644 index b77b6d9..0000000 --- a/internal/webassets/build/client/assets/input-Dx1GFl8F.js +++ /dev/null @@ -1 +0,0 @@ -import{A as e,M as t,t as n}from"./jsx-runtime-DT4cu4GC.js";import{t as r}from"./react-dom-I-EqbIrq.js";import{E as i,L as a,P as o,R as s,w as c}from"./api-Bh4xuY1_.js";import{A as l,C as u,D as d,E as f,F as p,N as m,S as ee,h,t as g,x as _}from"./createBaseUIEventDetails-BTtuUEn7.js";var v=t(e(),1),y=t(r(),1);function b(e){if(!e||typeof document>`u`)return;let t=document.head||document.getElementsByTagName(`head`)[0],n=document.createElement(`style`);n.type=`text/css`,t.appendChild(n),n.styleSheet?n.styleSheet.cssText=e:n.appendChild(document.createTextNode(e))}var te=e=>{switch(e){case`success`:return C;case`info`:return T;case`warning`:return w;case`error`:return E;default:return null}},x=Array(12).fill(0),S=({visible:e,className:t})=>v.createElement(`div`,{className:[`sonner-loading-wrapper`,t].filter(Boolean).join(` `),"data-visible":e},v.createElement(`div`,{className:`sonner-spinner`},x.map((e,t)=>v.createElement(`div`,{className:`sonner-loading-bar`,key:`spinner-bar-${t}`})))),C=v.createElement(`svg`,{xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 20 20`,fill:`currentColor`,height:`20`,width:`20`,"aria-hidden":`true`},v.createElement(`path`,{fillRule:`evenodd`,d:`M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z`,clipRule:`evenodd`})),w=v.createElement(`svg`,{xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 24 24`,fill:`currentColor`,height:`20`,width:`20`,"aria-hidden":`true`},v.createElement(`path`,{fillRule:`evenodd`,d:`M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z`,clipRule:`evenodd`})),T=v.createElement(`svg`,{xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 20 20`,fill:`currentColor`,height:`20`,width:`20`,"aria-hidden":`true`},v.createElement(`path`,{fillRule:`evenodd`,d:`M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z`,clipRule:`evenodd`})),E=v.createElement(`svg`,{xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 20 20`,fill:`currentColor`,height:`20`,width:`20`,"aria-hidden":`true`},v.createElement(`path`,{fillRule:`evenodd`,d:`M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z`,clipRule:`evenodd`})),ne=v.createElement(`svg`,{xmlns:`http://www.w3.org/2000/svg`,width:`12`,height:`12`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.5`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`},v.createElement(`line`,{x1:`18`,y1:`6`,x2:`6`,y2:`18`}),v.createElement(`line`,{x1:`6`,y1:`6`,x2:`18`,y2:`18`})),re=()=>{let[e,t]=v.useState(document.hidden);return v.useEffect(()=>{let e=()=>{t(document.hidden)};return document.addEventListener(`visibilitychange`,e),()=>document.removeEventListener(`visibilitychange`,e)},[]),e},D=1,O=100,k=e=>typeof e?.id==`number`||e?.id?.length>0?e.id:D++,A=new class{constructor(){this.subscribe=e=>(this.subscribers.push(e),this.getActiveToasts().forEach(t=>e(t)),()=>{let t=this.subscribers.indexOf(e);this.subscribers.splice(t,1)}),this.publish=e=>{this.subscribers.forEach(t=>t(e))},this.addToast=e=>{this.publish(e),this.toasts=[...this.toasts,e],this.trimHistory()},this.trimHistory=()=>{let e=this.toasts.length-O;e<=0||(this.toasts=this.toasts.filter(t=>e>0&&this.dismissedToasts.has(t.id)?(this.dismissedToasts.delete(t.id),e--,!1):!0))},this.create=e=>{let{message:t,...n}=e,r=k(e),i=this.pendingDismissals.get(r);i!==void 0&&(cancelAnimationFrame(i),this.pendingDismissals.delete(r),this.dismissedToasts.delete(r));let a=this.dismissedToasts.has(r),o=e.dismissible===void 0?!0:e.dismissible;return a&&(this.dismissedToasts.delete(r),this.toasts=this.toasts.filter(e=>e.id!==r)),!a&&this.toasts.find(e=>e.id===r)?this.toasts=this.toasts.map(n=>n.id===r?(this.publish({...n,...e,id:r,title:t}),{...n,...e,id:r,dismissible:o,title:t}):n):this.addToast({title:t,...n,dismissible:o,id:r}),r},this.dismiss=e=>{if(e==null)return this.getActiveToasts().forEach(e=>{this.dismissedToasts.add(e.id),this.subscribers.forEach(t=>t({id:e.id,dismiss:!0}))}),e;this.dismissedToasts.add(e);let t=this.pendingDismissals.get(e);return t!==void 0&&cancelAnimationFrame(t),this.pendingDismissals.set(e,requestAnimationFrame(()=>{this.pendingDismissals.delete(e),this.subscribers.forEach(t=>t({id:e,dismiss:!0}))})),e},this.message=(e,t)=>this.create({...t,message:e,type:void 0}),this.error=(e,t)=>this.create({...t,message:e,type:`error`}),this.success=(e,t)=>this.create({...t,type:`success`,message:e}),this.info=(e,t)=>this.create({...t,type:`info`,message:e}),this.warning=(e,t)=>this.create({...t,type:`warning`,message:e}),this.loading=(e,t)=>this.create({...t,type:`loading`,message:e}),this.promise=(e,t)=>{if(!t)return;let n;t.loading!==void 0&&(n=this.create({...t,promise:e,type:`loading`,message:t.loading,description:typeof t.description==`function`?void 0:t.description}));let r=Promise.resolve(e instanceof Function?e():e),i=n!==void 0,a,o=r.then(async e=>{if(a=[`resolve`,e],v.isValidElement(e))i=!1,this.create({id:n,type:`default`,message:e});else if(M(e)&&!e.ok){i=!1;let r=typeof t.error==`function`?await t.error(`HTTP error! status: ${e.status}`):t.error,a=typeof t.description==`function`?await t.description(`HTTP error! status: ${e.status}`):t.description,o=typeof r==`object`&&!v.isValidElement(r)?r:{message:r};this.create({id:n,type:`error`,description:a,...o})}else if(e instanceof Error){i=!1;let r=typeof t.error==`function`?await t.error(e):t.error,a=typeof t.description==`function`?await t.description(e):t.description,o=typeof r==`object`&&!v.isValidElement(r)?r:{message:r};this.create({id:n,type:`error`,description:a,...o})}else if(t.success!==void 0){i=!1;let r=typeof t.success==`function`?await t.success(e):t.success,a=typeof t.description==`function`?await t.description(e):t.description,o=typeof r==`object`&&!v.isValidElement(r)?r:{message:r};this.create({id:n,type:`success`,description:a,...o})}}).catch(async e=>{if(a=[`reject`,e],t.error!==void 0){i=!1;let r=typeof t.error==`function`?await t.error(e):t.error,a=typeof t.description==`function`?await t.description(e):t.description,o=typeof r==`object`&&!v.isValidElement(r)?r:{message:r};this.create({id:n,type:`error`,description:a,...o})}}).finally(()=>{i&&(this.dismiss(n),n=void 0),t.finally==null||t.finally.call(t)}),s=()=>new Promise((e,t)=>o.then(()=>a[0]===`reject`?t(a[1]):e(a[1])).catch(t));return typeof n!=`string`&&typeof n!=`number`?{unwrap:s}:Object.assign(n,{unwrap:s})},this.custom=(e,t)=>{let n=k(t);return this.create({...t,jsx:e(n),id:n,type:void 0}),n},this.getActiveToasts=()=>this.toasts.filter(e=>!this.dismissedToasts.has(e.id)),this.subscribers=[],this.toasts=[],this.dismissedToasts=new Set,this.pendingDismissals=new Map}},j=(e,t)=>A.message(e,t),M=e=>e&&typeof e==`object`&&`ok`in e&&typeof e.ok==`boolean`&&`status`in e&&typeof e.status==`number`,N=Object.assign(j,{success:A.success,info:A.info,warning:A.warning,error:A.error,custom:A.custom,message:A.message,promise:A.promise,dismiss:A.dismiss,loading:A.loading},{getHistory:()=>A.toasts,getToasts:()=>A.getActiveToasts()});b(`[data-sonner-toaster][dir=ltr],html[dir=ltr]{--toast-icon-margin-start:-3px;--toast-icon-margin-end:4px;--toast-svg-margin-start:-1px;--toast-svg-margin-end:0px;--toast-button-margin-start:auto;--toast-button-margin-end:0;--toast-close-button-start:0;--toast-close-button-end:unset;--toast-close-button-transform:translate(-35%, -35%)}[data-sonner-toaster][dir=rtl],html[dir=rtl]{--toast-icon-margin-start:4px;--toast-icon-margin-end:-3px;--toast-svg-margin-start:0px;--toast-svg-margin-end:-1px;--toast-button-margin-start:0;--toast-button-margin-end:auto;--toast-close-button-start:unset;--toast-close-button-end:0;--toast-close-button-transform:translate(35%, -35%)}[data-sonner-toaster]{position:fixed;width:var(--width);font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;--gray1:hsl(0, 0%, 99%);--gray2:hsl(0, 0%, 97.3%);--gray3:hsl(0, 0%, 95.1%);--gray4:hsl(0, 0%, 93%);--gray5:hsl(0, 0%, 90.9%);--gray6:hsl(0, 0%, 88.7%);--gray7:hsl(0, 0%, 85.8%);--gray8:hsl(0, 0%, 78%);--gray9:hsl(0, 0%, 56.1%);--gray10:hsl(0, 0%, 52.3%);--gray11:hsl(0, 0%, 43.5%);--gray12:hsl(0, 0%, 9%);--border-radius:8px;box-sizing:border-box;padding:0;margin:0;list-style:none;outline:0;z-index:999999999;transition:transform .4s ease}@media (hover:none) and (pointer:coarse){[data-sonner-toaster][data-lifted=true]{transform:none}}[data-sonner-toaster][data-x-position=right]{right:var(--offset-right)}[data-sonner-toaster][data-x-position=left]{left:var(--offset-left)}[data-sonner-toaster][data-x-position=center]{left:50%;transform:translateX(-50%)}[data-sonner-toaster][data-y-position=top]{top:var(--offset-top)}[data-sonner-toaster][data-y-position=bottom]{bottom:var(--offset-bottom)}[data-sonner-toast]{--y:translateY(100%);--lift-amount:calc(var(--lift) * var(--gap));z-index:var(--z-index);position:absolute;opacity:0;transform:var(--y);touch-action:none;transition:transform .4s,opacity .4s,height .4s,box-shadow .2s;box-sizing:border-box;outline:0;overflow-wrap:anywhere}[data-sonner-toast][data-styled=true]{padding:16px;background:var(--normal-bg);border:1px solid var(--normal-border);color:var(--normal-text);border-radius:var(--border-radius);box-shadow:0 4px 12px rgba(0,0,0,.1);width:var(--width);font-size:13px;display:flex;align-items:center;gap:6px}[data-sonner-toast]:focus-visible{box-shadow:0 4px 12px rgba(0,0,0,.1),0 0 0 2px rgba(0,0,0,.2)}[data-sonner-toast][data-y-position=top]{top:0;--y:translateY(-100%);--lift:1;--lift-amount:calc(1 * var(--gap))}[data-sonner-toast][data-y-position=bottom]{bottom:0;--y:translateY(100%);--lift:-1;--lift-amount:calc(var(--lift) * var(--gap))}[data-sonner-toast][data-styled=true] [data-description]{font-weight:400;line-height:1.4;color:#3f3f3f}[data-rich-colors=true][data-sonner-toast][data-styled=true] [data-description]{color:inherit}[data-sonner-toaster][data-sonner-theme=dark] [data-description]{color:#e8e8e8}[data-sonner-toast][data-styled=true] [data-title]{font-weight:500;line-height:1.5;color:inherit}[data-sonner-toast][data-styled=true] [data-icon]{display:flex;height:16px;width:16px;position:relative;justify-content:flex-start;align-items:center;flex-shrink:0;margin-left:var(--toast-icon-margin-start);margin-right:var(--toast-icon-margin-end)}[data-sonner-toast][data-promise=true] [data-icon]>svg{opacity:0;transform:scale(.8);transform-origin:center;animation:sonner-fade-in .3s ease forwards}[data-sonner-toast][data-styled=true] [data-icon]>*{flex-shrink:0}[data-sonner-toast][data-styled=true] [data-icon] svg{margin-left:var(--toast-svg-margin-start);margin-right:var(--toast-svg-margin-end)}[data-sonner-toast][data-styled=true] [data-content]{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}[data-sonner-toast][data-styled=true] [data-button]{border-radius:4px;padding-left:8px;padding-right:8px;height:24px;font-size:12px;color:var(--normal-bg);background:var(--normal-text);margin-left:var(--toast-button-margin-start);margin-right:var(--toast-button-margin-end);border:none;font-weight:500;cursor:pointer;outline:0;display:flex;align-items:center;flex-shrink:0;transition:opacity .4s,box-shadow .2s}[data-sonner-toast][data-styled=true] [data-button]:focus-visible{box-shadow:0 0 0 2px rgba(0,0,0,.4)}[data-sonner-toast][data-styled=true] [data-button]:first-of-type{margin-left:var(--toast-button-margin-start);margin-right:var(--toast-button-margin-end)}[data-sonner-toast][data-styled=true] [data-cancel]{color:var(--normal-text);background:rgba(0,0,0,.08)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast][data-styled=true] [data-cancel]{background:rgba(255,255,255,.3)}[data-sonner-toast][data-styled=true] [data-close-button]{position:absolute;left:var(--toast-close-button-start);right:var(--toast-close-button-end);top:0;height:20px;width:20px;display:flex;justify-content:center;align-items:center;padding:0;color:var(--normal-text);background:var(--normal-bg);border:1px solid var(--normal-border);transform:var(--toast-close-button-transform);border-radius:50%;cursor:pointer;z-index:1;transition:opacity .1s,background .2s,border-color .2s}[data-sonner-toast][data-styled=true] [data-close-button]:focus-visible{box-shadow:0 4px 12px rgba(0,0,0,.1),0 0 0 2px rgba(0,0,0,.2)}[data-sonner-toast][data-styled=true] [data-disabled=true]{cursor:not-allowed}[data-sonner-toast][data-styled=true]:hover [data-close-button]:hover{background:var(--gray2);border-color:var(--gray5)}[data-sonner-toast][data-swiping=true]::before{content:'';position:absolute;left:-100%;right:-100%;height:100%;z-index:-1}[data-sonner-toast][data-y-position=top][data-swiping=true]::before{bottom:50%;transform:scaleY(3) translateY(50%)}[data-sonner-toast][data-y-position=bottom][data-swiping=true]::before{top:50%;transform:scaleY(3) translateY(-50%)}[data-sonner-toast][data-swiping=false][data-removed=true]::before{content:'';position:absolute;inset:0;transform:scaleY(2)}[data-sonner-toast][data-expanded=true]::after{content:'';position:absolute;left:0;height:calc(var(--gap) + 1px);bottom:100%;width:100%}[data-sonner-toast][data-mounted=true]{--y:translateY(0);opacity:1}[data-sonner-toast][data-expanded=false][data-front=false]{--scale:var(--toasts-before) * 0.05 + 1;--y:translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)));height:var(--front-toast-height)}[data-sonner-toast]>*{transition:opacity .4s}[data-sonner-toast][data-x-position=right]{right:0}[data-sonner-toast][data-x-position=left]{left:0}[data-sonner-toast][data-expanded=false][data-front=false][data-styled=true]>*{opacity:0}[data-sonner-toast][data-visible=false]{opacity:0;pointer-events:none}[data-sonner-toast][data-mounted=true][data-expanded=true]{--y:translateY(calc(var(--lift) * var(--offset)));height:var(--initial-height)}[data-sonner-toast][data-removed=true][data-front=true][data-swipe-out=false]{--y:translateY(calc(var(--lift) * -100%));opacity:0}[data-sonner-toast][data-removed=true][data-front=false][data-swipe-out=false][data-expanded=true]{--y:translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));opacity:0}[data-sonner-toast][data-removed=true][data-front=false][data-swipe-out=false][data-expanded=false]{--y:translateY(40%);opacity:0;transition:transform .5s,opacity .2s}[data-sonner-toast][data-removed=true][data-front=false]::before{height:calc(var(--initial-height) + 20%)}[data-sonner-toast][data-swiping=true]{transform:var(--y) translateY(var(--swipe-amount-y,0)) translateX(var(--swipe-amount-x,0));transition:none}[data-sonner-toast][data-swiped=true]{-webkit-user-select:none;user-select:none}[data-sonner-toast][data-swipe-out=true][data-y-position=bottom],[data-sonner-toast][data-swipe-out=true][data-y-position=top]{animation-duration:.2s;animation-timing-function:ease-out;animation-fill-mode:forwards}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=left]{animation-name:swipe-out-left}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=right]{animation-name:swipe-out-right}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=up]{animation-name:swipe-out-up}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=down]{animation-name:swipe-out-down}@keyframes swipe-out-left{from{transform:var(--y) translateX(var(--swipe-amount-x));opacity:1}to{transform:var(--y) translateX(calc(var(--swipe-amount-x) - 100%));opacity:0}}@keyframes swipe-out-right{from{transform:var(--y) translateX(var(--swipe-amount-x));opacity:1}to{transform:var(--y) translateX(calc(var(--swipe-amount-x) + 100%));opacity:0}}@keyframes swipe-out-up{from{transform:var(--y) translateY(var(--swipe-amount-y));opacity:1}to{transform:var(--y) translateY(calc(var(--swipe-amount-y) - 100%));opacity:0}}@keyframes swipe-out-down{from{transform:var(--y) translateY(var(--swipe-amount-y));opacity:1}to{transform:var(--y) translateY(calc(var(--swipe-amount-y) + 100%));opacity:0}}@media (max-width:600px){[data-sonner-toaster]{position:fixed;right:var(--mobile-offset-right);left:var(--mobile-offset-left);width:100%}[data-sonner-toaster][dir=rtl]{left:calc(var(--mobile-offset-left) * -1)}[data-sonner-toaster] [data-sonner-toast]{left:0;right:0;width:calc(100% - var(--mobile-offset-left) * 2)}[data-sonner-toaster][data-x-position=left]{left:var(--mobile-offset-left)}[data-sonner-toaster][data-y-position=bottom]{bottom:var(--mobile-offset-bottom)}[data-sonner-toaster][data-y-position=top]{top:var(--mobile-offset-top)}[data-sonner-toaster][data-x-position=center]{left:var(--mobile-offset-left);right:var(--mobile-offset-right);transform:none}}[data-sonner-toaster][data-sonner-theme=light]{--normal-bg:#fff;--normal-border:var(--gray4);--normal-text:var(--gray12);--success-bg:hsl(143, 85%, 96%);--success-border:hsl(145, 92%, 87%);--success-text:hsl(140, 100%, 27%);--info-bg:hsl(208, 100%, 97%);--info-border:hsl(221, 91%, 93%);--info-text:hsl(210, 92%, 45%);--warning-bg:hsl(49, 100%, 97%);--warning-border:hsl(49, 91%, 84%);--warning-text:hsl(31, 92%, 45%);--error-bg:hsl(359, 100%, 97%);--error-border:hsl(359, 100%, 94%);--error-text:hsl(360, 100%, 45%)}[data-sonner-toaster][data-sonner-theme=light] [data-sonner-toast][data-invert=true]{--normal-bg:#000;--normal-border:hsl(0, 0%, 20%);--normal-text:var(--gray1)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast][data-invert=true]{--normal-bg:#fff;--normal-border:var(--gray3);--normal-text:var(--gray12)}[data-sonner-toaster][data-sonner-theme=dark]{--normal-bg:#000;--normal-bg-hover:hsl(0, 0%, 12%);--normal-border:hsl(0, 0%, 20%);--normal-border-hover:hsl(0, 0%, 25%);--normal-text:var(--gray1);--success-bg:hsl(150, 100%, 6%);--success-border:hsl(147, 100%, 12%);--success-text:hsl(150, 86%, 65%);--info-bg:hsl(215, 100%, 6%);--info-border:hsl(223, 43%, 17%);--info-text:hsl(216, 87%, 65%);--warning-bg:hsl(64, 100%, 6%);--warning-border:hsl(60, 100%, 9%);--warning-text:hsl(46, 87%, 65%);--error-bg:hsl(358, 76%, 10%);--error-border:hsl(357, 89%, 16%);--error-text:hsl(358, 100%, 81%)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast] [data-close-button]{background:var(--normal-bg);border-color:var(--normal-border);color:var(--normal-text)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast] [data-close-button]:hover{background:var(--normal-bg-hover);border-color:var(--normal-border-hover)}[data-rich-colors=true][data-sonner-toast][data-type=success]{background:var(--success-bg);border-color:var(--success-border);color:var(--success-text)}[data-rich-colors=true][data-sonner-toast][data-type=success] [data-close-button]{background:var(--success-bg);border-color:var(--success-border);color:var(--success-text)}[data-rich-colors=true][data-sonner-toast][data-type=info]{background:var(--info-bg);border-color:var(--info-border);color:var(--info-text)}[data-rich-colors=true][data-sonner-toast][data-type=info] [data-close-button]{background:var(--info-bg);border-color:var(--info-border);color:var(--info-text)}[data-rich-colors=true][data-sonner-toast][data-type=warning]{background:var(--warning-bg);border-color:var(--warning-border);color:var(--warning-text)}[data-rich-colors=true][data-sonner-toast][data-type=warning] [data-close-button]{background:var(--warning-bg);border-color:var(--warning-border);color:var(--warning-text)}[data-rich-colors=true][data-sonner-toast][data-type=error]{background:var(--error-bg);border-color:var(--error-border);color:var(--error-text)}[data-rich-colors=true][data-sonner-toast][data-type=error] [data-close-button]{background:var(--error-bg);border-color:var(--error-border);color:var(--error-text)}.sonner-loading-wrapper{--size:16px;height:var(--size);width:var(--size);position:absolute;inset:0;z-index:10}.sonner-loading-wrapper[data-visible=false]{transform-origin:center;animation:sonner-fade-out .2s ease forwards}.sonner-spinner{position:relative;top:50%;left:50%;height:var(--size);width:var(--size)}.sonner-loading-bar{animation:sonner-spin 1.2s linear infinite;background:var(--gray11);border-radius:6px;height:8%;left:-10%;position:absolute;top:-3.9%;width:24%}.sonner-loading-bar:first-child{animation-delay:-1.2s;transform:rotate(.0001deg) translate(146%)}.sonner-loading-bar:nth-child(2){animation-delay:-1.1s;transform:rotate(30deg) translate(146%)}.sonner-loading-bar:nth-child(3){animation-delay:-1s;transform:rotate(60deg) translate(146%)}.sonner-loading-bar:nth-child(4){animation-delay:-.9s;transform:rotate(90deg) translate(146%)}.sonner-loading-bar:nth-child(5){animation-delay:-.8s;transform:rotate(120deg) translate(146%)}.sonner-loading-bar:nth-child(6){animation-delay:-.7s;transform:rotate(150deg) translate(146%)}.sonner-loading-bar:nth-child(7){animation-delay:-.6s;transform:rotate(180deg) translate(146%)}.sonner-loading-bar:nth-child(8){animation-delay:-.5s;transform:rotate(210deg) translate(146%)}.sonner-loading-bar:nth-child(9){animation-delay:-.4s;transform:rotate(240deg) translate(146%)}.sonner-loading-bar:nth-child(10){animation-delay:-.3s;transform:rotate(270deg) translate(146%)}.sonner-loading-bar:nth-child(11){animation-delay:-.2s;transform:rotate(300deg) translate(146%)}.sonner-loading-bar:nth-child(12){animation-delay:-.1s;transform:rotate(330deg) translate(146%)}@keyframes sonner-fade-in{0%{opacity:0;transform:scale(.8)}100%{opacity:1;transform:scale(1)}}@keyframes sonner-fade-out{0%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(.8)}}@keyframes sonner-spin{0%{opacity:1}100%{opacity:.15}}@media (prefers-reduced-motion){.sonner-loading-bar,[data-sonner-toast],[data-sonner-toast]>*{transition:none!important;animation:none!important}}.sonner-loader{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);transform-origin:center;transition:opacity .2s,transform .2s}.sonner-loader[data-visible=false]{opacity:0;transform:scale(.8) translate(-50%,-50%)}`);function P(e){return e.label!==void 0}var F=3,I=`24px`,L=`16px`,R=4e3,z=356,B=14,ie=45,ae=200;function V(...e){return e.filter(Boolean).join(` `)}function oe(e){let[t,n]=e.split(`-`),r=[];return t&&r.push(t),n&&r.push(n),r}var se=e=>{let{invert:t,toast:n,unstyled:r,interacting:i,setHeights:a,visibleToasts:o,heights:s,index:c,toasts:l,expanded:u,removeToast:d,defaultRichColors:f,closeButton:p,style:m,cancelButtonStyle:ee,actionButtonStyle:h,className:g=``,descriptionClassName:_=``,duration:y,position:b,gap:x,expandByDefault:C,classNames:w,icons:T,closeButtonAriaLabel:E=`Close toast`}=e,[D,O]=v.useState(null),[k,A]=v.useState(null),[j,M]=v.useState(!1),[N,F]=v.useState(!1),[I,L]=v.useState(!1),[z,B]=v.useState(!1),[se,H]=v.useState(!1),[ce,U]=v.useState(0),[le,W]=v.useState(0),G=v.useRef(n.duration||y||R),ue=v.useRef(null),K=v.useRef(null),de=c===0,fe=c+1<=o,q=n.type,pe=q??`default`,J=n.dismissible!==!1,me=n.className||``,he=n.descriptionClassName||``,Y=v.useMemo(()=>s.findIndex(e=>e.toastId===n.id)||0,[s,n.id]),ge=v.useMemo(()=>n.closeButton??p,[n.closeButton,p]),_e=v.useMemo(()=>n.duration||y||R,[n.duration,y]),ve=v.useRef(0),X=v.useRef(0),ye=v.useRef(0),Z=v.useRef(null),[be,xe]=b.split(`-`),Se=v.useMemo(()=>s.reduce((e,t,n)=>n>=Y?e:e+t.height,0),[s,Y]),Ce=re(),Q=v.useMemo(()=>e.swipeDirections??oe(b),[e.swipeDirections,b]),we=n.invert||t,Te=q===`loading`;X.current=v.useMemo(()=>Y*x+Se,[Y,Se]),v.useEffect(()=>{G.current=_e},[_e]),v.useEffect(()=>{M(!0)},[]),v.useEffect(()=>{let e=K.current;if(e){let t=e.getBoundingClientRect().height;return W(t),a(e=>[{toastId:n.id,height:t,position:n.position},...e]),()=>a(e=>e.filter(e=>e.toastId!==n.id))}},[a,n.id]),v.useLayoutEffect(()=>{if(!j)return;let e=K.current,t=e.style.height;e.style.height=`auto`;let r=e.getBoundingClientRect().height;e.style.height=t,W(r),a(e=>e.find(e=>e.toastId===n.id)?e.map(e=>e.toastId===n.id?{...e,height:r}:e):[{toastId:n.id,height:r,position:n.position},...e])},[j,n.title,n.description,a,n.id,n.jsx,n.action,n.cancel]);let $=v.useCallback(()=>{F(!0),U(X.current),a(e=>e.filter(e=>e.toastId!==n.id)),setTimeout(()=>{d(n)},ae)},[n,d,a,X]);v.useEffect(()=>{if(n.promise&&q===`loading`||n.duration===1/0||n.type===`loading`)return;let e;return u||i||Ce?(()=>{if(ye.current{n.onAutoClose==null||n.onAutoClose.call(n,n),$()},G.current)),()=>clearTimeout(e)},[u,i,n,q,Ce,$]),v.useEffect(()=>{n.delete&&($(),n.onDismiss==null||n.onDismiss.call(n,n))},[$,n.delete]);function Ee(){return T?.loading?v.createElement(`div`,{className:V(w?.loader,n?.classNames?.loader,`sonner-loader`),"data-visible":q===`loading`},T.loading):v.createElement(S,{className:V(w?.loader,n?.classNames?.loader),visible:q===`loading`})}let De=n.icon||T?.[q]||te(q);return v.createElement(`li`,{tabIndex:0,ref:K,className:V(g,me,w?.toast,n?.classNames?.toast,w?.[pe],n?.classNames?.[pe]),"data-sonner-toast":``,"data-rich-colors":n.richColors??f,"data-styled":!(n.jsx||n.unstyled||r),"data-mounted":j,"data-promise":!!n.promise,"data-swiped":se,"data-removed":N,"data-visible":fe,"data-y-position":be,"data-x-position":xe,"data-index":c,"data-front":de,"data-swiping":I,"data-dismissible":J,"data-type":q,"data-invert":we,"data-swipe-out":z,"data-swipe-direction":k,"data-expanded":!!(u||C&&j),"data-testid":n.testId,style:{"--index":c,"--toasts-before":c,"--z-index":l.length-c,"--offset":`${N?ce:X.current}px`,"--initial-height":C?`auto`:`${le}px`,...m,...n.style},onDragEnd:()=>{L(!1),O(null),Z.current=null},onPointerDown:e=>{e.button!==2&&(Te||!J||(ue.current=new Date,U(X.current),e.target.setPointerCapture(e.pointerId),e.target.tagName!==`BUTTON`&&(L(!0),Z.current={x:e.clientX,y:e.clientY})))},onPointerUp:()=>{if(z||!J)return;Z.current=null;let e=Number(K.current?.style.getPropertyValue(`--swipe-amount-x`).replace(`px`,``)||0),t=Number(K.current?.style.getPropertyValue(`--swipe-amount-y`).replace(`px`,``)||0),r=new Date().getTime()-ue.current?.getTime(),i=D===`x`?e:t,a=Math.abs(i)/r;if((D===`x`?Q.includes(e>0?`right`:`left`):Q.includes(t>0?`bottom`:`top`))&&(Math.abs(i)>=ie||a>.11)){U(X.current),n.onDismiss==null||n.onDismiss.call(n,n),A(D===`x`?e>0?`right`:`left`:t>0?`down`:`up`),$(),B(!0);return}else{var o,s;(o=K.current)==null||o.style.setProperty(`--swipe-amount-x`,`0px`),(s=K.current)==null||s.style.setProperty(`--swipe-amount-y`,`0px`)}H(!1),L(!1),O(null)},onPointerMove:e=>{var t,n;if(!Z.current||!J||window.getSelection()?.toString().length>0)return;let r=e.clientY-Z.current.y,i=e.clientX-Z.current.x;!D&&(Math.abs(i)>1||Math.abs(r)>1)&&O(Math.abs(i)>Math.abs(r)?`x`:`y`);let a={x:0,y:0},o=e=>1/(1.5+Math.abs(e)/20);if(D===`y`){if(Q.includes(`top`)||Q.includes(`bottom`))if(Q.includes(`top`)&&r<0||Q.includes(`bottom`)&&r>0)a.y=r;else{let e=r*o(r);a.y=Math.abs(e)0)a.x=i;else{let e=i*o(i);a.x=Math.abs(e)0||Math.abs(a.y)>0)&&H(!0),(t=K.current)==null||t.style.setProperty(`--swipe-amount-x`,`${a.x}px`),(n=K.current)==null||n.style.setProperty(`--swipe-amount-y`,`${a.y}px`)}},ge&&!n.jsx&&q!==`loading`?v.createElement(`button`,{"aria-label":E,"data-disabled":Te,"data-close-button":!0,onClick:Te||!J?()=>{}:()=>{$(),n.onDismiss==null||n.onDismiss.call(n,n)},className:V(w?.closeButton,n?.classNames?.closeButton)},T?.close??ne):null,(q||n.icon||n.promise)&&n.icon!==null&&(T?.[q]!==null||n.icon)?v.createElement(`div`,{"data-icon":``,className:V(w?.icon,n?.classNames?.icon)},q===`loading`?n.icon||Ee():n.promise?Ee():null,q===`loading`?null:De):null,v.createElement(`div`,{"data-content":``,className:V(w?.content,n?.classNames?.content)},v.createElement(`div`,{"data-title":``,className:V(w?.title,n?.classNames?.title)},n.jsx?n.jsx:typeof n.title==`function`?n.title():n.title),n.description?v.createElement(`div`,{"data-description":``,className:V(_,he,w?.description,n?.classNames?.description)},typeof n.description==`function`?n.description():n.description):null),v.isValidElement(n.cancel)?n.cancel:n.cancel&&P(n.cancel)?v.createElement(`button`,{"data-button":!0,"data-cancel":!0,style:n.cancelButtonStyle||ee,onClick:e=>{P(n.cancel)&&J&&(n.cancel.onClick==null||n.cancel.onClick.call(n.cancel,e),$())},className:V(w?.cancelButton,n?.classNames?.cancelButton)},n.cancel.label):null,v.isValidElement(n.action)?n.action:n.action&&P(n.action)?v.createElement(`button`,{"data-button":!0,"data-action":!0,style:n.actionButtonStyle||h,onClick:e=>{P(n.action)&&(n.action.onClick==null||n.action.onClick.call(n.action,e),!e.defaultPrevented&&$())},className:V(w?.actionButton,n?.classNames?.actionButton)},n.action.label):null)};function H(){if(typeof window>`u`||typeof document>`u`)return`ltr`;let e=document.documentElement.getAttribute(`dir`);return e===`auto`||!e?window.getComputedStyle(document.documentElement).direction:e}function ce(e,t){let n={};return[e,t].forEach((e,t)=>{let r=t===1,i=r?`--mobile-offset`:`--offset`,a=r?L:I;function o(e){[`top`,`right`,`bottom`,`left`].forEach(t=>{n[`${i}-${t}`]=typeof e==`number`?`${e}px`:e})}typeof e==`number`||typeof e==`string`?o(e):typeof e==`object`?[`top`,`right`,`bottom`,`left`].forEach(t=>{e[t]===void 0?n[`${i}-${t}`]=a:n[`${i}-${t}`]=typeof e[t]==`number`?`${e[t]}px`:e[t]}):o(a)}),n}var U=v.forwardRef(function(e,t){let{id:n,invert:r,position:i=`bottom-right`,hotkey:a=[`altKey`,`KeyT`],expand:o,closeButton:s,className:c,offset:l,mobileOffset:u,theme:d=`light`,richColors:f,duration:p,style:m,visibleToasts:ee=F,toastOptions:h,dir:g=H(),gap:_=B,icons:b,customAriaLabel:te,containerAriaLabel:x=`Notifications`}=e,[S,C]=v.useState([]),w=v.useMemo(()=>n?S.filter(e=>e.toasterId===n):S.filter(e=>!e.toasterId),[S,n]),T=v.useMemo(()=>Array.from(new Set([i].concat(w.filter(e=>e.position).map(e=>e.position)))),[w,i]),[E,ne]=v.useState([]),[re,D]=v.useState(!1),[O,k]=v.useState(!1),[j,M]=v.useState(d===`system`?typeof window<`u`&&window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`:d),N=v.useRef(null),P=a.join(`+`).replace(/Key/g,``).replace(/Digit/g,``),I=v.useRef(null),L=v.useRef(!1),R=v.useCallback(e=>{C(t=>(t.find(t=>t.id===e.id)?.delete||A.dismiss(e.id),t.filter(({id:t})=>t!==e.id)))},[]);return v.useEffect(()=>A.subscribe(e=>{if(e.dismiss){requestAnimationFrame(()=>{C(t=>t.map(t=>t.id===e.id?{...t,delete:!0}:t))});return}setTimeout(()=>{y.flushSync(()=>{C(t=>{let n=t.findIndex(t=>t.id===e.id);return n===-1?[e,...t]:[...t.slice(0,n),{...t[n],...e},...t.slice(n+1)]})})})}),[]),v.useEffect(()=>{if(d!==`system`){M(d);return}if(d===`system`&&(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?M(`dark`):M(`light`)),typeof window>`u`)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`);try{e.addEventListener(`change`,({matches:e})=>{M(e?`dark`:`light`)})}catch{e.addListener(({matches:e})=>{try{M(e?`dark`:`light`)}catch(e){console.error(e)}})}},[d]),v.useEffect(()=>{S.length<=1&&D(!1)},[S]),v.useEffect(()=>{let e=e=>{if(a.length>0&&a.every(t=>e[t]||e.code===t)){var t;D(!0),(t=N.current)==null||t.focus()}e.code===`Escape`&&(document.activeElement===N.current||N.current?.contains(document.activeElement))&&D(!1)};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[a]),v.useEffect(()=>{if(N.current)return()=>{I.current&&(I.current.focus({preventScroll:!0}),I.current=null,L.current=!1)}},[N.current]),v.createElement(`section`,{ref:t,"aria-label":te??`${x} ${P}`,tabIndex:-1,"aria-live":`polite`,"aria-relevant":`additions text`,"aria-atomic":`false`,suppressHydrationWarning:!0,"data-react-aria-top-layer":!0},T.map((t,n)=>{let[i,a]=t.split(`-`);return w.length?v.createElement(`ol`,{key:t,dir:g===`auto`?H():g,tabIndex:-1,ref:N,className:c,"data-sonner-toaster":!0,"data-sonner-theme":j,"data-y-position":i,"data-x-position":a,style:{"--front-toast-height":`${E[0]?.height||0}px`,"--width":`${z}px`,"--gap":`${_}px`,...m,...ce(l,u)},onBlur:e=>{L.current&&!e.currentTarget.contains(e.relatedTarget)&&(L.current=!1,I.current&&=(I.current.focus({preventScroll:!0}),null))},onFocus:e=>{e.target instanceof HTMLElement&&e.target.dataset.dismissible===`false`||L.current||(L.current=!0,I.current=e.relatedTarget)},onMouseEnter:()=>D(!0),onMouseMove:()=>D(!0),onMouseLeave:()=>{O||D(!1)},onDragEnd:()=>D(!1),onPointerDown:e=>{e.target instanceof HTMLElement&&e.target.dataset.dismissible===`false`||k(!0)},onPointerUp:()=>k(!1)},w.filter(e=>!e.position&&n===0||e.position===t).map((n,i)=>v.createElement(se,{key:n.id,icons:b,index:i,toast:n,defaultRichColors:f,duration:h?.duration??p,className:h?.className,descriptionClassName:h?.descriptionClassName,invert:r,visibleToasts:ee,closeButton:h?.closeButton??s,interacting:O,position:t,style:h?.style,unstyled:h?.unstyled,classNames:h?.classNames,cancelButtonStyle:h?.cancelButtonStyle,actionButtonStyle:h?.actionButtonStyle,closeButtonAriaLabel:h?.closeButtonAriaLabel,removeToast:R,toasts:w.filter(e=>e.position==n.position),heights:E.filter(e=>e.position==n.position),setHeights:ne,expandByDefault:o,gap:_,expanded:re,swipeDirections:e.swipeDirections}))):null}))}),le=v.forwardRef(function(e,t){let{render:n,className:r,id:c,name:y,value:b,disabled:te=!1,onValueChange:x,defaultValue:S,autoFocus:C=!1,style:w,...T}=e,{state:E,name:ne,disabled:re,setTouched:D,setDirty:O,validityData:k,setFocused:A,setFilled:j,validationMode:M,validation:N}=m(),{clearErrors:P}=l(),F=re||te,I=ne??y,L={...E,disabled:F},{labelId:R}=d(),z=f({id:c});a(()=>{let e=b!=null;N.inputRef.current?.value||e&&b!==``?j(!0):e&&b===``&&j(!1)},[N.inputRef,j,b]);let B=v.useRef(null);a(()=>{C&&B.current===u(o(B.current))&&A(!0)},[C,A]);let[ie]=ee({controlled:b,default:S,name:`FieldControl`,state:`value`}),ae=b!==void 0,V=ae?ie:void 0,oe=s(()=>N.inputRef.current?.value);return _(N.inputRef,z,V,oe,!F,y),i(`input`,e,{ref:[t,B],state:L,props:[{id:z,disabled:F,name:I,ref:N.inputRef,"aria-labelledby":R,autoFocus:C,...ae?{value:V}:{defaultValue:S},onChange(e){let t=e.currentTarget.value;x?.(t,g(h,e.nativeEvent)),O(t!==(k.initialValue??``)),j(t!==``),e.nativeEvent.defaultPrevented||(P(I),N.change(t))},onFocus(){A(!0)},onBlur(e){D(!0),A(!1),M===`onBlur`&&N.commit(e.currentTarget.value)},onKeyDown(e){e.currentTarget.tagName===`INPUT`&&e.key===`Enter`&&(D(!0),N.commit(e.currentTarget.value))}},T,e=>N.getValidationProps(F,e)],stateAttributesMapping:p})}),W=n(),G=v.forwardRef(function(e,t){return(0,W.jsx)(le,{ref:t,...e})});function ue({className:e,type:t,...n}){return(0,W.jsx)(G,{type:t,"data-slot":`input`,className:c(`h-9 w-full min-w-0 rounded-4xl border border-input bg-input/30 px-3 py-1 text-base transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40`,e),...n})}export{U as n,N as r,ue as t}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/input-IDkwig6l.js b/internal/webassets/build/client/assets/input-IDkwig6l.js new file mode 100644 index 0000000..b944d5b --- /dev/null +++ b/internal/webassets/build/client/assets/input-IDkwig6l.js @@ -0,0 +1 @@ +import{N as e,j as t,t as n}from"./jsx-runtime-D0QO-s14.js";import{t as r}from"./react-dom-B1AcAK82.js";import{E as i,L as a,P as o,R as s,w as c}from"./api-k_NcBvGr.js";import{C as l,D as u,I as d,O as f,P as p,S as m,g as ee,j as h,t as g,w as _}from"./createBaseUIEventDetails-wEMuC0RZ.js";var v=e(t(),1),y=e(r(),1);function b(e){if(!e||typeof document>`u`)return;let t=document.head||document.getElementsByTagName(`head`)[0],n=document.createElement(`style`);n.type=`text/css`,t.appendChild(n),n.styleSheet?n.styleSheet.cssText=e:n.appendChild(document.createTextNode(e))}var te=e=>{switch(e){case`success`:return C;case`info`:return T;case`warning`:return w;case`error`:return E;default:return null}},x=Array(12).fill(0),S=({visible:e,className:t})=>v.createElement(`div`,{className:[`sonner-loading-wrapper`,t].filter(Boolean).join(` `),"data-visible":e},v.createElement(`div`,{className:`sonner-spinner`},x.map((e,t)=>v.createElement(`div`,{className:`sonner-loading-bar`,key:`spinner-bar-${t}`})))),C=v.createElement(`svg`,{xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 20 20`,fill:`currentColor`,height:`20`,width:`20`,"aria-hidden":`true`},v.createElement(`path`,{fillRule:`evenodd`,d:`M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z`,clipRule:`evenodd`})),w=v.createElement(`svg`,{xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 24 24`,fill:`currentColor`,height:`20`,width:`20`,"aria-hidden":`true`},v.createElement(`path`,{fillRule:`evenodd`,d:`M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z`,clipRule:`evenodd`})),T=v.createElement(`svg`,{xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 20 20`,fill:`currentColor`,height:`20`,width:`20`,"aria-hidden":`true`},v.createElement(`path`,{fillRule:`evenodd`,d:`M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z`,clipRule:`evenodd`})),E=v.createElement(`svg`,{xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 20 20`,fill:`currentColor`,height:`20`,width:`20`,"aria-hidden":`true`},v.createElement(`path`,{fillRule:`evenodd`,d:`M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z`,clipRule:`evenodd`})),ne=v.createElement(`svg`,{xmlns:`http://www.w3.org/2000/svg`,width:`12`,height:`12`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.5`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`},v.createElement(`line`,{x1:`18`,y1:`6`,x2:`6`,y2:`18`}),v.createElement(`line`,{x1:`6`,y1:`6`,x2:`18`,y2:`18`})),re=()=>{let[e,t]=v.useState(document.hidden);return v.useEffect(()=>{let e=()=>{t(document.hidden)};return document.addEventListener(`visibilitychange`,e),()=>document.removeEventListener(`visibilitychange`,e)},[]),e},D=1,O=100,k=e=>typeof e?.id==`number`||e?.id?.length>0?e.id:D++,A=new class{constructor(){this.subscribe=e=>(this.subscribers.push(e),this.getActiveToasts().forEach(t=>e(t)),()=>{let t=this.subscribers.indexOf(e);this.subscribers.splice(t,1)}),this.publish=e=>{this.subscribers.forEach(t=>t(e))},this.addToast=e=>{this.publish(e),this.toasts=[...this.toasts,e],this.trimHistory()},this.trimHistory=()=>{let e=this.toasts.length-O;e<=0||(this.toasts=this.toasts.filter(t=>e>0&&this.dismissedToasts.has(t.id)?(this.dismissedToasts.delete(t.id),e--,!1):!0))},this.create=e=>{let{message:t,...n}=e,r=k(e),i=this.pendingDismissals.get(r);i!==void 0&&(cancelAnimationFrame(i),this.pendingDismissals.delete(r),this.dismissedToasts.delete(r));let a=this.dismissedToasts.has(r),o=e.dismissible===void 0?!0:e.dismissible;return a&&(this.dismissedToasts.delete(r),this.toasts=this.toasts.filter(e=>e.id!==r)),!a&&this.toasts.find(e=>e.id===r)?this.toasts=this.toasts.map(n=>n.id===r?(this.publish({...n,...e,id:r,title:t}),{...n,...e,id:r,dismissible:o,title:t}):n):this.addToast({title:t,...n,dismissible:o,id:r}),r},this.dismiss=e=>{if(e==null)return this.getActiveToasts().forEach(e=>{this.dismissedToasts.add(e.id),this.subscribers.forEach(t=>t({id:e.id,dismiss:!0}))}),e;this.dismissedToasts.add(e);let t=this.pendingDismissals.get(e);return t!==void 0&&cancelAnimationFrame(t),this.pendingDismissals.set(e,requestAnimationFrame(()=>{this.pendingDismissals.delete(e),this.subscribers.forEach(t=>t({id:e,dismiss:!0}))})),e},this.message=(e,t)=>this.create({...t,message:e,type:void 0}),this.error=(e,t)=>this.create({...t,message:e,type:`error`}),this.success=(e,t)=>this.create({...t,type:`success`,message:e}),this.info=(e,t)=>this.create({...t,type:`info`,message:e}),this.warning=(e,t)=>this.create({...t,type:`warning`,message:e}),this.loading=(e,t)=>this.create({...t,type:`loading`,message:e}),this.promise=(e,t)=>{if(!t)return;let n;t.loading!==void 0&&(n=this.create({...t,promise:e,type:`loading`,message:t.loading,description:typeof t.description==`function`?void 0:t.description}));let r=Promise.resolve(e instanceof Function?e():e),i=n!==void 0,a,o=r.then(async e=>{if(a=[`resolve`,e],v.isValidElement(e))i=!1,this.create({id:n,type:`default`,message:e});else if(M(e)&&!e.ok){i=!1;let r=typeof t.error==`function`?await t.error(`HTTP error! status: ${e.status}`):t.error,a=typeof t.description==`function`?await t.description(`HTTP error! status: ${e.status}`):t.description,o=typeof r==`object`&&!v.isValidElement(r)?r:{message:r};this.create({id:n,type:`error`,description:a,...o})}else if(e instanceof Error){i=!1;let r=typeof t.error==`function`?await t.error(e):t.error,a=typeof t.description==`function`?await t.description(e):t.description,o=typeof r==`object`&&!v.isValidElement(r)?r:{message:r};this.create({id:n,type:`error`,description:a,...o})}else if(t.success!==void 0){i=!1;let r=typeof t.success==`function`?await t.success(e):t.success,a=typeof t.description==`function`?await t.description(e):t.description,o=typeof r==`object`&&!v.isValidElement(r)?r:{message:r};this.create({id:n,type:`success`,description:a,...o})}}).catch(async e=>{if(a=[`reject`,e],t.error!==void 0){i=!1;let r=typeof t.error==`function`?await t.error(e):t.error,a=typeof t.description==`function`?await t.description(e):t.description,o=typeof r==`object`&&!v.isValidElement(r)?r:{message:r};this.create({id:n,type:`error`,description:a,...o})}}).finally(()=>{i&&(this.dismiss(n),n=void 0),t.finally==null||t.finally.call(t)}),s=()=>new Promise((e,t)=>o.then(()=>a[0]===`reject`?t(a[1]):e(a[1])).catch(t));return typeof n!=`string`&&typeof n!=`number`?{unwrap:s}:Object.assign(n,{unwrap:s})},this.custom=(e,t)=>{let n=k(t);return this.create({...t,jsx:e(n),id:n,type:void 0}),n},this.getActiveToasts=()=>this.toasts.filter(e=>!this.dismissedToasts.has(e.id)),this.subscribers=[],this.toasts=[],this.dismissedToasts=new Set,this.pendingDismissals=new Map}},j=(e,t)=>A.message(e,t),M=e=>e&&typeof e==`object`&&`ok`in e&&typeof e.ok==`boolean`&&`status`in e&&typeof e.status==`number`,N=Object.assign(j,{success:A.success,info:A.info,warning:A.warning,error:A.error,custom:A.custom,message:A.message,promise:A.promise,dismiss:A.dismiss,loading:A.loading},{getHistory:()=>A.toasts,getToasts:()=>A.getActiveToasts()});b(`[data-sonner-toaster][dir=ltr],html[dir=ltr]{--toast-icon-margin-start:-3px;--toast-icon-margin-end:4px;--toast-svg-margin-start:-1px;--toast-svg-margin-end:0px;--toast-button-margin-start:auto;--toast-button-margin-end:0;--toast-close-button-start:0;--toast-close-button-end:unset;--toast-close-button-transform:translate(-35%, -35%)}[data-sonner-toaster][dir=rtl],html[dir=rtl]{--toast-icon-margin-start:4px;--toast-icon-margin-end:-3px;--toast-svg-margin-start:0px;--toast-svg-margin-end:-1px;--toast-button-margin-start:0;--toast-button-margin-end:auto;--toast-close-button-start:unset;--toast-close-button-end:0;--toast-close-button-transform:translate(35%, -35%)}[data-sonner-toaster]{position:fixed;width:var(--width);font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;--gray1:hsl(0, 0%, 99%);--gray2:hsl(0, 0%, 97.3%);--gray3:hsl(0, 0%, 95.1%);--gray4:hsl(0, 0%, 93%);--gray5:hsl(0, 0%, 90.9%);--gray6:hsl(0, 0%, 88.7%);--gray7:hsl(0, 0%, 85.8%);--gray8:hsl(0, 0%, 78%);--gray9:hsl(0, 0%, 56.1%);--gray10:hsl(0, 0%, 52.3%);--gray11:hsl(0, 0%, 43.5%);--gray12:hsl(0, 0%, 9%);--border-radius:8px;box-sizing:border-box;padding:0;margin:0;list-style:none;outline:0;z-index:999999999;transition:transform .4s ease}@media (hover:none) and (pointer:coarse){[data-sonner-toaster][data-lifted=true]{transform:none}}[data-sonner-toaster][data-x-position=right]{right:var(--offset-right)}[data-sonner-toaster][data-x-position=left]{left:var(--offset-left)}[data-sonner-toaster][data-x-position=center]{left:50%;transform:translateX(-50%)}[data-sonner-toaster][data-y-position=top]{top:var(--offset-top)}[data-sonner-toaster][data-y-position=bottom]{bottom:var(--offset-bottom)}[data-sonner-toast]{--y:translateY(100%);--lift-amount:calc(var(--lift) * var(--gap));z-index:var(--z-index);position:absolute;opacity:0;transform:var(--y);touch-action:none;transition:transform .4s,opacity .4s,height .4s,box-shadow .2s;box-sizing:border-box;outline:0;overflow-wrap:anywhere}[data-sonner-toast][data-styled=true]{padding:16px;background:var(--normal-bg);border:1px solid var(--normal-border);color:var(--normal-text);border-radius:var(--border-radius);box-shadow:0 4px 12px rgba(0,0,0,.1);width:var(--width);font-size:13px;display:flex;align-items:center;gap:6px}[data-sonner-toast]:focus-visible{box-shadow:0 4px 12px rgba(0,0,0,.1),0 0 0 2px rgba(0,0,0,.2)}[data-sonner-toast][data-y-position=top]{top:0;--y:translateY(-100%);--lift:1;--lift-amount:calc(1 * var(--gap))}[data-sonner-toast][data-y-position=bottom]{bottom:0;--y:translateY(100%);--lift:-1;--lift-amount:calc(var(--lift) * var(--gap))}[data-sonner-toast][data-styled=true] [data-description]{font-weight:400;line-height:1.4;color:#3f3f3f}[data-rich-colors=true][data-sonner-toast][data-styled=true] [data-description]{color:inherit}[data-sonner-toaster][data-sonner-theme=dark] [data-description]{color:#e8e8e8}[data-sonner-toast][data-styled=true] [data-title]{font-weight:500;line-height:1.5;color:inherit}[data-sonner-toast][data-styled=true] [data-icon]{display:flex;height:16px;width:16px;position:relative;justify-content:flex-start;align-items:center;flex-shrink:0;margin-left:var(--toast-icon-margin-start);margin-right:var(--toast-icon-margin-end)}[data-sonner-toast][data-promise=true] [data-icon]>svg{opacity:0;transform:scale(.8);transform-origin:center;animation:sonner-fade-in .3s ease forwards}[data-sonner-toast][data-styled=true] [data-icon]>*{flex-shrink:0}[data-sonner-toast][data-styled=true] [data-icon] svg{margin-left:var(--toast-svg-margin-start);margin-right:var(--toast-svg-margin-end)}[data-sonner-toast][data-styled=true] [data-content]{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}[data-sonner-toast][data-styled=true] [data-button]{border-radius:4px;padding-left:8px;padding-right:8px;height:24px;font-size:12px;color:var(--normal-bg);background:var(--normal-text);margin-left:var(--toast-button-margin-start);margin-right:var(--toast-button-margin-end);border:none;font-weight:500;cursor:pointer;outline:0;display:flex;align-items:center;flex-shrink:0;transition:opacity .4s,box-shadow .2s}[data-sonner-toast][data-styled=true] [data-button]:focus-visible{box-shadow:0 0 0 2px rgba(0,0,0,.4)}[data-sonner-toast][data-styled=true] [data-button]:first-of-type{margin-left:var(--toast-button-margin-start);margin-right:var(--toast-button-margin-end)}[data-sonner-toast][data-styled=true] [data-cancel]{color:var(--normal-text);background:rgba(0,0,0,.08)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast][data-styled=true] [data-cancel]{background:rgba(255,255,255,.3)}[data-sonner-toast][data-styled=true] [data-close-button]{position:absolute;left:var(--toast-close-button-start);right:var(--toast-close-button-end);top:0;height:20px;width:20px;display:flex;justify-content:center;align-items:center;padding:0;color:var(--normal-text);background:var(--normal-bg);border:1px solid var(--normal-border);transform:var(--toast-close-button-transform);border-radius:50%;cursor:pointer;z-index:1;transition:opacity .1s,background .2s,border-color .2s}[data-sonner-toast][data-styled=true] [data-close-button]:focus-visible{box-shadow:0 4px 12px rgba(0,0,0,.1),0 0 0 2px rgba(0,0,0,.2)}[data-sonner-toast][data-styled=true] [data-disabled=true]{cursor:not-allowed}[data-sonner-toast][data-styled=true]:hover [data-close-button]:hover{background:var(--gray2);border-color:var(--gray5)}[data-sonner-toast][data-swiping=true]::before{content:'';position:absolute;left:-100%;right:-100%;height:100%;z-index:-1}[data-sonner-toast][data-y-position=top][data-swiping=true]::before{bottom:50%;transform:scaleY(3) translateY(50%)}[data-sonner-toast][data-y-position=bottom][data-swiping=true]::before{top:50%;transform:scaleY(3) translateY(-50%)}[data-sonner-toast][data-swiping=false][data-removed=true]::before{content:'';position:absolute;inset:0;transform:scaleY(2)}[data-sonner-toast][data-expanded=true]::after{content:'';position:absolute;left:0;height:calc(var(--gap) + 1px);bottom:100%;width:100%}[data-sonner-toast][data-mounted=true]{--y:translateY(0);opacity:1}[data-sonner-toast][data-expanded=false][data-front=false]{--scale:var(--toasts-before) * 0.05 + 1;--y:translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)));height:var(--front-toast-height)}[data-sonner-toast]>*{transition:opacity .4s}[data-sonner-toast][data-x-position=right]{right:0}[data-sonner-toast][data-x-position=left]{left:0}[data-sonner-toast][data-expanded=false][data-front=false][data-styled=true]>*{opacity:0}[data-sonner-toast][data-visible=false]{opacity:0;pointer-events:none}[data-sonner-toast][data-mounted=true][data-expanded=true]{--y:translateY(calc(var(--lift) * var(--offset)));height:var(--initial-height)}[data-sonner-toast][data-removed=true][data-front=true][data-swipe-out=false]{--y:translateY(calc(var(--lift) * -100%));opacity:0}[data-sonner-toast][data-removed=true][data-front=false][data-swipe-out=false][data-expanded=true]{--y:translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));opacity:0}[data-sonner-toast][data-removed=true][data-front=false][data-swipe-out=false][data-expanded=false]{--y:translateY(40%);opacity:0;transition:transform .5s,opacity .2s}[data-sonner-toast][data-removed=true][data-front=false]::before{height:calc(var(--initial-height) + 20%)}[data-sonner-toast][data-swiping=true]{transform:var(--y) translateY(var(--swipe-amount-y,0)) translateX(var(--swipe-amount-x,0));transition:none}[data-sonner-toast][data-swiped=true]{-webkit-user-select:none;user-select:none}[data-sonner-toast][data-swipe-out=true][data-y-position=bottom],[data-sonner-toast][data-swipe-out=true][data-y-position=top]{animation-duration:.2s;animation-timing-function:ease-out;animation-fill-mode:forwards}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=left]{animation-name:swipe-out-left}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=right]{animation-name:swipe-out-right}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=up]{animation-name:swipe-out-up}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=down]{animation-name:swipe-out-down}@keyframes swipe-out-left{from{transform:var(--y) translateX(var(--swipe-amount-x));opacity:1}to{transform:var(--y) translateX(calc(var(--swipe-amount-x) - 100%));opacity:0}}@keyframes swipe-out-right{from{transform:var(--y) translateX(var(--swipe-amount-x));opacity:1}to{transform:var(--y) translateX(calc(var(--swipe-amount-x) + 100%));opacity:0}}@keyframes swipe-out-up{from{transform:var(--y) translateY(var(--swipe-amount-y));opacity:1}to{transform:var(--y) translateY(calc(var(--swipe-amount-y) - 100%));opacity:0}}@keyframes swipe-out-down{from{transform:var(--y) translateY(var(--swipe-amount-y));opacity:1}to{transform:var(--y) translateY(calc(var(--swipe-amount-y) + 100%));opacity:0}}@media (max-width:600px){[data-sonner-toaster]{position:fixed;right:var(--mobile-offset-right);left:var(--mobile-offset-left);width:100%}[data-sonner-toaster][dir=rtl]{left:calc(var(--mobile-offset-left) * -1)}[data-sonner-toaster] [data-sonner-toast]{left:0;right:0;width:calc(100% - var(--mobile-offset-left) * 2)}[data-sonner-toaster][data-x-position=left]{left:var(--mobile-offset-left)}[data-sonner-toaster][data-y-position=bottom]{bottom:var(--mobile-offset-bottom)}[data-sonner-toaster][data-y-position=top]{top:var(--mobile-offset-top)}[data-sonner-toaster][data-x-position=center]{left:var(--mobile-offset-left);right:var(--mobile-offset-right);transform:none}}[data-sonner-toaster][data-sonner-theme=light]{--normal-bg:#fff;--normal-border:var(--gray4);--normal-text:var(--gray12);--success-bg:hsl(143, 85%, 96%);--success-border:hsl(145, 92%, 87%);--success-text:hsl(140, 100%, 27%);--info-bg:hsl(208, 100%, 97%);--info-border:hsl(221, 91%, 93%);--info-text:hsl(210, 92%, 45%);--warning-bg:hsl(49, 100%, 97%);--warning-border:hsl(49, 91%, 84%);--warning-text:hsl(31, 92%, 45%);--error-bg:hsl(359, 100%, 97%);--error-border:hsl(359, 100%, 94%);--error-text:hsl(360, 100%, 45%)}[data-sonner-toaster][data-sonner-theme=light] [data-sonner-toast][data-invert=true]{--normal-bg:#000;--normal-border:hsl(0, 0%, 20%);--normal-text:var(--gray1)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast][data-invert=true]{--normal-bg:#fff;--normal-border:var(--gray3);--normal-text:var(--gray12)}[data-sonner-toaster][data-sonner-theme=dark]{--normal-bg:#000;--normal-bg-hover:hsl(0, 0%, 12%);--normal-border:hsl(0, 0%, 20%);--normal-border-hover:hsl(0, 0%, 25%);--normal-text:var(--gray1);--success-bg:hsl(150, 100%, 6%);--success-border:hsl(147, 100%, 12%);--success-text:hsl(150, 86%, 65%);--info-bg:hsl(215, 100%, 6%);--info-border:hsl(223, 43%, 17%);--info-text:hsl(216, 87%, 65%);--warning-bg:hsl(64, 100%, 6%);--warning-border:hsl(60, 100%, 9%);--warning-text:hsl(46, 87%, 65%);--error-bg:hsl(358, 76%, 10%);--error-border:hsl(357, 89%, 16%);--error-text:hsl(358, 100%, 81%)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast] [data-close-button]{background:var(--normal-bg);border-color:var(--normal-border);color:var(--normal-text)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast] [data-close-button]:hover{background:var(--normal-bg-hover);border-color:var(--normal-border-hover)}[data-rich-colors=true][data-sonner-toast][data-type=success]{background:var(--success-bg);border-color:var(--success-border);color:var(--success-text)}[data-rich-colors=true][data-sonner-toast][data-type=success] [data-close-button]{background:var(--success-bg);border-color:var(--success-border);color:var(--success-text)}[data-rich-colors=true][data-sonner-toast][data-type=info]{background:var(--info-bg);border-color:var(--info-border);color:var(--info-text)}[data-rich-colors=true][data-sonner-toast][data-type=info] [data-close-button]{background:var(--info-bg);border-color:var(--info-border);color:var(--info-text)}[data-rich-colors=true][data-sonner-toast][data-type=warning]{background:var(--warning-bg);border-color:var(--warning-border);color:var(--warning-text)}[data-rich-colors=true][data-sonner-toast][data-type=warning] [data-close-button]{background:var(--warning-bg);border-color:var(--warning-border);color:var(--warning-text)}[data-rich-colors=true][data-sonner-toast][data-type=error]{background:var(--error-bg);border-color:var(--error-border);color:var(--error-text)}[data-rich-colors=true][data-sonner-toast][data-type=error] [data-close-button]{background:var(--error-bg);border-color:var(--error-border);color:var(--error-text)}.sonner-loading-wrapper{--size:16px;height:var(--size);width:var(--size);position:absolute;inset:0;z-index:10}.sonner-loading-wrapper[data-visible=false]{transform-origin:center;animation:sonner-fade-out .2s ease forwards}.sonner-spinner{position:relative;top:50%;left:50%;height:var(--size);width:var(--size)}.sonner-loading-bar{animation:sonner-spin 1.2s linear infinite;background:var(--gray11);border-radius:6px;height:8%;left:-10%;position:absolute;top:-3.9%;width:24%}.sonner-loading-bar:first-child{animation-delay:-1.2s;transform:rotate(.0001deg) translate(146%)}.sonner-loading-bar:nth-child(2){animation-delay:-1.1s;transform:rotate(30deg) translate(146%)}.sonner-loading-bar:nth-child(3){animation-delay:-1s;transform:rotate(60deg) translate(146%)}.sonner-loading-bar:nth-child(4){animation-delay:-.9s;transform:rotate(90deg) translate(146%)}.sonner-loading-bar:nth-child(5){animation-delay:-.8s;transform:rotate(120deg) translate(146%)}.sonner-loading-bar:nth-child(6){animation-delay:-.7s;transform:rotate(150deg) translate(146%)}.sonner-loading-bar:nth-child(7){animation-delay:-.6s;transform:rotate(180deg) translate(146%)}.sonner-loading-bar:nth-child(8){animation-delay:-.5s;transform:rotate(210deg) translate(146%)}.sonner-loading-bar:nth-child(9){animation-delay:-.4s;transform:rotate(240deg) translate(146%)}.sonner-loading-bar:nth-child(10){animation-delay:-.3s;transform:rotate(270deg) translate(146%)}.sonner-loading-bar:nth-child(11){animation-delay:-.2s;transform:rotate(300deg) translate(146%)}.sonner-loading-bar:nth-child(12){animation-delay:-.1s;transform:rotate(330deg) translate(146%)}@keyframes sonner-fade-in{0%{opacity:0;transform:scale(.8)}100%{opacity:1;transform:scale(1)}}@keyframes sonner-fade-out{0%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(.8)}}@keyframes sonner-spin{0%{opacity:1}100%{opacity:.15}}@media (prefers-reduced-motion){.sonner-loading-bar,[data-sonner-toast],[data-sonner-toast]>*{transition:none!important;animation:none!important}}.sonner-loader{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);transform-origin:center;transition:opacity .2s,transform .2s}.sonner-loader[data-visible=false]{opacity:0;transform:scale(.8) translate(-50%,-50%)}`);function P(e){return e.label!==void 0}var F=3,I=`24px`,L=`16px`,R=4e3,z=356,B=14,ie=45,ae=200;function V(...e){return e.filter(Boolean).join(` `)}function oe(e){let[t,n]=e.split(`-`),r=[];return t&&r.push(t),n&&r.push(n),r}var se=e=>{let{invert:t,toast:n,unstyled:r,interacting:i,setHeights:a,visibleToasts:o,heights:s,index:c,toasts:l,expanded:u,removeToast:d,defaultRichColors:f,closeButton:p,style:m,cancelButtonStyle:ee,actionButtonStyle:h,className:g=``,descriptionClassName:_=``,duration:y,position:b,gap:x,expandByDefault:C,classNames:w,icons:T,closeButtonAriaLabel:E=`Close toast`}=e,[D,O]=v.useState(null),[k,A]=v.useState(null),[j,M]=v.useState(!1),[N,F]=v.useState(!1),[I,L]=v.useState(!1),[z,B]=v.useState(!1),[se,H]=v.useState(!1),[ce,U]=v.useState(0),[le,W]=v.useState(0),G=v.useRef(n.duration||y||R),ue=v.useRef(null),K=v.useRef(null),de=c===0,fe=c+1<=o,q=n.type,pe=q??`default`,J=n.dismissible!==!1,me=n.className||``,he=n.descriptionClassName||``,Y=v.useMemo(()=>s.findIndex(e=>e.toastId===n.id)||0,[s,n.id]),ge=v.useMemo(()=>n.closeButton??p,[n.closeButton,p]),_e=v.useMemo(()=>n.duration||y||R,[n.duration,y]),ve=v.useRef(0),X=v.useRef(0),ye=v.useRef(0),Z=v.useRef(null),[be,xe]=b.split(`-`),Se=v.useMemo(()=>s.reduce((e,t,n)=>n>=Y?e:e+t.height,0),[s,Y]),Ce=re(),Q=v.useMemo(()=>e.swipeDirections??oe(b),[e.swipeDirections,b]),we=n.invert||t,Te=q===`loading`;X.current=v.useMemo(()=>Y*x+Se,[Y,Se]),v.useEffect(()=>{G.current=_e},[_e]),v.useEffect(()=>{M(!0)},[]),v.useEffect(()=>{let e=K.current;if(e){let t=e.getBoundingClientRect().height;return W(t),a(e=>[{toastId:n.id,height:t,position:n.position},...e]),()=>a(e=>e.filter(e=>e.toastId!==n.id))}},[a,n.id]),v.useLayoutEffect(()=>{if(!j)return;let e=K.current,t=e.style.height;e.style.height=`auto`;let r=e.getBoundingClientRect().height;e.style.height=t,W(r),a(e=>e.find(e=>e.toastId===n.id)?e.map(e=>e.toastId===n.id?{...e,height:r}:e):[{toastId:n.id,height:r,position:n.position},...e])},[j,n.title,n.description,a,n.id,n.jsx,n.action,n.cancel]);let $=v.useCallback(()=>{F(!0),U(X.current),a(e=>e.filter(e=>e.toastId!==n.id)),setTimeout(()=>{d(n)},ae)},[n,d,a,X]);v.useEffect(()=>{if(n.promise&&q===`loading`||n.duration===1/0||n.type===`loading`)return;let e;return u||i||Ce?(()=>{if(ye.current{n.onAutoClose==null||n.onAutoClose.call(n,n),$()},G.current)),()=>clearTimeout(e)},[u,i,n,q,Ce,$]),v.useEffect(()=>{n.delete&&($(),n.onDismiss==null||n.onDismiss.call(n,n))},[$,n.delete]);function Ee(){return T?.loading?v.createElement(`div`,{className:V(w?.loader,n?.classNames?.loader,`sonner-loader`),"data-visible":q===`loading`},T.loading):v.createElement(S,{className:V(w?.loader,n?.classNames?.loader),visible:q===`loading`})}let De=n.icon||T?.[q]||te(q);return v.createElement(`li`,{tabIndex:0,ref:K,className:V(g,me,w?.toast,n?.classNames?.toast,w?.[pe],n?.classNames?.[pe]),"data-sonner-toast":``,"data-rich-colors":n.richColors??f,"data-styled":!(n.jsx||n.unstyled||r),"data-mounted":j,"data-promise":!!n.promise,"data-swiped":se,"data-removed":N,"data-visible":fe,"data-y-position":be,"data-x-position":xe,"data-index":c,"data-front":de,"data-swiping":I,"data-dismissible":J,"data-type":q,"data-invert":we,"data-swipe-out":z,"data-swipe-direction":k,"data-expanded":!!(u||C&&j),"data-testid":n.testId,style:{"--index":c,"--toasts-before":c,"--z-index":l.length-c,"--offset":`${N?ce:X.current}px`,"--initial-height":C?`auto`:`${le}px`,...m,...n.style},onDragEnd:()=>{L(!1),O(null),Z.current=null},onPointerDown:e=>{e.button!==2&&(Te||!J||(ue.current=new Date,U(X.current),e.target.setPointerCapture(e.pointerId),e.target.tagName!==`BUTTON`&&(L(!0),Z.current={x:e.clientX,y:e.clientY})))},onPointerUp:()=>{if(z||!J)return;Z.current=null;let e=Number(K.current?.style.getPropertyValue(`--swipe-amount-x`).replace(`px`,``)||0),t=Number(K.current?.style.getPropertyValue(`--swipe-amount-y`).replace(`px`,``)||0),r=new Date().getTime()-ue.current?.getTime(),i=D===`x`?e:t,a=Math.abs(i)/r;if((D===`x`?Q.includes(e>0?`right`:`left`):Q.includes(t>0?`bottom`:`top`))&&(Math.abs(i)>=ie||a>.11)){U(X.current),n.onDismiss==null||n.onDismiss.call(n,n),A(D===`x`?e>0?`right`:`left`:t>0?`down`:`up`),$(),B(!0);return}else{var o,s;(o=K.current)==null||o.style.setProperty(`--swipe-amount-x`,`0px`),(s=K.current)==null||s.style.setProperty(`--swipe-amount-y`,`0px`)}H(!1),L(!1),O(null)},onPointerMove:e=>{var t,n;if(!Z.current||!J||window.getSelection()?.toString().length>0)return;let r=e.clientY-Z.current.y,i=e.clientX-Z.current.x;!D&&(Math.abs(i)>1||Math.abs(r)>1)&&O(Math.abs(i)>Math.abs(r)?`x`:`y`);let a={x:0,y:0},o=e=>1/(1.5+Math.abs(e)/20);if(D===`y`){if(Q.includes(`top`)||Q.includes(`bottom`))if(Q.includes(`top`)&&r<0||Q.includes(`bottom`)&&r>0)a.y=r;else{let e=r*o(r);a.y=Math.abs(e)0)a.x=i;else{let e=i*o(i);a.x=Math.abs(e)0||Math.abs(a.y)>0)&&H(!0),(t=K.current)==null||t.style.setProperty(`--swipe-amount-x`,`${a.x}px`),(n=K.current)==null||n.style.setProperty(`--swipe-amount-y`,`${a.y}px`)}},ge&&!n.jsx&&q!==`loading`?v.createElement(`button`,{"aria-label":E,"data-disabled":Te,"data-close-button":!0,onClick:Te||!J?()=>{}:()=>{$(),n.onDismiss==null||n.onDismiss.call(n,n)},className:V(w?.closeButton,n?.classNames?.closeButton)},T?.close??ne):null,(q||n.icon||n.promise)&&n.icon!==null&&(T?.[q]!==null||n.icon)?v.createElement(`div`,{"data-icon":``,className:V(w?.icon,n?.classNames?.icon)},q===`loading`?n.icon||Ee():n.promise?Ee():null,q===`loading`?null:De):null,v.createElement(`div`,{"data-content":``,className:V(w?.content,n?.classNames?.content)},v.createElement(`div`,{"data-title":``,className:V(w?.title,n?.classNames?.title)},n.jsx?n.jsx:typeof n.title==`function`?n.title():n.title),n.description?v.createElement(`div`,{"data-description":``,className:V(_,he,w?.description,n?.classNames?.description)},typeof n.description==`function`?n.description():n.description):null),v.isValidElement(n.cancel)?n.cancel:n.cancel&&P(n.cancel)?v.createElement(`button`,{"data-button":!0,"data-cancel":!0,style:n.cancelButtonStyle||ee,onClick:e=>{P(n.cancel)&&J&&(n.cancel.onClick==null||n.cancel.onClick.call(n.cancel,e),$())},className:V(w?.cancelButton,n?.classNames?.cancelButton)},n.cancel.label):null,v.isValidElement(n.action)?n.action:n.action&&P(n.action)?v.createElement(`button`,{"data-button":!0,"data-action":!0,style:n.actionButtonStyle||h,onClick:e=>{P(n.action)&&(n.action.onClick==null||n.action.onClick.call(n.action,e),!e.defaultPrevented&&$())},className:V(w?.actionButton,n?.classNames?.actionButton)},n.action.label):null)};function H(){if(typeof window>`u`||typeof document>`u`)return`ltr`;let e=document.documentElement.getAttribute(`dir`);return e===`auto`||!e?window.getComputedStyle(document.documentElement).direction:e}function ce(e,t){let n={};return[e,t].forEach((e,t)=>{let r=t===1,i=r?`--mobile-offset`:`--offset`,a=r?L:I;function o(e){[`top`,`right`,`bottom`,`left`].forEach(t=>{n[`${i}-${t}`]=typeof e==`number`?`${e}px`:e})}typeof e==`number`||typeof e==`string`?o(e):typeof e==`object`?[`top`,`right`,`bottom`,`left`].forEach(t=>{e[t]===void 0?n[`${i}-${t}`]=a:n[`${i}-${t}`]=typeof e[t]==`number`?`${e[t]}px`:e[t]}):o(a)}),n}var U=v.forwardRef(function(e,t){let{id:n,invert:r,position:i=`bottom-right`,hotkey:a=[`altKey`,`KeyT`],expand:o,closeButton:s,className:c,offset:l,mobileOffset:u,theme:d=`light`,richColors:f,duration:p,style:m,visibleToasts:ee=F,toastOptions:h,dir:g=H(),gap:_=B,icons:b,customAriaLabel:te,containerAriaLabel:x=`Notifications`}=e,[S,C]=v.useState([]),w=v.useMemo(()=>n?S.filter(e=>e.toasterId===n):S.filter(e=>!e.toasterId),[S,n]),T=v.useMemo(()=>Array.from(new Set([i].concat(w.filter(e=>e.position).map(e=>e.position)))),[w,i]),[E,ne]=v.useState([]),[re,D]=v.useState(!1),[O,k]=v.useState(!1),[j,M]=v.useState(d===`system`?typeof window<`u`&&window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`:d),N=v.useRef(null),P=a.join(`+`).replace(/Key/g,``).replace(/Digit/g,``),I=v.useRef(null),L=v.useRef(!1),R=v.useCallback(e=>{C(t=>(t.find(t=>t.id===e.id)?.delete||A.dismiss(e.id),t.filter(({id:t})=>t!==e.id)))},[]);return v.useEffect(()=>A.subscribe(e=>{if(e.dismiss){requestAnimationFrame(()=>{C(t=>t.map(t=>t.id===e.id?{...t,delete:!0}:t))});return}setTimeout(()=>{y.flushSync(()=>{C(t=>{let n=t.findIndex(t=>t.id===e.id);return n===-1?[e,...t]:[...t.slice(0,n),{...t[n],...e},...t.slice(n+1)]})})})}),[]),v.useEffect(()=>{if(d!==`system`){M(d);return}if(d===`system`&&(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?M(`dark`):M(`light`)),typeof window>`u`)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`);try{e.addEventListener(`change`,({matches:e})=>{M(e?`dark`:`light`)})}catch{e.addListener(({matches:e})=>{try{M(e?`dark`:`light`)}catch(e){console.error(e)}})}},[d]),v.useEffect(()=>{S.length<=1&&D(!1)},[S]),v.useEffect(()=>{let e=e=>{if(a.length>0&&a.every(t=>e[t]||e.code===t)){var t;D(!0),(t=N.current)==null||t.focus()}e.code===`Escape`&&(document.activeElement===N.current||N.current?.contains(document.activeElement))&&D(!1)};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[a]),v.useEffect(()=>{if(N.current)return()=>{I.current&&(I.current.focus({preventScroll:!0}),I.current=null,L.current=!1)}},[N.current]),v.createElement(`section`,{ref:t,"aria-label":te??`${x} ${P}`,tabIndex:-1,"aria-live":`polite`,"aria-relevant":`additions text`,"aria-atomic":`false`,suppressHydrationWarning:!0,"data-react-aria-top-layer":!0},T.map((t,n)=>{let[i,a]=t.split(`-`);return w.length?v.createElement(`ol`,{key:t,dir:g===`auto`?H():g,tabIndex:-1,ref:N,className:c,"data-sonner-toaster":!0,"data-sonner-theme":j,"data-y-position":i,"data-x-position":a,style:{"--front-toast-height":`${E[0]?.height||0}px`,"--width":`${z}px`,"--gap":`${_}px`,...m,...ce(l,u)},onBlur:e=>{L.current&&!e.currentTarget.contains(e.relatedTarget)&&(L.current=!1,I.current&&=(I.current.focus({preventScroll:!0}),null))},onFocus:e=>{e.target instanceof HTMLElement&&e.target.dataset.dismissible===`false`||L.current||(L.current=!0,I.current=e.relatedTarget)},onMouseEnter:()=>D(!0),onMouseMove:()=>D(!0),onMouseLeave:()=>{O||D(!1)},onDragEnd:()=>D(!1),onPointerDown:e=>{e.target instanceof HTMLElement&&e.target.dataset.dismissible===`false`||k(!0)},onPointerUp:()=>k(!1)},w.filter(e=>!e.position&&n===0||e.position===t).map((n,i)=>v.createElement(se,{key:n.id,icons:b,index:i,toast:n,defaultRichColors:f,duration:h?.duration??p,className:h?.className,descriptionClassName:h?.descriptionClassName,invert:r,visibleToasts:ee,closeButton:h?.closeButton??s,interacting:O,position:t,style:h?.style,unstyled:h?.unstyled,classNames:h?.classNames,cancelButtonStyle:h?.cancelButtonStyle,actionButtonStyle:h?.actionButtonStyle,closeButtonAriaLabel:h?.closeButtonAriaLabel,removeToast:R,toasts:w.filter(e=>e.position==n.position),heights:E.filter(e=>e.position==n.position),setHeights:ne,expandByDefault:o,gap:_,expanded:re,swipeDirections:e.swipeDirections}))):null}))}),le=v.forwardRef(function(e,t){let{render:n,className:r,id:c,name:y,value:b,disabled:te=!1,onValueChange:x,defaultValue:S,autoFocus:C=!1,style:w,...T}=e,{state:E,name:ne,disabled:re,setTouched:D,setDirty:O,validityData:k,setFocused:A,setFilled:j,validationMode:M,validation:N}=p(),{clearErrors:P}=h(),F=re||te,I=ne??y,L={...E,disabled:F},{labelId:R}=f(),z=u({id:c});a(()=>{let e=b!=null;N.inputRef.current?.value||e&&b!==``?j(!0):e&&b===``&&j(!1)},[N.inputRef,j,b]);let B=v.useRef(null);a(()=>{C&&B.current===_(o(B.current))&&A(!0)},[C,A]);let[ie]=l({controlled:b,default:S,name:`FieldControl`,state:`value`}),ae=b!==void 0,V=ae?ie:void 0,oe=s(()=>N.inputRef.current?.value);return m(N.inputRef,z,V,oe,!F,y),i(`input`,e,{ref:[t,B],state:L,props:[{id:z,disabled:F,name:I,ref:N.inputRef,"aria-labelledby":R,autoFocus:C,...ae?{value:V}:{defaultValue:S},onChange(e){let t=e.currentTarget.value;x?.(t,g(ee,e.nativeEvent)),O(t!==(k.initialValue??``)),j(t!==``),e.nativeEvent.defaultPrevented||(P(I),N.change(t))},onFocus(){A(!0)},onBlur(e){D(!0),A(!1),M===`onBlur`&&N.commit(e.currentTarget.value)},onKeyDown(e){e.currentTarget.tagName===`INPUT`&&e.key===`Enter`&&(D(!0),N.commit(e.currentTarget.value))}},T,e=>N.getValidationProps(F,e)],stateAttributesMapping:d})}),W=n(),G=v.forwardRef(function(e,t){return(0,W.jsx)(le,{ref:t,...e})});function ue({className:e,type:t,...n}){return(0,W.jsx)(G,{type:t,"data-slot":`input`,className:c(`h-9 w-full min-w-0 rounded-4xl border border-input bg-input/30 px-3 py-1 text-base transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40`,e),...n})}export{U as n,N as r,ue as t}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/jsx-runtime-DT4cu4GC.js b/internal/webassets/build/client/assets/jsx-runtime-D0QO-s14.js similarity index 83% rename from internal/webassets/build/client/assets/jsx-runtime-DT4cu4GC.js rename to internal/webassets/build/client/assets/jsx-runtime-D0QO-s14.js index 4c46e96..582dc2d 100644 --- a/internal/webassets/build/client/assets/jsx-runtime-DT4cu4GC.js +++ b/internal/webassets/build/client/assets/jsx-runtime-D0QO-s14.js @@ -1,26 +1,26 @@ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function k(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var A=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?k(``+e.key):t.toString(36)}function M(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function N(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,N(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(A,`$&/`)+`/`),N(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(A,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=l()})),d=c(u(),1),f=`modulepreload`,p=function(e){return`/`+e},m={},h=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=p(t,n),t in m)return;m[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:f,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},g=e=>{throw TypeError(e)},_=(e,t,n)=>t.has(e)||g(`Cannot `+n),v=(e,t,n)=>(_(e,t,`read from private field`),n?n.call(e):t.get(e)),y=(e,t,n)=>t.has(e)?g(`Cannot add the same private member more than once`):t instanceof WeakSet?t.add(e):t.set(e,n),b=(e,t,n,r)=>(_(e,t,`write to private field`),r?r.call(e,n):t.set(e,n),n),x=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,S=/^[\\/]{2}/;function C(e,t){return t+e.replace(/\\/g,`/`)}var w=`popstate`;function T(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function E(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return j(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:M(t)}return P(t,n,null,e)}function D(e,t){if(e===!1||e==null)throw Error(t)}function O(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function k(){return Math.random().toString(36).substring(2,10)}function A(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function j(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?N(t):t,state:n,key:t&&t.key||r||k(),mask:i}}function M({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function N(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function P(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=T(e)?e:j(h.location,e,t);n&&n(r,e),l=u()+1;let d=A(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=T(e)?e:j(h.location,e,t);n&&n(r,e),l=u();let i=A(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return ee(i,e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(w,d),c=e,()=>{i.removeEventListener(w,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function ee(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),D(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:M(t);return i=i.replace(/ $/,`%20`),!n&&S.test(i)&&(i=r+i),new URL(i,r)}function te(e){return{defaultValue:e}}var F,ne=class{constructor(e){if(y(this,F,new Map),e)for(let[t,n]of e)this.set(t,n)}get(e){if(v(this,F).has(e))return v(this,F).get(e);if(e.defaultValue!==void 0)return e.defaultValue;throw Error(`No value found for context`)}set(e,t){v(this,F).set(e,t)}};F=new WeakMap;var re=new Set([`lazy`,`caseSensitive`,`path`,`id`,`index`,`children`]);function ie(e){return re.has(e)}var I=new Set([`lazy`,`caseSensitive`,`path`,`id`,`index`,`middleware`,`children`]);function ae(e){return I.has(e)}function oe(e){return e.index===!0}function se(e,t,n=[],r={},i=!1){return e.map((e,a)=>{let o=[...n,String(a)],s=typeof e.id==`string`?e.id:o.join(`-`);if(D(e.index!==!0||!e.children,`Cannot specify children on an index route`),D(i||!r[s],`Found a route id collision on id "${s}". Route id's must be globally unique within Data Router usages`),oe(e)){let n={...e,id:s};return r[s]=ce(n,t(n)),n}else{let n={...e,id:s,children:void 0};return r[s]=ce(n,t(n)),e.children&&(n.children=se(e.children,t,o,r,i)),n}})}function ce(e,t){return Object.assign(e,{...t,...typeof t.lazy==`object`&&t.lazy!=null?{lazy:{...e.lazy,...t.lazy}}:{}})}function L(e,t,n=`/`){return R(e,t,n,!1)}function R(e,t,n,r,i){let a=U((typeof t==`string`?N(t):t).pathname||`/`,n);if(a==null)return null;let o=i??ue(e),s=null,c=Ce(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;D(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=G([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(D(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),z(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:_e(l,e.index),routesMeta:u.map((e,t)=>{let[n,r]=Se(e.relativePath,e.caseSensitive,t===u.length-1);return{...e,matcher:n,compiledParams:r}})})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of B(e.path))a(e,t,!0,n)}),t}function B(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=B(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function V(e){e.sort((e,t)=>e.score===t.score?ve(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var de=/^:[\w-]+$/,fe=3,pe=2,me=1,he=10,H=-2,ge=e=>e===`*`;function _e(e,t){let n=e.split(`/`),r=n.length;return n.some(ge)&&(r+=H),t&&(r+=pe),n.filter(e=>!ge(e)).reduce((e,t)=>e+(de.test(t)?fe:t===``?me:he),r)}function ve(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function ye(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function Se(e,t=!1,n=!0){O(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function Ce(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return O(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function U(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}function we({basename:e,pathname:t}){return t===`/`?e:G([e,t])}var Te=e=>x.test(e);function Ee(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?N(e):e,a;return n?(n=W(n),a=n.startsWith(`/`)?De(n.substring(1),`/`):De(n,t)):a=t,{pathname:a,search:Pe(r),hash:Fe(i)}}function De(e,t){let n=Me(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function Oe(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function ke(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function Ae(e){let t=ke(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function je(e,t,n,r=!1){let i;typeof e==`string`?i=N(e):(i={...e},D(!i.pathname||!i.pathname.includes(`?`),Oe(`?`,`pathname`,`search`,i)),D(!i.pathname||!i.pathname.includes(`#`),Oe(`#`,`pathname`,`hash`,i)),D(!i.search||!i.search.includes(`#`),Oe(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=Ee(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var W=e=>e.replace(/[\\/]{2,}/g,`/`),G=e=>W(e.join(`/`)),Me=e=>e.replace(/\/+$/,``),Ne=e=>Me(e).replace(/^\/*/,`/`),Pe=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Fe=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Ie=class{constructor(e,t){this.type=`DataWithResponseInit`,this.data=e,this.init=t||null}};function Le(e,t){return new Ie(e,typeof t==`number`?{status:t}:t)}var Re=(e,t=302)=>{let n=t;typeof n==`number`?n={status:n}:n.status===void 0&&(n.status=302);let r=new Headers(n.headers);return r.set(`Location`,e),new Response(null,{...n,headers:r})},ze=[`EvalError`,`RangeError`,`ReferenceError`,`SyntaxError`,`TypeError`,`URIError`],Be=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Ve(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function He(e){return G(e.map(e=>e.route.path).filter(Boolean))||`/`}var Ue=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function We(e,t){let n=e;if(typeof n!=`string`||!x.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Ue)try{let e=new URL(window.location.href),r=S.test(n)?new URL(C(n,e.protocol)):new URL(n),a=U(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{O(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}var K=Symbol(`Uninstrumented`);function Ge(e,t){let n={lazy:[],"lazy.loader":[],"lazy.action":[],"lazy.middleware":[],middleware:[],loader:[],action:[]};e.forEach(e=>e({id:t.id,index:t.index,path:t.path,instrument(e){let t=Object.keys(n);for(let r of t)e[r]&&n[r].push(e[r])}}));let r={};if(typeof t.lazy==`function`&&n.lazy.length>0){let e=qe(n.lazy,t.lazy,()=>void 0);e&&(r.lazy=e)}if(typeof t.lazy==`object`){let e=t.lazy;[`middleware`,`loader`,`action`].forEach(t=>{let i=e[t],a=n[`lazy.${t}`];if(typeof i==`function`&&a.length>0){let e=qe(a,i,()=>void 0);e&&(r.lazy=Object.assign(r.lazy||{},{[t]:e}))}})}return[`loader`,`action`].forEach(e=>{let i=t[e];if(typeof i==`function`&&n[e].length>0){let t=i[K]??i,a=qe(n[e],t,(...e)=>Ye(e[0]));a&&(e===`loader`&&t.hydrate===!0&&(a.hydrate=!0),a[K]=t,r[e]=a)}}),t.middleware&&t.middleware.length>0&&n.middleware.length>0&&(r.middleware=t.middleware.map(e=>{let t=e[K]??e,r=qe(n.middleware,t,(...e)=>Ye(e[0]));return r?(r[K]=t,r):e})),r}function Ke(e,t){let n={navigate:[],fetch:[]};if(t.forEach(e=>e({instrument(e){let t=Object.keys(e);for(let r of t)e[r]&&n[r].push(e[r])}})),n.navigate.length>0){let t=e.navigate[K]??e.navigate,r=qe(n.navigate,t,(...t)=>{let[n,r]=t;return{to:typeof n==`number`||typeof n==`string`?n:n?M(n):`.`,...Xe(e,r??{})}});r&&(r[K]=t,e.navigate=r)}if(n.fetch.length>0){let t=e.fetch[K]??e.fetch,r=qe(n.fetch,t,(...t)=>{let[n,,r,i]=t;return{href:r??`.`,fetcherKey:n,...Xe(e,i??{})}});r&&(r[K]=t,e.fetch=r)}return e}function qe(e,t,n){return e.length===0?null:async(...r)=>{let i=await Je(e,n(...r),()=>t(...r),e.length-1);if(i.type===`error`)throw i.value;return i.value}}async function Je(e,t,n,r){let i=e[r],a;if(i){let o,s=async()=>(o?console.error(`You cannot call instrumented handlers more than once`):o=Je(e,t,n,r-1),a=await o,D(a,`Expected a result`),a.type===`error`&&a.value instanceof Error?{status:`error`,error:a.value}:{status:`success`,error:void 0});try{await i(s,t)}catch(e){console.error(`An instrumentation function threw an error:`,e)}o||await s(),await o}else try{a={type:`success`,value:await n()}}catch(e){a={type:`error`,value:e}}return a||{type:`error`,value:Error(`No result assigned in instrumentation chain.`)}}function Ye(e){let{request:t,context:n,params:r,pattern:i}=e;return{request:Ze(t),params:{...r},pattern:i,context:Qe(n)}}function Xe(e,t){return{currentUrl:M(e.state.location),...`formMethod`in t?{formMethod:t.formMethod}:{},...`formEncType`in t?{formEncType:t.formEncType}:{},...`formData`in t?{formData:t.formData}:{},...`body`in t?{body:t.body}:{}}}function Ze(e){return{method:e.method,url:e.url,headers:{get:(...t)=>e.headers.get(...t)}}}function Qe(e){if(et(e)){let t={...e};return Object.freeze(t),t}else return{get:t=>e.get(t)}}var $e=Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);function et(e){if(typeof e!=`object`||!e)return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null||Object.getOwnPropertyNames(t).sort().join(`\0`)===$e}var tt=[`POST`,`PUT`,`PATCH`,`DELETE`],nt=new Set(tt),rt=[`GET`,...tt],it=new Set(rt),at=new Set([301,302,303,307,308]),ot=new Set([307,308]),st={state:`idle`,location:void 0,matches:void 0,historyAction:void 0,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0},ct={state:`idle`,data:void 0,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0},lt={state:`unblocked`,proceed:void 0,reset:void 0,location:void 0},ut=e=>({hasErrorBoundary:!!e.hasErrorBoundary}),dt=`remix-router-transitions`,ft=Symbol(`ResetLoaderData`),pt,mt,ht,gt,_t=class{constructor(e){y(this,pt),y(this,mt),y(this,ht),y(this,gt),b(this,pt,e),b(this,mt,ue(e))}get stableRoutes(){return v(this,pt)}get activeRoutes(){return v(this,ht)??v(this,pt)}get branches(){return v(this,gt)??v(this,mt)}get hasHMRRoutes(){return v(this,ht)!=null}setRoutes(e){b(this,pt,e),b(this,mt,ue(e))}setHmrRoutes(e){b(this,ht,e),b(this,gt,ue(e))}commitHmrRoutes(){v(this,ht)&&(b(this,pt,v(this,ht)),b(this,mt,v(this,gt)),b(this,ht,void 0),b(this,gt,void 0))}};pt=new WeakMap,mt=new WeakMap,ht=new WeakMap,gt=new WeakMap;function vt(e){let t=e.window?e.window:typeof window<`u`?window:void 0,n=t!==void 0&&t.document!==void 0&&t.document.createElement!==void 0;D(e.routes.length>0,`You must provide a non-empty routes array to createRouter`);let r=e.hydrationRouteProperties||[],i=e.mapRouteProperties||ut,a=i;if(e.instrumentations){let t=e.instrumentations;a=e=>({...i(e),...Ge(t.map(e=>e.route).filter(Boolean),e)})}let o={},s=new _t(se(e.routes,a,void 0,o)),c=e.basename||`/`;c.startsWith(`/`)||(c=`/${c}`);let l=e.dataStrategy||Ft,u={...e.future},d=null,f=new Set,p=null,m=null,h=null,g=null,_=e.hydrationData!=null,v=R(s.activeRoutes,e.history.location,c,!1,s.branches),y=!1,b=null,x,S;if(v==null&&!e.patchRoutesOnNavigation){let t=q(404,{pathname:e.history.location.pathname}),{matches:n,route:r}=on(s.activeRoutes);x=!0,S=!x,v=n,b={[r.id]:t}}else if(v&&!e.hydrationData&&Ze(v,s.activeRoutes,e.history.location.pathname).active&&(v=null),!v){x=!1,S=!x,v=[];let t=Ze(null,s.activeRoutes,e.history.location.pathname);t.active&&t.matches&&(y=!0,v=t.matches)}else if(v.some(e=>e.route.lazy))x=!1,S=!x;else if(!v.some(e=>Ct(e.route)))x=!0,S=!x;else{let t=e.hydrationData?e.hydrationData.loaderData:null,n=e.hydrationData?e.hydrationData.errors:null,r=v;if(n){let e=v.findIndex(e=>n[e.route.id]!==void 0);r=r.slice(0,e+1)}S=!1,x=!0,r.forEach(e=>{let r=wt(e.route,t,n);S||=r.renderFallback,x&&=!r.shouldLoad})}let C,w={historyAction:e.history.action,location:e.history.location,matches:v,initialized:x,renderFallback:S,navigation:st,restoreScrollPosition:e.hydrationData==null?null:!1,preventScrollReset:!1,revalidation:`idle`,loaderData:e.hydrationData&&e.hydrationData.loaderData||{},actionData:e.hydrationData&&e.hydrationData.actionData||null,errors:e.hydrationData&&e.hydrationData.errors||b,fetchers:new Map,blockers:new Map},T=`POP`,E=null,k=!1,A,M=!1,P=new Map,te=null,F=!1,re=!1,ie=new Set,I=new Map,ae=0,oe=-1,ce=new Map,L=new Set,ue=new Map,z=new Map,B=new Set,V=new Map,de,fe=null;function pe(){if(d=e.history.listen(({action:t,location:n,delta:r})=>{if(de){de(),de=void 0;return}O(V.size===0||r!=null,"You are trying to use a blocker on a POP navigation to a location that was not created by @remix-run/router. This will fail silently in production. This can happen if you are navigating outside the router via `window.history.pushState`/`window.location.hash` instead of using router navigation APIs. This can also happen if you are using createHashRouter and the user manually changes the URL.");let i=We({currentLocation:w.location,nextLocation:n,historyAction:t});if(i&&r!=null){let t=new Promise(e=>{de=e});e.history.go(r*-1),Ue(i,{state:`blocked`,location:n,proceed(){Ue(i,{state:`proceeding`,proceed:void 0,reset:void 0,location:n}),t.then(()=>e.history.go(r))},reset(){let e=new Map(w.blockers);e.set(i,lt),H({blockers:e})}}),E?.resolve(),E=null;return}return ye(t,n)}),n){On(t,P);let e=()=>kn(t,P);t.addEventListener(`pagehide`,e),te=()=>t.removeEventListener(`pagehide`,e)}return w.initialized||ye(`POP`,w.location,{initialHydration:!0}),C}function me(){d&&d(),te&&te(),f.clear(),A&&A.abort(),w.fetchers.forEach((e,t)=>Pe(w.fetchers,t)),w.blockers.forEach((e,t)=>He(t))}function he(e){if(f.add(e),p){let{newErrors:t}=p;p=null,e(w,{deletedFetchers:[],newErrors:t,viewTransitionOpts:void 0,flushSync:!1})}return()=>f.delete(e)}function H(e,t={}){e.matches&&=e.matches.map(e=>{let t=o[e.route.id],n=e.route;return n.element!==t.element||n.errorElement!==t.errorElement||n.hydrateFallbackElement!==t.hydrateFallbackElement?{...e,route:t}:e}),w={...w,...e};let n=[],r=[];w.fetchers.forEach((e,t)=>{e.state===`idle`&&(B.has(t)?n.push(t):r.push(t))}),B.forEach(e=>{!w.fetchers.has(e)&&!I.has(e)&&n.push(e)}),f.size===0&&(p={newErrors:e.errors??null}),[...f].forEach(r=>r(w,{deletedFetchers:n,newErrors:e.errors??null,viewTransitionOpts:t.viewTransitionOpts,flushSync:t.flushSync===!0})),n.forEach(e=>Pe(w.fetchers,e)),r.forEach(e=>w.fetchers.delete(e))}function ge(t,n,{flushSync:r}={}){let i=w.actionData!=null&&w.navigation.formMethod!=null&&Y(w.navigation.formMethod)&&w.navigation.state===`loading`&&t.state?._isRedirect!==!0,a;a=n.actionData?Object.keys(n.actionData).length>0?n.actionData:null:i?w.actionData:null;let o=n.loaderData?nn(w.loaderData,n.loaderData,n.matches||[],n.errors):w.loaderData,c=w.blockers;c.size>0&&(c=new Map(c),c.forEach((e,t)=>c.set(t,lt)));let l=F?!1:Xe(t,n.matches||w.matches),u=k===!0||w.navigation.formMethod!=null&&Y(w.navigation.formMethod)&&t.state?._isRedirect!==!0;s.commitHmrRoutes(),F||T===`POP`||(T===`PUSH`?e.history.push(t,t.state):T===`REPLACE`&&e.history.replace(t,t.state));let d;if(T===`POP`){let e=P.get(w.location.pathname);e&&e.has(t.pathname)?d={currentLocation:w.location,nextLocation:t}:P.has(t.pathname)&&(d={currentLocation:t,nextLocation:w.location})}else if(M){let e=P.get(w.location.pathname);e?e.add(t.pathname):(e=new Set([t.pathname]),P.set(w.location.pathname,e)),d={currentLocation:w.location,nextLocation:t}}H({...n,actionData:a,loaderData:o,historyAction:T,location:t,initialized:!0,renderFallback:!1,navigation:st,revalidation:`idle`,restoreScrollPosition:l,preventScrollReset:u,blockers:c},{viewTransitionOpts:d,flushSync:r===!0}),T=`POP`,k=!1,M=!1,F=!1,re=!1,E?.resolve(),E=null,fe?.resolve(),fe=null}async function _e(t,n){if(E?.resolve(),E=null,typeof t==`number`){E||=An();let n=E.promise;return e.history.go(t),n}let{path:r,submission:i,error:a}=xt(!1,bt(w.location,w.matches,c,t,n?.fromRouteId,n?.relative),n),o;n?.mask&&(o={pathname:``,search:``,hash:``,...typeof n.mask==`string`?N(n.mask):{...w.location.mask,...n.mask}});let s=w.location,l=j(s,r,n&&n.state,void 0,o);l={...l,...e.history.encodeLocation(l)};let u=n&&n.replace!=null?n.replace:void 0,d=`PUSH`;u===!0?d=`REPLACE`:u===!1||i!=null&&Y(i.formMethod)&&i.formAction===w.location.pathname+w.location.search&&(d=`REPLACE`);let f=n&&`preventScrollReset`in n?n.preventScrollReset===!0:void 0,p=(n&&n.flushSync)===!0,m=We({currentLocation:s,nextLocation:l,historyAction:d});if(m){Ue(m,{state:`blocked`,location:l,proceed(){Ue(m,{state:`proceeding`,proceed:void 0,reset:void 0,location:l}),_e(t,n)},reset(){let e=new Map(w.blockers);e.set(m,lt),H({blockers:e})}});return}await ye(d,l,{submission:i,pendingError:a,preventScrollReset:f,replace:n&&n.replace,enableViewTransition:n&&n.viewTransition,flushSync:p,callSiteDefaultShouldRevalidate:n&&n.defaultShouldRevalidate})}function ve(){fe||=An(),je(),H({revalidation:`loading`});let e=fe.promise;return w.navigation.state===`submitting`?e:w.navigation.state===`idle`?(ye(w.historyAction,w.location,{startUninterruptedRevalidation:!0}),e):(ye(T||w.historyAction,w.navigation.location,{overrideNavigation:w.navigation,enableViewTransition:M===!0}),e)}async function ye(t,n,r){A&&A.abort(),A=null,T=t,F=(r&&r.startUninterruptedRevalidation)===!0,Ye(w.location,w.matches),k=(r&&r.preventScrollReset)===!0,M=(r&&r.enableViewTransition)===!0;let i=s.activeRoutes,a=r?.initialHydration&&w.matches&&w.matches.length>0&&!y?w.matches:R(i,n,c,!1,s.branches),o=(r&&r.flushSync)===!0;if(a&&w.initialized&&!re&&ln(w.location,n)&&!(r&&r.submission&&Y(r.submission.formMethod))){ge(n,{matches:a},{flushSync:o});return}let l=Ze(a,i,n.pathname);if(l.active&&l.matches&&(a=l.matches),!a){let{error:e,notFoundMatches:t,route:r}=K(n.pathname);ge(n,{matches:t,loaderData:{},errors:{[r.id]:e}},{flushSync:o});return}let u=r&&r.overrideNavigation?{...r.overrideNavigation,matches:a,historyAction:t}:void 0;A=new AbortController;let d=Xt(e.history,n,A.signal,r&&r.submission),f=e.getContext?await e.getContext():new ne,p;if(r&&r.pendingError)p=[an(a).route.id,{type:`error`,error:r.pendingError}];else if(r&&r.submission&&Y(r.submission.formMethod)){let i=await be(d,n,r.submission,a,t,f,l.active,r&&r.initialHydration===!0,{replace:r.replace,flushSync:o});if(i.shortCircuited)return;if(i.pendingActionResult){let[e,t]=i.pendingActionResult;if(J(t)&&Ve(t.error)&&t.error.status===404){A=null,ge(n,{matches:i.matches,loaderData:{},errors:{[e]:t.error}});return}}a=i.matches||a,p=i.pendingActionResult,u=Cn(n,a,t,r.submission),o=!1,l.active=!1,d=Xt(e.history,d.url,d.signal)}let{shortCircuited:m,matches:h,loaderData:g,errors:_,workingFetchers:v}=await xe(d,n,a,t,f,l.active,u,r&&r.submission,r&&r.fetcherSubmission,r&&r.replace,r&&r.initialHydration===!0,o,p,r&&r.callSiteDefaultShouldRevalidate);m||(A=null,ge(n,{matches:h||a,...rn(p),loaderData:g,errors:_,...v?{fetchers:v}:{}}))}async function be(t,n,i,l,u,d,f,p,m={}){if(je(),H({navigation:wn(n,l,u,i)},{flushSync:m.flushSync===!0}),f){let e=await Qe(l,n.pathname,t.signal);if(e.type===`aborted`)return{shortCircuited:!0};if(e.type===`error`){if(e.partialMatches.length===0){let{matches:t,route:n}=on(s.activeRoutes);return{matches:t,pendingActionResult:[n.id,{type:`error`,error:e.error}]}}let t=an(e.partialMatches).route.id;return{matches:e.partialMatches,pendingActionResult:[t,{type:`error`,error:e.error}]}}else if(e.matches)l=e.matches;else{let{notFoundMatches:e,error:t,route:r}=K(n.pathname);return{matches:e,pendingActionResult:[r.id,{type:`error`,error:t}]}}}let h,g=xn(l,n);if(!g.route.action&&!g.route.lazy)h={type:`error`,error:q(405,{method:t.method,pathname:n.pathname,routeId:g.route.id})};else{let e=await ke(t,n,Vt(a,o,t,n,l,g,p?[]:r,d),d,null);if(h=e[g.route.id],!h){for(let t of l)if(e[t.route.id]){h=e[t.route.id];break}}if(t.signal.aborted)return{shortCircuited:!0}}if(mn(h)){let n;return n=m&&m.replace!=null?m.replace:Yt(h.response.headers.get(`Location`),new URL(t.url),c,e.history)===w.location.pathname+w.location.search,await Oe(t,h,!0,{submission:i,replace:n}),{shortCircuited:!0}}if(J(h)){let e=an(l,g.route.id);return(m&&m.replace)!==!0&&(T=`PUSH`),{matches:l,pendingActionResult:[e.route.id,h,g.route.id]}}return{matches:l,pendingActionResult:[g.route.id,h]}}async function xe(t,n,i,l,u,d,f,p,m,h,g,_,v,y){let b=f||Cn(n,i,l,p),x=p||m||Sn(b),S=!F&&!g;if(d){if(S){let e=Se(v);H({navigation:b,...e===void 0?{}:{actionData:e}},{flushSync:_})}let e=await Qe(i,n.pathname,t.signal);if(e.type===`aborted`)return{shortCircuited:!0};if(e.type===`error`){if(e.partialMatches.length===0){let{matches:t,route:n}=on(s.activeRoutes);return{matches:t,loaderData:{},errors:{[n.id]:e.error}}}let t=an(e.partialMatches).route.id;return{matches:e.partialMatches,loaderData:{},errors:{[t]:e.error}}}else if(e.matches)i=e.matches;else{let{error:e,notFoundMatches:t,route:r}=K(n.pathname);return{matches:t,loaderData:{},errors:{[r.id]:e}}}}let C=s.activeRoutes,{dsMatches:T,revalidatingFetchers:E}=St(t,u,a,o,e.history,w,i,x,n,g?[]:r,g===!0,re,ie,B,ue,L,C,c,e.patchRoutesOnNavigation!=null,s.branches,v,y);if(oe=++ae,!e.dataStrategy&&!T.some(e=>e.shouldLoad)&&!T.some(e=>e.route.middleware&&e.route.middleware.length>0)&&E.length===0){let e=new Map(w.fetchers),t=Re(e);return ge(n,{matches:i,loaderData:{},errors:v&&J(v[1])?{[v[0]]:v[1].error}:null,...rn(v),...t?{fetchers:e}:{}},{flushSync:_}),{shortCircuited:!0}}if(S){let e={};if(!d){e.navigation=b;let t=Se(v);t!==void 0&&(e.actionData=t)}E.length>0&&(e.fetchers=Ce(E)),H(e,{flushSync:_})}E.forEach(e=>{Ie(e.key),e.controller&&I.set(e.key,e.controller)});let D=()=>E.forEach(e=>Ie(e.key));A&&A.signal.addEventListener(`abort`,D);let{loaderResults:O,fetcherResults:k}=await Ae(T,E,t,n,u);if(t.signal.aborted)return{shortCircuited:!0};A&&A.signal.removeEventListener(`abort`,D),E.forEach(e=>I.delete(e.key));let j=sn(O);if(j)return await Oe(t,j.result,!0,{replace:h}),{shortCircuited:!0};if(j=sn(k),j)return L.add(j.key),await Oe(t,j.result,!0,{replace:h}),{shortCircuited:!0};let M=new Map(w.fetchers),{loaderData:N,errors:P}=tn(w,i,O,v,E,k,M);g&&w.errors&&(P={...w.errors,...P});let ee=Re(M),te=ze(oe,M),ne=ee||te||E.length>0;return{matches:i,loaderData:N,errors:P,...ne?{workingFetchers:M}:{}}}function Se(e){if(e&&!J(e[1]))return{[e[0]]:e[1].data};if(w.actionData)return Object.keys(w.actionData).length===0?null:w.actionData}function Ce(e){let t=new Map(w.fetchers);return e.forEach(e=>{let n=t.get(e.key),r=Tn(void 0,n?n.data:void 0);t.set(e.key,r)}),t}async function we(t,n,r,i){Ie(t);let a=(i&&i.flushSync)===!0,o=s.activeRoutes,l=bt(w.location,w.matches,c,r,n,i?.relative),u=R(o,l,c,!1,s.branches),d=Ze(u,o,l);if(d.active&&d.matches&&(u=d.matches),!u){G(t,n,q(404,{pathname:l}),{flushSync:a});return}let{path:f,submission:p,error:m}=xt(!0,l,i);if(m){G(t,n,m,{flushSync:a});return}let h=e.getContext?await e.getContext():new ne,g=(i&&i.preventScrollReset)===!0;if(p&&Y(p.formMethod)){await Ee(t,n,f,u,h,d.active,a,g,p,i&&i.defaultShouldRevalidate);return}ue.set(t,{routeId:n,path:f}),await De(t,n,f,u,h,d.active,a,g,p)}async function Ee(t,n,i,l,u,d,f,p,m,h){je(),ue.delete(t),W(t,En(m,w.fetchers.get(t)),{flushSync:f});let g=new AbortController,_=Xt(e.history,i,g.signal,m);if(d){let e=await Qe(l,new URL(_.url).pathname,_.signal,t);if(e.type===`aborted`)return;if(e.type===`error`){G(t,n,e.error,{flushSync:f});return}else if(e.matches)l=e.matches;else{G(t,n,q(404,{pathname:i}),{flushSync:f});return}}let v=xn(l,i);if(!v.route.action&&!v.route.lazy){G(t,n,q(405,{method:m.formMethod,pathname:i,routeId:n}),{flushSync:f});return}I.set(t,g);let y=ae,b=Vt(a,o,_,i,l,v,r,u),x=await ke(_,i,b,u,t),S=x[v.route.id];if(!S){for(let e of b)if(x[e.route.id]){S=x[e.route.id];break}}if(_.signal.aborted){I.get(t)===g&&I.delete(t);return}if(B.has(t)){if(mn(S)||J(S)){W(t,Dn(void 0));return}}else{if(mn(S))if(I.delete(t),oe>y){W(t,Dn(void 0));return}else return L.add(t),W(t,Tn(m)),Oe(_,S,!1,{fetcherSubmission:m,preventScrollReset:p});if(J(S)){G(t,n,S.error);return}}let C=w.navigation.location||w.location,E=Xt(e.history,C,g.signal),O=s.activeRoutes,k=w.navigation.state===`idle`?w.matches:R(O,w.navigation.location,c,!1,s.branches);D(k,`Didn't find any matches after fetcher action`);let j=++ae;ce.set(t,j);let{dsMatches:M,revalidatingFetchers:N}=St(E,u,a,o,e.history,w,k,m,C,r,!1,re,ie,B,ue,L,O,c,e.patchRoutesOnNavigation!=null,s.branches,[v.route.id,S],h),P=Tn(m,S.data),ee=new Map(w.fetchers);ee.set(t,P),N.filter(e=>e.key!==t).forEach(e=>{let t=e.key,n=ee.get(t),r=Tn(void 0,n?n.data:void 0);ee.set(t,r),Ie(t),e.controller&&I.set(t,e.controller)}),H({fetchers:ee});let te=()=>N.forEach(e=>Ie(e.key));g.signal.addEventListener(`abort`,te);let{loaderResults:F,fetcherResults:ne}=await Ae(M,N,E,C,u);if(g.signal.aborted)return;g.signal.removeEventListener(`abort`,te),ce.delete(t),I.delete(t),N.forEach(e=>I.delete(e.key));let se=w.fetchers.has(t),le=e=>{if(!se)return e;let n=new Map(e.fetchers);return n.set(t,Dn(S.data)),{...e,fetchers:n}},z=sn(F);if(z)return w=le(w),Oe(E,z.result,!1,{preventScrollReset:p});if(z=sn(ne),z)return L.add(z.key),w=le(w),Oe(E,z.result,!1,{preventScrollReset:p});let V=new Map(w.fetchers);se&&V.set(t,Dn(S.data));let{loaderData:de,errors:fe}=tn(w,k,F,void 0,N,ne,V);ze(j,V),w.navigation.state===`loading`&&j>oe?(D(T,`Expected pending action`),A&&A.abort(),ge(w.navigation.location,{matches:k,loaderData:de,errors:fe,fetchers:V})):(H({errors:fe,loaderData:nn(w.loaderData,de,k,fe),fetchers:V}),re=!1)}async function De(t,n,i,s,c,l,u,d,f){let p=w.fetchers.get(t);W(t,Tn(f,p?p.data:void 0),{flushSync:u});let m=new AbortController,h=Xt(e.history,i,m.signal);if(l){let e=await Qe(s,new URL(h.url).pathname,h.signal,t);if(e.type===`aborted`)return;if(e.type===`error`){G(t,n,e.error,{flushSync:u});return}else if(e.matches)s=e.matches;else{G(t,n,q(404,{pathname:i}),{flushSync:u});return}}let g=xn(s,i);I.set(t,m);let _=ae,v=await ke(h,i,Vt(a,o,h,i,s,g,r,c),c,t),y=v[g.route.id];if(!y){for(let e of s)if(v[e.route.id]){y=v[e.route.id];break}}if(I.get(t)===m&&I.delete(t),!h.signal.aborted){if(B.has(t)){W(t,Dn(void 0));return}if(mn(y))if(oe>_){W(t,Dn(void 0));return}else{L.add(t),await Oe(h,y,!1,{preventScrollReset:d});return}if(J(y)){G(t,n,y.error);return}W(t,Dn(y.data))}}async function Oe(r,i,a,{submission:o,fetcherSubmission:s,preventScrollReset:l,replace:u}={}){a||(E?.resolve(),E=null),i.response.headers.has(`X-Remix-Revalidate`)&&(re=!0);let d=i.response.headers.get(`Location`);D(d,`Expected a Location header on the redirect Response`),d=Yt(d,new URL(r.url),c,e.history);let f=j(w.location,d,{_isRedirect:!0});if(n){let e=!1;if(i.response.headers.has(`X-Remix-Reload-Document`))e=!0;else if(Te(d)){let n=ee(t,d,!0);e=n.origin!==t.location.origin||U(n.pathname,c)==null}if(e){u?t.location.replace(d):t.location.assign(d);return}}A=null;let p=u===!0||i.response.headers.has(`X-Remix-Replace`)?`REPLACE`:`PUSH`,{formMethod:m,formAction:h,formEncType:g}=w.navigation;!o&&!s&&m&&h&&g&&(o=Sn(w.navigation));let _=o||s;ot.has(i.response.status)&&_&&Y(_.formMethod)?await ye(p,f,{submission:{..._,formAction:d},preventScrollReset:l||k,enableViewTransition:a?M:void 0}):await ye(p,f,{overrideNavigation:Cn(f,[],p,o),fetcherSubmission:s,preventScrollReset:l||k,enableViewTransition:a?M:void 0})}async function ke(e,t,n,r,i){let a,o={};try{a=await Ht(l,e,t,n,i,r,!1)}catch(e){return n.filter(e=>e.shouldLoad).forEach(t=>{o[t.route.id]={type:`error`,error:e}}),o}if(e.signal.aborted)return o;if(!Y(e.method))for(let e of n){if(a[e.route.id]?.type===`error`)break;!a.hasOwnProperty(e.route.id)&&!w.loaderData.hasOwnProperty(e.route.id)&&(!w.errors||!w.errors.hasOwnProperty(e.route.id))&&e.shouldCallHandler()&&(a[e.route.id]={type:`error`,result:Error(`No result returned from dataStrategy for route ${e.route.id}`)})}for(let[t,r]of Object.entries(a))if(pn(r)){let i=r.result;o[t]={type:`redirect`,response:Kt(i,e,t,n,c)}}else o[t]=await Gt(r);return o}async function Ae(e,t,n,r,i){let a=ke(n,r,e,i,null),o=Promise.all(t.map(async e=>{if(e.matches&&e.match&&e.request&&e.controller){let t=(await ke(e.request,e.path,e.matches,i,e.key))[e.match.route.id];return{[e.key]:t}}else return Promise.resolve({[e.key]:{type:`error`,error:q(404,{pathname:e.path})}})}));return{loaderResults:await a,fetcherResults:(await o).reduce((e,t)=>Object.assign(e,t),{})}}function je(){re=!0,ue.forEach((e,t)=>{I.has(t)&&ie.add(t),Ie(t)})}function W(e,t,n={}){let r=new Map(w.fetchers);r.set(e,t),H({fetchers:r},{flushSync:(n&&n.flushSync)===!0})}function G(e,t,n,r={}){let i=an(w.matches,t),a=new Map(w.fetchers);Pe(a,e),H({errors:{[i.route.id]:n},fetchers:a},{flushSync:(r&&r.flushSync)===!0})}function Me(e){return z.set(e,(z.get(e)||0)+1),B.has(e)&&B.delete(e),w.fetchers.get(e)||ct}function Ne(e,t){Ie(e,t?.reason),W(e,Dn(null))}function Pe(e,t){let n=w.fetchers.get(t);I.has(t)&&!(n&&n.state===`loading`&&ce.has(t))&&Ie(t),ue.delete(t),ce.delete(t),L.delete(t),B.delete(t),ie.delete(t),e.delete(t)}function Fe(e){let t=(z.get(e)||0)-1;t<=0?(z.delete(e),B.add(e)):z.set(e,t),H({fetchers:new Map(w.fetchers)})}function Ie(e,t){let n=I.get(e);n&&(n.abort(t),I.delete(e))}function Le(e,t){for(let n of e){let e=t.get(n);D(e,`Expected fetcher: ${n}`);let r=Dn(e.data);t.set(n,r)}}function Re(e){let t=[],n=!1;for(let r of L){let i=e.get(r);D(i,`Expected fetcher: ${r}`),i.state===`loading`&&(L.delete(r),t.push(r),n=!0)}return Le(t,e),n}function ze(e,t){let n=[];for(let[r,i]of ce)if(i0}function Be(e,t){let n=w.blockers.get(e)||lt;return V.get(e)!==t&&V.set(e,t),n}function He(e){w.blockers.delete(e),V.delete(e)}function Ue(e,t){let n=w.blockers.get(e)||lt;D(n.state===`unblocked`&&t.state===`blocked`||n.state===`blocked`&&t.state===`blocked`||n.state===`blocked`&&t.state===`proceeding`||n.state===`blocked`&&t.state===`unblocked`||n.state===`proceeding`&&t.state===`unblocked`,`Invalid blocker state transition: ${n.state} -> ${t.state}`);let r=new Map(w.blockers);r.set(e,t),H({blockers:r})}function We({currentLocation:e,nextLocation:t,historyAction:n}){if(V.size===0)return;V.size>1&&O(!1,`A router only supports one blocker at a time`);let r=Array.from(V.entries()),[i,a]=r[r.length-1],o=w.blockers.get(i);if(!(o&&o.state===`proceeding`)&&a({currentLocation:e,nextLocation:t,historyAction:n}))return i}function K(e){let t=q(404,{pathname:e}),n=s.activeRoutes,{matches:r,route:i}=on(n);return{notFoundMatches:r,route:i,error:t}}function qe(e,t,n){if(m=e,g=t,h=n||null,!_&&w.navigation===st){_=!0;let e=Xe(w.location,w.matches);e!=null&&H({restoreScrollPosition:e})}return()=>{m=null,g=null,h=null}}function Je(e,t){return h&&h(e,t.map(e=>le(e,w.loaderData)))||e.key}function Ye(e,t){if(m&&g){let n=Je(e,t);m[n]=g()}}function Xe(e,t){if(m){let n=Je(e,t),r=m[n];if(typeof r==`number`)return r}return null}function Ze(t,n,r){if(e.patchRoutesOnNavigation){let e=s.branches;if(!t)return{active:!0,matches:R(n,r,c,!0,e)||[]};if(Object.keys(t[0].params).length>0)return{active:!0,matches:R(n,r,c,!0,e)}}return{active:!1,matches:null}}async function Qe(t,n,r,i){if(!e.patchRoutesOnNavigation)return{type:`success`,matches:t};let l=t;for(;;){let t=o;try{await e.patchRoutesOnNavigation({signal:r,path:n,matches:l,fetcherKey:i,patch:(e,n)=>{r.aborted||Ot(e,n,s,t,a,!1)}})}catch(e){return{type:`error`,error:e,partialMatches:l}}if(r.aborted)return{type:`aborted`};let u=s.branches,d=R(s.activeRoutes,n,c,!1,u),f=null;if(d&&(Object.keys(d[0].params).length===0||(f=R(s.activeRoutes,n,c,!0,u),!(f&&l.lengthe.route.id===t[n].route.id)}function et(e){o={},s.setHmrRoutes(se(e,a,void 0,o))}function tt(e,t,n=!1){Ot(e,t,s,o,a,n),s.hasHMRRoutes||H({})}return C={get basename(){return c},get future(){return u},get state(){return w},get routes(){return s.stableRoutes},get branches(){return s.branches},get manifest(){return o},get window(){return t},initialize:pe,subscribe:he,enableScrollRestoration:qe,navigate:_e,fetch:we,revalidate:ve,createHref:t=>e.history.createHref(t),encodeLocation:t=>e.history.encodeLocation(t),getFetcher:Me,resetFetcher:Ne,deleteFetcher:Fe,dispose:me,getBlocker:Be,deleteBlocker:He,patchRoutes:tt,_internalFetchControllers:I,_internalSetRoutes:et,_internalSetStateDoNotUseOrYouWillBreakYourApp(e){H(e)}},e.instrumentations&&(C=Ke(C,e.instrumentations.map(e=>e.router).filter(Boolean))),C}function yt(e){return e!=null&&(`formData`in e&&e.formData!=null||`body`in e&&e.body!==void 0)}function bt(e,t,n,r,i,a){let o,s;if(i){o=[];for(let e of t)if(o.push(e),e.route.id===i){s=e;break}}else o=t,s=t[t.length-1];let c=je(r||`.`,Ae(o),U(e.pathname,n)||e.pathname,a===`path`);if(r??(c.search=e.search,c.hash=e.hash),(r==null||r===``||r===`.`)&&s){let e=bn(c.search);if(s.route.index&&!e)c.search=c.search?c.search.replace(/^\?/,`?index&`):`?index`;else if(!s.route.index&&e){let e=new URLSearchParams(c.search),t=e.getAll(`index`);e.delete(`index`),t.filter(e=>e).forEach(t=>e.append(`index`,t));let n=e.toString();c.search=n?`?${n}`:``}}return n!==`/`&&(c.pathname=we({basename:n,pathname:c.pathname})),M(c)}function xt(e,t,n){if(!n||!yt(n))return{path:t};if(n.formMethod&&!yn(n.formMethod))return{path:t,error:q(405,{method:n.formMethod})};let r=()=>({path:t,error:q(400,{type:`invalid-body`})}),i=(n.formMethod||`get`).toUpperCase(),a=cn(t);if(n.body!==void 0){if(n.formEncType===`text/plain`){if(!Y(i))return r();let e=typeof n.body==`string`?n.body:n.body instanceof FormData||n.body instanceof URLSearchParams?Array.from(n.body.entries()).reduce((e,[t,n])=>`${e}${t}=${n} `,``):String(n.body);return{path:t,submission:{formMethod:i,formAction:a,formEncType:n.formEncType,formData:void 0,json:void 0,text:e}}}else if(n.formEncType===`application/json`){if(!Y(i))return r();try{let e=typeof n.body==`string`?JSON.parse(n.body):n.body;return{path:t,submission:{formMethod:i,formAction:a,formEncType:n.formEncType,formData:void 0,json:e,text:void 0}}}catch{return r()}}}D(typeof FormData==`function`,`FormData is not available in this environment`);let o,s;if(n.formData)o=Qt(n.formData),s=n.formData;else if(n.body instanceof FormData)o=Qt(n.body),s=n.body;else if(n.body instanceof URLSearchParams)o=n.body,s=$t(o);else if(n.body==null)o=new URLSearchParams,s=new FormData;else try{o=new URLSearchParams(n.body),s=$t(o)}catch{return r()}let c={formMethod:i,formAction:a,formEncType:n&&n.formEncType||`application/x-www-form-urlencoded`,formData:s,json:void 0,text:void 0};if(Y(c.formMethod))return{path:t,submission:c};let l=N(t);return e&&l.search&&bn(l.search)&&o.append(`index`,``),l.search=`?${o}`,{path:M(l),submission:c}}function St(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m,h,g,_,v,y,b,x){let S=b?J(b[1])?b[1].error:b[1].data:void 0,C=i.createURL(a.location),w=i.createURL(c),T;if(u&&a.errors){let e=Object.keys(a.errors)[0];T=o.findIndex(t=>t.route.id===e)}else if(b&&J(b[1])){let e=b[0];T=o.findIndex(t=>t.route.id===e)-1}let E=b?b[1].statusCode:void 0,D=E&&E>=400,O={currentUrl:C,currentParams:a.matches[0]?.params||{},nextUrl:w,nextParams:o[0].params,...s,actionResult:S,actionStatus:E},k=He(o),A=o.map((i,o)=>{let{route:s}=i,f=null;if(T!=null&&o>T)f=!1;else if(s.lazy)f=!0;else if(!Ct(s))f=!1;else if(u){let{shouldLoad:e}=wt(s,a.loaderData,a.errors);f=e}else Tt(a.loaderData,a.matches[o],i)&&(f=!0);if(f!==null)return Bt(n,r,e,c,k,i,l,t,f);let p=!1;typeof x==`boolean`?p=x:D?p=!1:d||C.pathname+C.search===w.pathname+w.search?p=!0:C.search===w.search?Et(a.matches[o],i)&&(p=!0):p=!0;let m={...O,defaultShouldRevalidate:p};return Bt(n,r,e,c,k,i,l,t,Dt(i,m),m,x)}),j=[];return m.forEach((e,s)=>{if(u||!o.some(t=>t.route.id===e.routeId)||p.has(s))return;let c=a.fetchers.get(s),m=c&&c.state!==`idle`&&c.data===void 0,b=R(g,e.path,_??`/`,!1,y);if(!b){if(v&&m)return;j.push({key:s,routeId:e.routeId,path:e.path,matches:null,match:null,request:null,controller:null});return}if(h.has(s))return;let S=xn(b,e.path),C=new AbortController,w=Xt(i,e.path,C.signal),T=null;if(f.has(s))f.delete(s),T=Vt(n,r,w,e.path,b,S,l,t);else if(m)d&&(T=Vt(n,r,w,e.path,b,S,l,t));else{let i;i=typeof x==`boolean`?x:D?!1:d;let a={...O,defaultShouldRevalidate:i};Dt(S,a)&&(T=Vt(n,r,w,e.path,b,S,l,t,a))}T&&j.push({key:s,routeId:e.routeId,path:e.path,matches:T,match:S,request:w,controller:C})}),{dsMatches:A,revalidatingFetchers:j}}function Ct(e){return e.loader!=null||e.middleware!=null&&e.middleware.length>0}function wt(e,t,n){if(e.lazy)return{shouldLoad:!0,renderFallback:!0};if(!Ct(e))return{shouldLoad:!1,renderFallback:!1};let r=t!=null&&e.id in t,i=n!=null&&n[e.id]!==void 0;if(!r&&i)return{shouldLoad:!1,renderFallback:!1};if(typeof e.loader==`function`&&e.loader.hydrate===!0)return{shouldLoad:!0,renderFallback:!r};let a=!r&&!i;return{shouldLoad:a,renderFallback:a}}function Tt(e,t,n){let r=!t||n.route.id!==t.route.id,i=!e.hasOwnProperty(n.route.id);return r||i}function Et(e,t){let n=e.route.path;return e.pathname!==t.pathname||n!=null&&n.endsWith(`*`)&&e.params[`*`]!==t.params[`*`]}function Dt(e,t){if(e.route.shouldRevalidate){let n=e.route.shouldRevalidate(t);if(typeof n==`boolean`)return n}return t.defaultShouldRevalidate}function Ot(e,t,n,r,i,a){let o;if(e){let t=r[e];D(t,`No route found to patch children into: routeId = ${e}`),t.children||=[],o=t.children}else o=n.activeRoutes;let s=[],c=[];if(t.forEach(e=>{let t=o.find(t=>kt(e,t));t?c.push({existingRoute:t,newRoute:e}):s.push(e)}),s.length>0){let t=se(s,i,[e||`_`,`patch`,String(o?.length||`0`)],r);o.push(...t)}if(a&&c.length>0)for(let e=0;et.children?.some(t=>kt(e,t)))??!1:!1}var At=new WeakMap,jt=({key:e,route:t,manifest:n,mapRouteProperties:r})=>{let i=n[t.id];if(D(i,`No route found in manifest`),!i.lazy||typeof i.lazy!=`object`)return;let a=i.lazy[e];if(!a)return;let o=At.get(i);o||(o={},At.set(i,o));let s=o[e];if(s)return s;let c=(async()=>{let t=ie(e),n=i[e]!==void 0&&e!==`hasErrorBoundary`;if(t)O(!t,`Route property `+e+` is not a supported lazy route property. This property will be ignored.`),o[e]=Promise.resolve();else if(n)O(!1,`Route "${i.id}" has a static property "${e}" defined. The lazy property will be ignored.`);else{let t=await a();t!=null&&(Object.assign(i,{[e]:t}),Object.assign(i,r(i)))}typeof i.lazy==`object`&&(i.lazy[e]=void 0,Object.values(i.lazy).every(e=>e===void 0)&&(i.lazy=void 0))})();return o[e]=c,c},Mt=new WeakMap;function Nt(e,t,n,r,i){let a=n[e.id];if(D(a,`No route found in manifest`),!e.lazy)return{lazyRoutePromise:void 0,lazyHandlerPromise:void 0};if(typeof e.lazy==`function`){let t=Mt.get(a);if(t)return{lazyRoutePromise:t,lazyHandlerPromise:t};let n=(async()=>{D(typeof e.lazy==`function`,`No lazy route function found`);let t=await e.lazy(),n={};for(let e in t){let r=t[e];if(r===void 0)continue;let i=ae(e),o=a[e]!==void 0&&e!==`hasErrorBoundary`;i?O(!i,`Route property `+e+` is not a supported property to be returned from a lazy route function. This property will be ignored.`):o?O(!o,`Route "${a.id}" has a static property "${e}" defined but its lazy function is also returning a value for this property. The lazy route property "${e}" will be ignored.`):n[e]=r}Object.assign(a,n),Object.assign(a,{...r(a),lazy:void 0})})();return Mt.set(a,n),n.catch(()=>{}),{lazyRoutePromise:n,lazyHandlerPromise:n}}let o=Object.keys(e.lazy),s=[],c;for(let a of o){if(i&&i.includes(a))continue;let o=jt({key:a,route:e,manifest:n,mapRouteProperties:r});o&&(s.push(o),a===t&&(c=o))}let l=s.length>0?Promise.all(s).then(()=>{}):void 0;return l?.catch(()=>{}),c?.catch(()=>{}),{lazyRoutePromise:l,lazyHandlerPromise:c}}async function Pt(e){let t=e.matches.filter(e=>e.shouldLoad),n={};return(await Promise.all(t.map(e=>e.resolve()))).forEach((e,r)=>{n[t[r].route.id]=e}),n}async function Ft(e){return e.matches.some(e=>e.route.middleware)?It(e,()=>Pt(e)):Pt(e)}function It(e,t){return Lt(e,t,e=>{if(vn(e))throw e;return e},dn,n);function n(t,n,r){if(r)return Promise.resolve(Object.assign(r.value,{[n]:{type:`error`,result:t}}));{let{matches:r}=e,i=an(r,r[Math.min(Math.max(r.findIndex(e=>e.route.id===n),0),Math.max(r.findIndex(e=>e.shouldCallHandler()),0))].route.id).route.id;return Promise.resolve({[i]:{type:`error`,result:t}})}}}async function Lt(e,t,n,r,i){let{matches:a,...o}=e;return await Rt(o,a.flatMap(e=>e.route.middleware?e.route.middleware.map(t=>[e.route.id,t]):[]),t,n,r,i)}async function Rt(e,t,n,r,i,a,o=0){let{request:s}=e;if(s.signal.aborted)throw s.signal.reason??Error(`Request aborted: ${s.method} ${s.url}`);let c=t[o];if(!c)return await n();let[l,u]=c,d,f=async()=>{if(d)throw Error("You may only call `next()` once per middleware");try{return d={value:await Rt(e,t,n,r,i,a,o+1)},d.value}catch(e){return d={value:await a(e,l,d)},d.value}};try{let t=await u(e,f),n=t==null?void 0:r(t);return i(n)?n:d?n??d.value:(d={value:await f()},d.value)}catch(e){return await a(e,l,d)}}function zt(e,t,n,r,i){let a=jt({key:`middleware`,route:r.route,manifest:t,mapRouteProperties:e}),o=Nt(r.route,Y(n.method)?`action`:`loader`,t,e,i);return{middleware:a,route:o.lazyRoutePromise,handler:o.lazyHandlerPromise}}function Bt(e,t,n,r,i,a,o,s,c,l=null,u){let d=!1,f=zt(e,t,n,a,o);return{...a,_lazyPromises:f,shouldLoad:c,shouldRevalidateArgs:l,shouldCallHandler(e){return d=!0,l?typeof u==`boolean`?Dt(a,{...l,defaultShouldRevalidate:u}):typeof e==`boolean`?Dt(a,{...l,defaultShouldRevalidate:e}):Dt(a,l):c},resolve(e){let{lazy:t,loader:o,middleware:l}=a.route,u=d||c||e&&!Y(n.method)&&(t||o),p=l&&l.length>0&&!o&&!t;return u&&(Y(n.method)||!p)?Ut({request:n,path:r,pattern:i,match:a,lazyHandlerPromise:f?.handler,lazyRoutePromise:f?.route,handlerOverride:e,scopedContext:s}):Promise.resolve({type:`data`,result:void 0})}}}function Vt(e,t,n,r,i,a,o,s,c=null){return i.map(l=>l.route.id===a.route.id?Bt(e,t,n,r,He(i),l,o,s,!0,c):{...l,shouldLoad:!1,shouldRevalidateArgs:c,shouldCallHandler:()=>!1,_lazyPromises:zt(e,t,n,l,o),resolve:()=>Promise.resolve({type:`data`,result:void 0})})}async function Ht(e,t,n,r,i,a,o){r.some(e=>e._lazyPromises?.middleware)&&await Promise.all(r.map(e=>e._lazyPromises?.middleware));let s={request:t,url:Zt(t,n),pattern:He(r),params:r[0].params,context:a,matches:r},c=o?()=>{throw Error("You cannot call `runClientMiddleware()` from a static handler `dataStrategy`. Middleware is run outside of `dataStrategy` during SSR in order to bubble up the Response. You can enable middleware via the `respond` API in `query`/`queryRoute`")}:e=>{let t=s;return It(t,()=>e({...t,fetcherKey:i,runClientMiddleware:()=>{throw Error("Cannot call `runClientMiddleware()` from within an `runClientMiddleware` handler")}}))},l=await e({...s,fetcherKey:i,runClientMiddleware:c});try{await Promise.all(r.flatMap(e=>[e._lazyPromises?.handler,e._lazyPromises?.route]))}catch{}return l}async function Ut({request:e,path:t,pattern:n,match:r,lazyHandlerPromise:i,lazyRoutePromise:a,handlerOverride:o,scopedContext:s}){let c,l,u=Y(e.method),d=u?`action`:`loader`,f=i=>{let a,c=new Promise((e,t)=>a=t);l=()=>a(),e.signal.addEventListener(`abort`,l);let u=a=>typeof i==`function`?i({request:e,url:Zt(e,t),pattern:n,params:r.params,context:s},...a===void 0?[]:[a]):Promise.reject(Error(`You cannot call the handler for a route which defines a boolean "${d}" [routeId: ${r.route.id}]`)),f=(async()=>{try{return{type:`data`,result:await(o?o(e=>u(e)):u())}}catch(e){return{type:`error`,result:e}}})();return Promise.race([f,c])};try{let t=u?r.route.action:r.route.loader;if(i||a)if(t){let e,[n]=await Promise.all([f(t).catch(t=>{e=t}),i,a]);if(e!==void 0)throw e;c=n}else{await i;let t=u?r.route.action:r.route.loader;if(t)[c]=await Promise.all([f(t),a]);else if(d===`action`){let t=new URL(e.url),n=t.pathname+t.search;throw q(405,{method:e.method,pathname:n,routeId:r.route.id})}else return{type:`data`,result:void 0}}else if(t)c=await f(t);else{let t=new URL(e.url);throw q(404,{pathname:t.pathname+t.search})}}catch(e){return{type:`error`,result:e}}finally{l&&e.signal.removeEventListener(`abort`,l)}return c}async function Wt(e){let t=e.headers.get(`Content-Type`);return t&&/\bapplication\/json\b/.test(t)?e.body==null?null:e.json():e.text()}async function Gt(e){let{result:t,type:n}=e;if(gn(t)){let e;try{e=await Wt(t)}catch(e){return{type:`error`,error:e}}return n===`error`?{type:`error`,error:new Be(t.status,t.statusText,e),statusCode:t.status,headers:t.headers}:{type:`data`,data:e,statusCode:t.status,headers:t.headers}}return n===`error`?hn(t)?t.data instanceof Error?{type:`error`,error:t.data,statusCode:t.init?.status,headers:t.init?.headers?new Headers(t.init.headers):void 0}:{type:`error`,error:un(t),statusCode:Ve(t)?t.status:void 0,headers:t.init?.headers?new Headers(t.init.headers):void 0}:{type:`error`,error:t,statusCode:Ve(t)?t.status:void 0}:hn(t)?{type:`data`,data:t.data,statusCode:t.init?.status,headers:t.init?.headers?new Headers(t.init.headers):void 0}:{type:`data`,data:t}}function Kt(e,t,n,r,i){let a=e.headers.get(`Location`);if(D(a,`Redirects returned/thrown from loaders/actions must have a Location header`),!Te(a)){let o=r.slice(0,r.findIndex(e=>e.route.id===n)+1);a=bt(new URL(t.url),o,i,a),e.headers.set(`Location`,a)}return e}var qt=[`about:`,`blob:`,`chrome:`,`chrome-untrusted:`,`content:`,`data:`,`devtools:`,`file:`,`filesystem:`,`javascript:`];function Jt(e){try{return qt.includes(new URL(e).protocol)}catch{return!1}}function Yt(e,t,n,r){if(Te(e)){let r=e,i=S.test(r)?new URL(C(r,t.protocol)):new URL(r);if(Jt(i.toString()))throw Error(`Invalid redirect location`);let a=U(i.pathname,n)!=null;if(i.origin===t.origin&&a)return W(i.pathname)+i.search+i.hash}try{if(Jt(r.createURL(e).toString()))throw Error(`Invalid redirect location`)}catch{}return e}function Xt(e,t,n,r){let i=e.createURL(cn(t)).toString(),a={signal:n};if(r&&Y(r.formMethod)){let{formMethod:e,formEncType:t}=r;a.method=e.toUpperCase(),t===`application/json`?(a.headers=new Headers({"Content-Type":t}),a.body=JSON.stringify(r.json)):t===`text/plain`?a.body=r.text:t===`application/x-www-form-urlencoded`&&r.formData?a.body=Qt(r.formData):a.body=r.formData}return new Request(i,a)}function Zt(e,t){let n=new URL(e.url),r=typeof t==`string`?N(t):t;if(n.pathname=r.pathname||`/`,r.search){let e=new URLSearchParams(r.search),t=e.getAll(`index`);e.delete(`index`);for(let n of t.filter(Boolean))e.append(`index`,n);n.search=e.size?`?${e.toString()}`:``}else n.search=``;return n.hash=r.hash||``,n}function Qt(e){let t=new URLSearchParams;for(let[n,r]of e.entries())t.append(n,typeof r==`string`?r:r.name);return t}function $t(e){let t=new FormData;for(let[n,r]of e.entries())t.append(n,r);return t}function en(e,t,n,r=!1,i=!1){let a={},o=null,s,c=!1,l={},u=n&&J(n[1])?n[1].error:void 0;return e.forEach(n=>{if(!(n.route.id in t))return;let d=n.route.id,f=t[d];if(D(!mn(f),`Cannot handle redirect results in processLoaderData`),J(f)){let t=f.error;if(u!==void 0&&(t=u,u=void 0),o||={},i)o[d]=t;else{let n=an(e,d);o[n.route.id]??(o[n.route.id]=t)}r||(a[d]=ft),c||(c=!0,s=Ve(f.error)?f.error.status:500),f.headers&&(l[d]=f.headers)}else a[d]=f.data,f.statusCode&&f.statusCode!==200&&!c&&(s=f.statusCode),f.headers&&(l[d]=f.headers)}),u!==void 0&&n&&(o={[n[0]]:u},n[2]&&(a[n[2]]=void 0)),{loaderData:a,errors:o,statusCode:s||200,loaderHeaders:l}}function tn(e,t,n,r,i,a,o){let{loaderData:s,errors:c}=en(t,n,r);return i.filter(e=>!e.matches||e.matches.some(e=>e.shouldLoad)).forEach(t=>{let{key:n,match:r,controller:i}=t;if(i&&i.signal.aborted)return;let s=a[n];if(D(s,`Did not find corresponding fetcher result`),J(s)){let t=an(e.matches,r?.route.id);c&&c[t.route.id]||(c={...c,[t.route.id]:s.error}),o.delete(n)}else if(mn(s))D(!1,`Unhandled fetcher revalidation redirect`);else{let e=Dn(s.data);o.set(n,e)}}),{loaderData:s,errors:c}}function nn(e,t,n,r){let i=Object.entries(t).filter(([,e])=>e!==ft).reduce((e,[t,n])=>(e[t]=n,e),{});for(let a of n){let n=a.route.id;if(!t.hasOwnProperty(n)&&e.hasOwnProperty(n)&&a.route.loader&&(i[n]=e[n]),r&&r.hasOwnProperty(n))break}return i}function rn(e){return e?J(e[1])?{actionData:{}}:{actionData:{[e[0]]:e[1].data}}:{}}function an(e,t){return(t?e.slice(0,e.findIndex(e=>e.route.id===t)+1):[...e]).reverse().find(e=>e.route.hasErrorBoundary===!0)||e[0]}function on(e){let t=e.length===1?e[0]:e.find(e=>e.index||!e.path||e.path===`/`)||{id:`__shim-error-route__`};return{matches:[{params:{},pathname:``,pathnameBase:``,route:t}],route:t}}function q(e,{pathname:t,routeId:n,method:r,type:i,message:a}={}){let o=`Unknown Server Error`,s=`Unknown @remix-run/router error`;return e===400?(o=`Bad Request`,r&&t&&n?s=`You made a ${r} request to "${t}" but did not provide a \`loader\` for route "${n}", so there is no way to handle the request.`:i===`invalid-body`&&(s=`Unable to encode submission body`)):e===403?(o=`Forbidden`,s=`Route "${n}" does not match URL "${t}"`):e===404?(o=`Not Found`,s=`No route matches URL "${t}"`):e===405&&(o=`Method Not Allowed`,r&&t&&n?s=`You made a ${r.toUpperCase()} request to "${t}" but did not provide an \`action\` for route "${n}", so there is no way to handle the request.`:r&&(s=`Invalid request method "${r.toUpperCase()}"`)),new Be(e||500,o,Error(s),!0)}function sn(e){let t=Object.entries(e);for(let e=t.length-1;e>=0;e--){let[n,r]=t[e];if(mn(r))return{key:n,result:r}}}function cn(e){return M({...typeof e==`string`?N(e):e,hash:``})}function ln(e,t){return e.pathname!==t.pathname||e.search!==t.search?!1:e.hash===``?t.hash!==``:e.hash===t.hash?!0:t.hash!==``}function un(e){return new Be(e.init?.status??500,e.init?.statusText??`Internal Server Error`,e.data)}function dn(e){return typeof e==`object`&&!!e&&Object.entries(e).every(([e,t])=>typeof e==`string`&&fn(t))}function fn(e){return typeof e==`object`&&!!e&&`type`in e&&`result`in e&&(e.type===`data`||e.type===`error`)}function pn(e){return gn(e.result)&&at.has(e.result.status)}function J(e){return e.type===`error`}function mn(e){return(e&&e.type)===`redirect`}function hn(e){return typeof e==`object`&&!!e&&`type`in e&&`data`in e&&`init`in e&&e.type===`DataWithResponseInit`}function gn(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.headers==`object`&&e.body!==void 0}function _n(e){return at.has(e)}function vn(e){return gn(e)&&_n(e.status)&&e.headers.has(`Location`)}function yn(e){return it.has(e.toUpperCase())}function Y(e){return nt.has(e.toUpperCase())}function bn(e){return new URLSearchParams(e).getAll(`index`).some(e=>e===``)}function xn(e,t){let n=typeof t==`string`?N(t).search:t.search;if(e[e.length-1].route.index&&bn(n||``))return e[e.length-1];let r=ke(e);return r[r.length-1]}function Sn(e){let{formMethod:t,formAction:n,formEncType:r,text:i,formData:a,json:o}=e;if(!(!t||!n||!r)){if(i!=null)return{formMethod:t,formAction:n,formEncType:r,formData:void 0,json:void 0,text:i};if(a!=null)return{formMethod:t,formAction:n,formEncType:r,formData:a,json:void 0,text:void 0};if(o!==void 0)return{formMethod:t,formAction:n,formEncType:r,formData:void 0,json:o,text:void 0}}}function Cn(e,t,n,r){return r?{state:`loading`,location:e,matches:t,historyAction:n,formMethod:r.formMethod,formAction:r.formAction,formEncType:r.formEncType,formData:r.formData,json:r.json,text:r.text}:{state:`loading`,location:e,matches:t,historyAction:n,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0}}function wn(e,t,n,r){return{state:`submitting`,location:e,matches:t,historyAction:n,formMethod:r.formMethod,formAction:r.formAction,formEncType:r.formEncType,formData:r.formData,json:r.json,text:r.text}}function Tn(e,t){return e?{state:`loading`,formMethod:e.formMethod,formAction:e.formAction,formEncType:e.formEncType,formData:e.formData,json:e.json,text:e.text,data:t}:{state:`loading`,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0,data:t}}function En(e,t){return{state:`submitting`,formMethod:e.formMethod,formAction:e.formAction,formEncType:e.formEncType,formData:e.formData,json:e.json,text:e.text,data:t?t.data:void 0}}function Dn(e){return{state:`idle`,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0,data:e}}function On(e,t){try{let n=e.sessionStorage.getItem(dt);if(n){let e=JSON.parse(n);for(let[n,r]of Object.entries(e||{}))r&&Array.isArray(r)&&t.set(n,new Set(r||[]))}}catch{}}function kn(e,t){if(t.size>0){let n={};for(let[e,r]of t)n[e]=[...r];try{e.sessionStorage.setItem(dt,JSON.stringify(n))}catch(e){O(!1,`Failed to save applied view transitions in sessionStorage (${e}).`)}}}function An(){let e,t,n=new Promise((r,i)=>{e=async e=>{r(e);try{await n}catch{}},t=async e=>{i(e);try{await n}catch{}}});return{promise:n,resolve:e,reject:t}}var jn=d.createContext(null);jn.displayName=`DataRouter`;var Mn=d.createContext(null);Mn.displayName=`DataRouterState`;var Nn=d.createContext(!1);function Pn(){return d.useContext(Nn)}var Fn=d.createContext({isTransitioning:!1});Fn.displayName=`ViewTransition`;var In=d.createContext(new Map);In.displayName=`Fetchers`;var Ln=d.createContext(null);Ln.displayName=`Await`;var X=d.createContext(null);X.displayName=`Navigation`;var Rn=d.createContext(null);Rn.displayName=`Location`;var Z=d.createContext({outlet:null,matches:[],isDataRoute:!1});Z.displayName=`Route`;var zn=d.createContext(null);zn.displayName=`RouteError`;var Bn=`REACT_ROUTER_ERROR`,Vn=`REDIRECT`,Hn=`ROUTE_ERROR_RESPONSE`;function Un(e){if(e.startsWith(`${Bn}:${Vn}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function Wn(e){if(e.startsWith(`${Bn}:${Hn}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Be(t.status,t.statusText,t.data)}catch{}}function Gn(e,{relative:t}={}){D(Kn(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=d.useContext(X),{hash:i,pathname:a,search:o}=er(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:G([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Kn(){return d.useContext(Rn)!=null}function Q(){return D(Kn(),`useLocation() may be used only in the context of a component.`),d.useContext(Rn).location}var qn=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function Jn(e){d.useContext(X).static||d.useLayoutEffect(e)}function Yn(){let{isDataRoute:e}=d.useContext(Z);return e?br():Xn()}function Xn(){D(Kn(),`useNavigate() may be used only in the context of a component.`);let e=d.useContext(jn),{basename:t,navigator:n}=d.useContext(X),{matches:r}=d.useContext(Z),{pathname:i}=Q(),a=JSON.stringify(Ae(r)),o=d.useRef(!1);return Jn(()=>{o.current=!0}),d.useCallback((r,s={})=>{if(O(o.current,qn),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=je(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:G([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var Zn=d.createContext(null);function Qn(e){let t=d.useContext(Z).outlet;return d.useMemo(()=>t&&d.createElement(Zn.Provider,{value:e},t),[t,e])}function $n(){let{matches:e}=d.useContext(Z);return e[e.length-1]?.params??{}}function er(e,{relative:t}={}){let{matches:n}=d.useContext(Z),{pathname:r}=Q(),i=JSON.stringify(Ae(n));return d.useMemo(()=>je(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function tr(e,t,n){D(Kn(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=d.useContext(X),{matches:i}=d.useContext(Z),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;Sr(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. -Please change the parent to .`)}let u=Q(),f;if(t){let e=typeof t==`string`?N(t):t;D(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),f=e}else f=u;let p=f.pathname||`/`,m=p;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);m=`/`+p.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let h=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):L(e,{pathname:m});O(l||h!=null,`No routes matched location "${f.pathname}${f.search}${f.hash}" `),O(h==null||h[h.length-1].route.element!==void 0||h[h.length-1].route.Component!==void 0||h[h.length-1].route.lazy!==void 0,`Matched leaf route at location "${f.pathname}${f.search}${f.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let g=cr(h&&h.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:G([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:G([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&g?d.createElement(Rn.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...f},navigationType:`POP`}},g):g}function nr(){let e=yr(),t=Ve(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=d.createElement(d.Fragment,null,d.createElement(`p`,null,`💿 Hey developer 👋`),d.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,d.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,d.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),d.createElement(d.Fragment,null,d.createElement(`h2`,null,`Unexpected Application Error!`),d.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?d.createElement(`pre`,{style:i},n):null,o)}var rr=d.createElement(nr,null),ir=class extends d.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=Wn(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:d.createElement(Z.Provider,{value:this.props.routeContext},d.createElement(zn.Provider,{value:e,children:this.props.component}));return this.context?d.createElement(or,{error:e},t):t}};ir.contextType=Nn;var ar=new WeakMap;function or({children:e,error:t}){let{basename:n}=d.useContext(X);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Un(t.digest);if(e){let r=ar.get(t);if(r)throw r;let i=We(e.location,n),a=i.absoluteURL||i.to;if(Jt(a))throw Error(`Invalid redirect location`);if(Ue&&!ar.get(t))if(i.isExternal||e.reloadDocument)window.location.href=a;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw ar.set(t,n),n}return d.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${a}`})}}return e}function sr({routeContext:e,match:t,children:n}){let r=d.useContext(jn);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),d.createElement(Z.Provider,{value:e},n)}function cr(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);D(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:He(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,f=!1,p=null,m=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,p=n.route.errorElement||rr,o&&(s<0&&c===0?(Sr(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),f=!0,m=null):s===c&&(f=!0,m=n.route.hydrateFallbackElement||null)));let h=t.concat(i.slice(0,c+1)),g=()=>{let t;return t=u?p:f?m:n.route.Component?d.createElement(n.route.Component,null):n.route.element?n.route.element:e,d.createElement(sr,{match:n,routeContext:{outlet:e,matches:h,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?d.createElement(ir,{location:r.location,revalidation:r.revalidation,component:p,error:u,children:g(),routeContext:{outlet:null,matches:h,isDataRoute:!0},onError:l}):g()},null)}function lr(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function ur(e){let t=d.useContext(jn);return D(t,lr(e)),t}function dr(e){let t=d.useContext(Mn);return D(t,lr(e)),t}function fr(e){let t=d.useContext(Z);return D(t,lr(e)),t}function pr(e){let t=fr(e),n=t.matches[t.matches.length-1];return D(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function mr(){return pr(`useRouteId`)}function hr(){let e=dr(`useNavigation`);return d.useMemo(()=>{let{matches:t,historyAction:n,...r}=e.navigation;return r},[e.navigation])}function gr(){let{matches:e,loaderData:t}=dr(`useMatches`);return d.useMemo(()=>e.map(e=>le(e,t)),[e,t])}function _r(){let e=dr(`useLoaderData`),t=pr(`useLoaderData`);return e.loaderData[t]}function vr(){let e=dr(`useActionData`),t=pr(`useLoaderData`);return e.actionData?e.actionData[t]:void 0}function yr(){let e=d.useContext(zn),t=dr(`useRouteError`),n=pr(`useRouteError`);return e===void 0?t.errors?.[n]:e}function br(){let{router:e}=ur(`useNavigate`),t=pr(`useNavigate`),n=d.useRef(!1);return Jn(()=>{n.current=!0}),d.useCallback(async(r,i={})=>{O(n.current,qn),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var xr={};function Sr(e,t,n){!t&&!xr[e]&&(xr[e]=!0,O(!1,n))}var Cr={};function wr(e,t){!e&&!Cr[t]&&(Cr[t]=!0,console.warn(t))}var Tr=d.useOptimistic,Er=()=>void 0;function Dr(e){return Tr?Tr(e):[e,Er]}function Or(e){let t={hasErrorBoundary:e.hasErrorBoundary||e.ErrorBoundary!=null||e.errorElement!=null};return e.Component&&(e.element&&O(!1,"You should not include both `Component` and `element` on your route - `Component` will be used."),Object.assign(t,{element:d.createElement(e.Component),Component:void 0})),e.HydrateFallback&&(e.hydrateFallbackElement&&O(!1,"You should not include both `HydrateFallback` and `hydrateFallbackElement` on your route - `HydrateFallback` will be used."),Object.assign(t,{hydrateFallbackElement:d.createElement(e.HydrateFallback),HydrateFallback:void 0})),e.ErrorBoundary&&(e.errorElement&&O(!1,"You should not include both `ErrorBoundary` and `errorElement` on your route - `ErrorBoundary` will be used."),Object.assign(t,{errorElement:d.createElement(e.ErrorBoundary),ErrorBoundary:void 0})),t}var kr=[`HydrateFallback`,`hydrateFallbackElement`],Ar=class{constructor(){this.status=`pending`,this.promise=new Promise((e,t)=>{this.resolve=t=>{this.status===`pending`&&(this.status=`resolved`,e(t))},this.reject=e=>{this.status===`pending`&&(this.status=`rejected`,t(e))}})}};function jr({router:e,flushSync:t,onError:n,useTransitions:r}){r=Pn()||r;let[i,a]=d.useState(e.state),[o,s]=Dr(i),[c,l]=d.useState(),[u,f]=d.useState({isTransitioning:!1}),[p,m]=d.useState(),[h,g]=d.useState(),[_,v]=d.useState(),y=d.useRef(new Map),b=d.useCallback((i,{deletedFetchers:o,newErrors:c,flushSync:u,viewTransitionOpts:_})=>{c&&n&&Object.values(c).forEach(e=>n(e,{location:i.location,params:i.matches[0]?.params??{},pattern:He(i.matches)})),i.fetchers.forEach((e,t)=>{e.data!==void 0&&y.current.set(t,e.data)}),o.forEach(e=>y.current.delete(e)),wr(u===!1||t!=null,'You provided the `flushSync` option to a router update, but you are not using the `` from `react-router/dom` so `ReactDOM.flushSync()` is unavailable. Please update your app to `import { RouterProvider } from "react-router/dom"` and ensure you have `react-dom` installed as a dependency to use the `flushSync` option.');let b=e.window!=null&&e.window.document!=null&&typeof e.window.document.startViewTransition==`function`;if(wr(_==null||b,"You provided the `viewTransition` option to a router update, but you do not appear to be running in a DOM environment as `window.startViewTransition` is not available."),!_||!b){t&&u?t(()=>a(i)):r===!1?a(i):d.startTransition(()=>{r===!0&&s(e=>Mr(e,i)),a(i)});return}if(t&&u){t(()=>{h&&(p?.resolve(),h.skipTransition()),f({isTransitioning:!0,flushSync:!0,currentLocation:_.currentLocation,nextLocation:_.nextLocation})});let n=e.window.document.startViewTransition(()=>{t(()=>a(i))});n.finished.finally(()=>{t(()=>{m(void 0),g(void 0),l(void 0),f({isTransitioning:!1})})}),t(()=>g(n));return}h?(p?.resolve(),h.skipTransition(),v({state:i,currentLocation:_.currentLocation,nextLocation:_.nextLocation})):(l(i),f({isTransitioning:!0,flushSync:!1,currentLocation:_.currentLocation,nextLocation:_.nextLocation}))},[e.window,t,h,p,r,s,n]);d.useLayoutEffect(()=>e.subscribe(b),[e,b]),d.useEffect(()=>{u.isTransitioning&&!u.flushSync&&m(new Ar)},[u]),d.useEffect(()=>{if(p&&c&&e.window){let t=c,n=p.promise,i=e.window.document.startViewTransition(async()=>{r===!1?a(t):d.startTransition(()=>{r===!0&&s(e=>Mr(e,t)),a(t)}),await n});i.finished.finally(()=>{m(void 0),g(void 0),l(void 0),f({isTransitioning:!1})}),g(i)}},[c,p,e.window,r,s]),d.useEffect(()=>{p&&c&&o.location.key===c.location.key&&p.resolve()},[p,h,o.location,c]),d.useEffect(()=>{!u.isTransitioning&&_&&(l(_.state),f({isTransitioning:!0,flushSync:!1,currentLocation:_.currentLocation,nextLocation:_.nextLocation}),v(void 0))},[u.isTransitioning,_]);let x=d.useMemo(()=>({createHref:e.createHref,encodeLocation:e.encodeLocation,go:t=>e.navigate(t),push:(t,n,r)=>e.navigate(t,{state:n,preventScrollReset:r?.preventScrollReset}),replace:(t,n,r)=>e.navigate(t,{replace:!0,state:n,preventScrollReset:r?.preventScrollReset})}),[e]),S=e.basename||`/`,C=d.useMemo(()=>({router:e,navigator:x,static:!1,basename:S,onError:n}),[e,x,S,n]);return d.createElement(d.Fragment,null,d.createElement(jn.Provider,{value:C},d.createElement(Mn.Provider,{value:o},d.createElement(In.Provider,{value:y.current},d.createElement(Fn.Provider,{value:u},d.createElement(Ir,{basename:S,location:o.location,navigationType:o.historyAction,navigator:x,useTransitions:r},d.createElement(Nr,{routes:e.routes,manifest:e.manifest,future:e.future,state:o,isStatic:!1,onError:n})))))),null)}function Mr(e,t){return{...e,navigation:t.navigation.state===`idle`?e.navigation:t.navigation,revalidation:t.revalidation===`idle`?e.revalidation:t.revalidation,actionData:t.navigation.state===`submitting`?e.actionData:t.actionData,fetchers:t.fetchers}}var Nr=d.memo(Pr);function Pr({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return tr(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Fr(e){return Qn(e.context)}function Ir({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){D(!Kn(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=d.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=N(n));let{pathname:l=`/`,search:u=``,hash:f=``,state:p=null,key:m=`default`,mask:h}=n,g=d.useMemo(()=>{let e=U(l,s);return e==null?null:{location:{pathname:e,search:u,hash:f,state:p,key:m,mask:h},navigationType:r}},[s,l,u,f,p,m,r,h]);return O(g!=null,` is not able to match the URL "${l}${u}${f}" because it does not start with the basename, so the won't render anything.`),g==null?null:d.createElement(X.Provider,{value:c},d.createElement(Rn.Provider,{children:t,value:g}))}d.Component;function Lr(){return{params:$n(),loaderData:_r(),actionData:vr(),matches:gr()}}function Rr(e){return function(){let t=Lr();return d.createElement(e,t)}}function zr(){return{params:$n(),loaderData:_r(),actionData:vr(),error:yr()}}function Br(e){return function(){let t=zr();return d.createElement(e,t)}}var Vr=`get`,Hr=`application/x-www-form-urlencoded`;function Ur(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Wr(e){return Ur(e)&&e.tagName.toLowerCase()===`button`}function Gr(e){return Ur(e)&&e.tagName.toLowerCase()===`form`}function Kr(e){return Ur(e)&&e.tagName.toLowerCase()===`input`}function qr(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Jr(e,t){return e.button===0&&(!t||t===`_self`)&&!qr(e)}var Yr=null;function Xr(){if(Yr===null)try{new FormData(document.createElement(`form`),0),Yr=!1}catch{Yr=!0}return Yr}var Zr=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Qr(e){return e!=null&&!Zr.has(e)?(O(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Hr}"`),null):e}function $r(e,t){let n,r,i,a,o;if(Gr(e)){let o=e.getAttribute(`action`);r=o?U(o,t):null,n=e.getAttribute(`method`)||Vr,i=Qr(e.getAttribute(`enctype`))||Hr,a=new FormData(e)}else if(Wr(e)||Kr(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a + + + ) + } + renderRoute(WithBack, "/", "/?muted=1") + await screen.findByText("Bad Bunny") + + await userEvent.click(customToggle()) + expect(shown()).toEqual(["Bad Bunny", "Drake"]) + + const card = screen.getByText("Bad Bunny").closest("li")! + await userEvent.click( + within(card).getByRole("checkbox", { name: /^Mute guest feature alerts/ }) + ) + await waitFor(() => expect(updateWatchlistPreferences).toHaveBeenCalled()) + expect(shown()).toEqual(["Bad Bunny", "Drake"]) + + await userEvent.click(screen.getByRole("button", { name: "history back" })) + + await waitFor(() => expect(shown()).toEqual(["Bad Two", "Drake"])) + }) + it("drops an invalid muted or custom param with replace", async () => { mockListWatchlist.mockResolvedValue(rows) renderRoute(WatchlistWithProbe, "/", "/?muted=yes&custom=2&q=b") diff --git a/web/app/routes/watchlist.tsx b/web/app/routes/watchlist.tsx index 6737480..c056e8a 100644 --- a/web/app/routes/watchlist.tsx +++ b/web/app/routes/watchlist.tsx @@ -145,9 +145,19 @@ export default function Watchlist() { if (qTimer.current) clearTimeout(qTimer.current) lastWrittenQ.current = view.q setQuery(view.q) + clearSticky() } }, [view.q]) + // Back/Forward changes filters without going through a handler (G4). + const filterKey = `${view.sort}|${view.tagIds.join(",")}|${view.muted}|${view.custom}` + const prevFilterKey = useRef(filterKey) + useEffect(() => { + if (prevFilterKey.current === filterKey) return + prevFilterKey.current = filterKey + clearSticky() + }, [filterKey]) + // Drop an unknown or default sort, bad tag ids, and non-"1" flags from the // URL without a history entry. Once validated, retired and unknown tag ids go too. useEffect(() => { From 4bdc558d41fc860a43952454c7b2808685d4caca Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 13:02:36 -0500 Subject: [PATCH 054/142] fix(25): WR-03 ignore malformed release dates in watchlist latest/next release --- internal/db/sqlc/querier.go | 6 ++++-- internal/db/sqlc/watchlist.sql.go | 18 ++++++++++++------ internal/watchlist/service_test.go | 2 ++ queries/watchlist.sql | 18 ++++++++++++------ 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/internal/db/sqlc/querier.go b/internal/db/sqlc/querier.go index efd49c2..41fecb7 100644 --- a/internal/db/sqlc/querier.go +++ b/internal/db/sqlc/querier.go @@ -99,7 +99,8 @@ type Querier interface { // ListWatchlist's select list narrowed to one row (D-26); Service.get only // compiles while the projections stay identical. // latest/upcoming new_release dates (G1-G3): no retention window; Postgres - // decides "upcoming" at the date's own precision against UTC today. Plain + // decides "upcoming" at the date's own precision against UTC today; malformed + // or placeholder dates (e.g. 0000-00-00) are skipped. Plain // columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. GetWatchlistEntry(ctx context.Context, id int64) (GetWatchlistEntryRow, error) // D-14's implicit seed-mode check, scoped per-source per D-15: zero @@ -229,7 +230,8 @@ type Querier interface { // (sqlc#3438 emits interface{}). They must stay identical but for the column, // or zipTags would mispair ids and names. // latest/upcoming new_release dates (G1-G3): no retention window; Postgres - // decides "upcoming" at the date's own precision against UTC today. Plain + // decides "upcoming" at the date's own precision against UTC today; malformed + // or placeholder dates (e.g. 0000-00-00) are skipped. Plain // columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. ListWatchlist(ctx context.Context) ([]ListWatchlistRow, error) // Merge statement order and why it never inserts: ADR 0004. diff --git a/internal/db/sqlc/watchlist.sql.go b/internal/db/sqlc/watchlist.sql.go index d11781c..2ff3735 100644 --- a/internal/db/sqlc/watchlist.sql.go +++ b/internal/db/sqlc/watchlist.sql.go @@ -98,14 +98,16 @@ JOIN artists a ON a.id = w.artist_id LEFT JOIN LATERAL ( SELECT e.release_date FROM events e WHERE e.artist_id = a.id AND e.event_type = 'new_release' - AND e.release_date IS NOT NULL + AND e.release_date ~ '^[0-9]{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[12][0-9]|3[01]))?)?$' + AND e.release_date >= '0001' AND e.release_date <= left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) ORDER BY e.release_date DESC LIMIT 1 ) latest ON true LEFT JOIN LATERAL ( SELECT e.release_date FROM events e WHERE e.artist_id = a.id AND e.event_type = 'new_release' - AND e.release_date IS NOT NULL + AND e.release_date ~ '^[0-9]{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[12][0-9]|3[01]))?)?$' + AND e.release_date >= '0001' AND e.release_date > left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) ORDER BY e.release_date ASC LIMIT 1 ) upcoming ON true @@ -134,7 +136,8 @@ type GetWatchlistEntryRow struct { // ListWatchlist's select list narrowed to one row (D-26); Service.get only // compiles while the projections stay identical. // latest/upcoming new_release dates (G1-G3): no retention window; Postgres -// decides "upcoming" at the date's own precision against UTC today. Plain +// decides "upcoming" at the date's own precision against UTC today; malformed +// or placeholder dates (e.g. 0000-00-00) are skipped. Plain // columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. func (q *Queries) GetWatchlistEntry(ctx context.Context, id int64) (GetWatchlistEntryRow, error) { row := q.db.QueryRow(ctx, getWatchlistEntry, id) @@ -179,14 +182,16 @@ JOIN artists a ON a.id = w.artist_id LEFT JOIN LATERAL ( SELECT e.release_date FROM events e WHERE e.artist_id = a.id AND e.event_type = 'new_release' - AND e.release_date IS NOT NULL + AND e.release_date ~ '^[0-9]{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[12][0-9]|3[01]))?)?$' + AND e.release_date >= '0001' AND e.release_date <= left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) ORDER BY e.release_date DESC LIMIT 1 ) latest ON true LEFT JOIN LATERAL ( SELECT e.release_date FROM events e WHERE e.artist_id = a.id AND e.event_type = 'new_release' - AND e.release_date IS NOT NULL + AND e.release_date ~ '^[0-9]{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[12][0-9]|3[01]))?)?$' + AND e.release_date >= '0001' AND e.release_date > left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) ORDER BY e.release_date ASC LIMIT 1 ) upcoming ON true @@ -224,7 +229,8 @@ type ListWatchlistRow struct { // (sqlc#3438 emits interface{}). They must stay identical but for the column, // or zipTags would mispair ids and names. // latest/upcoming new_release dates (G1-G3): no retention window; Postgres -// decides "upcoming" at the date's own precision against UTC today. Plain +// decides "upcoming" at the date's own precision against UTC today; malformed +// or placeholder dates (e.g. 0000-00-00) are skipped. Plain // columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. func (q *Queries) ListWatchlist(ctx context.Context) ([]ListWatchlistRow, error) { rows, err := q.db.Query(ctx, listWatchlist) diff --git a/internal/watchlist/service_test.go b/internal/watchlist/service_test.go index 3f19add..42c3a1f 100644 --- a/internal/watchlist/service_test.go +++ b/internal/watchlist/service_test.go @@ -2448,6 +2448,8 @@ func TestService_List_LatestRelease(t *testing.T) { {"null-dated new_release ignored", []releaseSeed{{nr, nil, 0}}, nil, nil}, {"old event still counts (no retention)", []releaseSeed{{nr, ptr("1999-01-01"), 400 * 24 * time.Hour}}, ptr("1999-01-01"), nil}, {"current UTC year is latest, next year is next", []releaseSeed{{nr, ptr(thisYear), 0}, {nr, ptr(nextYear), 0}}, ptr(thisYear), ptr(nextYear)}, + {"placeholder and malformed dates ignored", []releaseSeed{{nr, ptr("0000-00-00"), 0}, {nr, ptr("0000"), 0}, {nr, ptr("2020-13"), 0}, {nr, ptr("TBA"), 0}}, nil, nil}, + {"malformed date never beats a real one", []releaseSeed{{nr, ptr("1999-05-17"), 0}, {nr, ptr("0000-00-00"), 0}}, ptr("1999-05-17"), nil}, {"zero events", nil, nil, nil}, } diff --git a/queries/watchlist.sql b/queries/watchlist.sql index 627cd21..8e7cc2b 100644 --- a/queries/watchlist.sql +++ b/queries/watchlist.sql @@ -33,19 +33,22 @@ SELECT w.id AS id, a.id AS artist_id, a.mbid, a.name, a.deezer_id, FROM watchlist w JOIN artists a ON a.id = w.artist_id -- latest/upcoming new_release dates (G1-G3): no retention window; Postgres --- decides "upcoming" at the date's own precision against UTC today. Plain +-- decides "upcoming" at the date's own precision against UTC today; malformed +-- or placeholder dates (e.g. 0000-00-00) are skipped. Plain -- columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. LEFT JOIN LATERAL ( SELECT e.release_date FROM events e WHERE e.artist_id = a.id AND e.event_type = 'new_release' - AND e.release_date IS NOT NULL + AND e.release_date ~ '^[0-9]{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[12][0-9]|3[01]))?)?$' + AND e.release_date >= '0001' AND e.release_date <= left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) ORDER BY e.release_date DESC LIMIT 1 ) latest ON true LEFT JOIN LATERAL ( SELECT e.release_date FROM events e WHERE e.artist_id = a.id AND e.event_type = 'new_release' - AND e.release_date IS NOT NULL + AND e.release_date ~ '^[0-9]{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[12][0-9]|3[01]))?)?$' + AND e.release_date >= '0001' AND e.release_date > left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) ORDER BY e.release_date ASC LIMIT 1 ) upcoming ON true @@ -70,19 +73,22 @@ SELECT w.id AS id, a.id AS artist_id, a.mbid, a.name, a.deezer_id, FROM watchlist w JOIN artists a ON a.id = w.artist_id -- latest/upcoming new_release dates (G1-G3): no retention window; Postgres --- decides "upcoming" at the date's own precision against UTC today. Plain +-- decides "upcoming" at the date's own precision against UTC today; malformed +-- or placeholder dates (e.g. 0000-00-00) are skipped. Plain -- columns, not aggregates, so sqlc emits *string and no-release artists scan NULL. LEFT JOIN LATERAL ( SELECT e.release_date FROM events e WHERE e.artist_id = a.id AND e.event_type = 'new_release' - AND e.release_date IS NOT NULL + AND e.release_date ~ '^[0-9]{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[12][0-9]|3[01]))?)?$' + AND e.release_date >= '0001' AND e.release_date <= left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) ORDER BY e.release_date DESC LIMIT 1 ) latest ON true LEFT JOIN LATERAL ( SELECT e.release_date FROM events e WHERE e.artist_id = a.id AND e.event_type = 'new_release' - AND e.release_date IS NOT NULL + AND e.release_date ~ '^[0-9]{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[12][0-9]|3[01]))?)?$' + AND e.release_date >= '0001' AND e.release_date > left(to_char((now() AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), length(e.release_date)) ORDER BY e.release_date ASC LIMIT 1 ) upcoming ON true From 9cc7050442dc004166c4f74d05d961b27229cf7e Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 13:04:55 -0500 Subject: [PATCH 055/142] fix(25): WR-04 drive count-announcement tests with fake timers --- web/app/routes/watchlist.test.tsx | 61 +++++++++++++++++++------------ 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/web/app/routes/watchlist.test.tsx b/web/app/routes/watchlist.test.tsx index 5732e66..c1ddd92 100644 --- a/web/app/routes/watchlist.test.tsx +++ b/web/app/routes/watchlist.test.tsx @@ -1399,40 +1399,55 @@ describe("Watchlist sort (WLVW-02, WLVW-03)", () => { } ) - it("announces the sort immediately with no count message after it", async () => { + // RTL's asyncWrapper awaits a real setTimeout, so shouldAdvanceTime is + // required; the extra advance after the pick is what trips the 500 ms timer. + async function pickSortWithFakeTimers(typed: string | null) { mockListWatchlist.mockResolvedValue(sortEntries) - renderRoute(Watchlist, "/") await screen.findByText("Alpha") - await userEvent.click(screen.getByRole("combobox", { name: "Sort by" })) - await userEvent.click( + vi.useFakeTimers({ shouldAdvanceTime: true }) + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }) + if (typed !== null) { + await user.type( + screen.getByRole("textbox", { name: "Filter watchlist" }), + typed + ) + } + await user.click(screen.getByRole("combobox", { name: "Sort by" })) + await user.click( await screen.findByRole("option", { name: "Date added (newest)" }) ) + } - expect(status()).toHaveTextContent("Sorted by Date added (newest).") - await new Promise((r) => setTimeout(r, 700)) - expect(status()).toHaveTextContent("Sorted by Date added (newest).") - expect(status()).not.toHaveTextContent("Showing") + it("announces the sort immediately with no count message after it", async () => { + try { + await pickSortWithFakeTimers(null) + + expect(status()).toHaveTextContent("Sorted by Date added (newest).") + act(() => { + vi.advanceTimersByTime(1000) + }) + expect(status()).toHaveTextContent("Sorted by Date added (newest).") + expect(status()).not.toHaveTextContent("Showing") + } finally { + vi.useRealTimers() + } }) it("cancels a pending count announcement when the sort changes", async () => { - mockListWatchlist.mockResolvedValue(sortEntries) + try { + await pickSortWithFakeTimers("a") - renderRoute(Watchlist, "/") - await screen.findByText("Alpha") - - await userEvent.type( - screen.getByRole("textbox", { name: "Filter watchlist" }), - "a" - ) - await userEvent.click(screen.getByRole("combobox", { name: "Sort by" })) - await userEvent.click( - await screen.findByRole("option", { name: "Date added (newest)" }) - ) - await new Promise((r) => setTimeout(r, 700)) - - expect(status()).toHaveTextContent("Sorted by Date added (newest).") + expect(status()).toHaveTextContent("Sorted by Date added (newest).") + act(() => { + vi.advanceTimersByTime(1000) + }) + expect(status()).toHaveTextContent("Sorted by Date added (newest).") + expect(status()).not.toHaveTextContent("Showing") + } finally { + vi.useRealTimers() + } }) it("keeps the sort when Clear filters runs", async () => { From befe939430f61e83746701d765cd303df8c1b618 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Sat, 10 Oct 2026 13:06:32 -0500 Subject: [PATCH 056/142] build(25): rebuild embedded SPA after review fixes --- .../assets/{history-CiwTCwWj.js => history-CxaPqYlO.js} | 2 +- .../assets/{manifest-645baaf8.js => manifest-5df375ec.js} | 2 +- internal/webassets/build/client/assets/watchlist-BuIICAgv.js | 1 - internal/webassets/build/client/assets/watchlist-DVr_lpXm.js | 1 + internal/webassets/build/client/index.html | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename internal/webassets/build/client/assets/{history-CiwTCwWj.js => history-CxaPqYlO.js} (76%) rename internal/webassets/build/client/assets/{manifest-645baaf8.js => manifest-5df375ec.js} (92%) delete mode 100644 internal/webassets/build/client/assets/watchlist-BuIICAgv.js create mode 100644 internal/webassets/build/client/assets/watchlist-DVr_lpXm.js diff --git a/internal/webassets/build/client/assets/history-CiwTCwWj.js b/internal/webassets/build/client/assets/history-CxaPqYlO.js similarity index 76% rename from internal/webassets/build/client/assets/history-CiwTCwWj.js rename to internal/webassets/build/client/assets/history-CxaPqYlO.js index a31c8f7..71b5dfa 100644 --- a/internal/webassets/build/client/assets/history-CiwTCwWj.js +++ b/internal/webassets/build/client/assets/history-CxaPqYlO.js @@ -1 +1 @@ -import{N as e,j as t,k as n,t as r}from"./jsx-runtime-D0QO-s14.js";import{C as i,d as a,f as o,ot as s,u as c}from"./api-k_NcBvGr.js";import{i as l,r as u,t as d}from"./useUrlParams-DI9KFxsS.js";import{n as f,r as p,t as m}from"./badge-B492YG3Q.js";var h=e(t(),1),g=r(),_=[{value:`new_release`,label:`New release`},{value:`guest_feature`,label:`Guest feature`},{value:`deluxe_change`,label:`Deluxe change`}],v=`flex h-9 max-w-64 min-w-40 items-center justify-between gap-2 rounded-md border border-input bg-input/30 px-3 text-body text-foreground outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50`,y=`cursor-pointer px-3 py-1.5 text-body text-popover-foreground data-[highlighted=true]:bg-accent`;function b({label:e,options:t,value:n,onSelect:r,triggerLabel:i,triggerRef:a}){let[o,s]=(0,h.useState)(!1),[c,l]=(0,h.useState)(0),u=(0,h.useRef)(null),d=(0,h.useRef)(null),f=a??d,p=(0,h.useId)(),m=t.findIndex(e=>e.value===``?n===null:e.value===n),_=t[m===-1?0:m],b=e=>`${p}-option-${e}`,x=i??_?.label;(0,h.useEffect)(()=>{o&&document.getElementById(b(c))?.scrollIntoView?.({block:`nearest`})},[o,c]),(0,h.useEffect)(()=>{if(!o)return;function e(e){u.current?.contains(e.target)||s(!1)}return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[o]);function S(e){l(e<0?0:e),s(!0)}function C(e){let n=t[e];n&&(r(n.value===``?null:n.value),s(!1),f.current?.focus())}function w(e){if(!o){(e.key===`ArrowDown`||e.key===`ArrowUp`||e.key===`Enter`||e.key===` `)&&(e.preventDefault(),S(m===-1?0:m));return}e.key===`ArrowDown`?(e.preventDefault(),l(e=>Math.min(e+1,t.length-1))):e.key===`ArrowUp`?(e.preventDefault(),l(e=>Math.max(e-1,0))):e.key===`Enter`||e.key===` `?(e.preventDefault(),C(c)):e.key===`Escape`&&(e.preventDefault(),s(!1),f.current?.focus())}return(0,g.jsxs)(`div`,{ref:u,className:`relative`,children:[(0,g.jsxs)(`button`,{ref:f,type:`button`,role:`combobox`,"aria-expanded":o,"aria-haspopup":`listbox`,"aria-controls":p,"aria-label":e,"aria-activedescendant":o?b(c):void 0,className:v,onClick:()=>o?s(!1):S(m===-1?0:m),onKeyDown:w,children:[(0,g.jsx)(`span`,{className:`truncate`,title:x,children:x}),(0,g.jsx)(`span`,{"aria-hidden":`true`,className:`text-muted-foreground`,children:`▾`})]}),o&&(0,g.jsx)(`ul`,{id:p,role:`listbox`,"aria-label":e,className:`absolute top-full left-0 z-50 mt-1 max-h-72 min-w-full overflow-y-auto rounded-md border border-input bg-popover text-popover-foreground shadow-md`,children:t.map((e,t)=>(0,g.jsx)(`li`,{id:b(t),role:`option`,"aria-selected":e.value===``?n===null:e.value===n,"data-highlighted":t===c,className:y,onMouseEnter:()=>l(t),onClick:()=>C(t),children:e.label},e.value===``?`__all__`:e.value))})]})}function x({value:e,onChange:t,tags:n,tagsStatus:r,tagTriggerRef:i,onArtistsLoaded:a}){let[s,c]=(0,h.useState)([]),[l,d]=(0,h.useState)(`loading`),f=(0,h.useRef)(a);f.current=a,(0,h.useEffect)(()=>{let e=!1;return o().then(t=>{e||(c(t),d(`loaded`),f.current?.(t))}).catch(()=>{e||(c([]),d(`error`))}),()=>{e=!0}},[]);let p=[{value:``,label:`All artists`},...s.map(e=>({value:String(e.artist_id),label:e.name}))],m;e.artistId!==null&&l===`loading`?m=`Loading artists…`:e.artistId!==null&&l===`error`&&(m=`Artist unavailable`);let v=[{value:``,label:`All event types`},..._],y;e.tagId!==null&&r===`loading`?y=`Loading tags…`:e.tagId!==null&&r===`error`&&(y=`Tag unavailable`);let x=[{value:``,label:`All tags`},...u(n).map(e=>({value:String(e.id),label:e.name}))];return(0,g.jsxs)(`div`,{className:`flex flex-wrap gap-4`,children:[(0,g.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Artist`,(0,g.jsx)(b,{label:`Artist`,options:p,value:e.artistId===null?null:String(e.artistId),triggerLabel:m,onSelect:n=>t({...e,artistId:n===null?null:Number(n)})})]}),(0,g.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Event type`,(0,g.jsx)(b,{label:`Event type`,options:v,value:e.eventType,onSelect:n=>t({...e,eventType:n})})]}),(0,g.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Tag`,(0,g.jsx)(b,{label:`Tag`,options:x,value:e.tagId===null?null:String(e.tagId),triggerLabel:y,triggerRef:i,onSelect:n=>t({...e,tagId:n===null?null:Number(n)})})]})]})}var S={new_release:{label:`New release`,emoji:`🆕`,color:`var(--color-event-new-release)`},guest_feature:{label:`Guest feature`,emoji:`🎤`,color:`var(--color-event-guest-feature)`},deluxe_change:{label:`Deluxe change`,emoji:`💿`,color:`var(--color-event-deluxe-change)`}},C={label:`Unknown`,emoji:`❔`,color:`var(--color-muted-foreground)`};function w({event:e}){let t=S[e.event_type]??C;return(0,g.jsxs)(`div`,{className:`flex gap-4 rounded-2xl bg-card p-4 ring-1 ring-foreground/10`,children:[(0,g.jsx)(l,{src:e.cover_art_url,alt:e.title,size:96,className:`rounded-md`}),(0,g.jsxs)(`div`,{className:`flex min-w-0 flex-1 flex-col gap-2`,children:[(0,g.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[(0,g.jsx)(`h3`,{className:`line-clamp-2 text-heading font-semibold text-foreground`,title:e.title,children:e.title}),(0,g.jsxs)(m,{variant:`secondary`,className:`shrink-0 gap-1 text-background`,style:{backgroundColor:t.color},children:[(0,g.jsx)(`span`,{"aria-hidden":`true`,children:t.emoji}),t.label]})]}),(0,g.jsx)(`p`,{className:`text-body text-muted-foreground`,children:e.artist_name}),(0,g.jsx)(T,{event:e})]})]})}function T({event:e}){switch(e.event_type){case`new_release`:return(0,g.jsx)(E,{event:e});case`guest_feature`:return(0,g.jsx)(k,{event:e});case`deluxe_change`:return(0,g.jsx)(A,{event:e});default:return null}}function E({event:e}){let t=e.release_date??`Release date unknown`;return(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[e.release_type?`${e.release_type} · `:``,t]})}function D(e){return e.source===`musicbrainz`?`https://musicbrainz.org/recording/${encodeURIComponent(e.external_id)}`:e.source===`deezer`?`https://www.deezer.com/track/${encodeURIComponent(e.external_id)}`:null}function O(e){let t=e.watched_artist_name;return!t||t===e.artist_name?null:t}function k({event:e}){let t=D(e),n=e.release_date??`Release date unknown`,r=O(e);return(0,g.jsxs)(g.Fragment,{children:[t?(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[`Featured on`,` `,(0,g.jsx)(`a`,{href:t,target:`_blank`,rel:`noreferrer`,className:`underline underline-offset-2 hover:text-foreground`,children:e.title}),r&&` — ${r} is on your watchlist`]}):(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[e.title,r&&` — ${r} is on your watchlist`]}),(0,g.jsx)(`p`,{className:`text-label text-muted-foreground`,children:n})]})}function A({event:e}){let t=e.release_date??`Release date unknown`,n=e.track_count??`?`;return e.previous_track_count==null?(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[t,` · `,n,` tracks`]}):(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[t,` · `,e.previous_track_count,` → `,n,` tracks`]})}var j=[`new_release`,`guest_feature`,`deluxe_change`],M=/^[1-9]\d*$/;function N(e){if(e===null||!M.test(e))return null;let t=Number(e);return Number.isSafeInteger(t)?t:null}function P(e){let t=e.get(`type`);return{artistId:N(e.get(`artist`)),eventType:j.find(e=>e===t)??null,tagId:N(e.get(`tag`))}}function F(e){let t=P(e),n=new URLSearchParams(e),r=[[`artist`,t.artistId===null?null:String(t.artistId)],[`type`,t.eventType],[`tag`,t.tagId===null?null:String(t.tagId)]];for(let[e,t]of r)t===null?n.delete(e):n.set(e,t);return n}function I(){return(0,g.jsxs)(`div`,{className:`flex gap-4 rounded-2xl bg-card p-4 ring-1 ring-foreground/10`,children:[(0,g.jsx)(f,{className:`h-24 w-24 shrink-0 rounded-md`}),(0,g.jsxs)(`div`,{className:`flex flex-1 flex-col gap-2 py-1`,children:[(0,g.jsx)(f,{className:`h-5 w-2/3`}),(0,g.jsx)(f,{className:`h-4 w-1/3`}),(0,g.jsx)(f,{className:`h-4 w-1/2`})]})]})}var L=3;function R(e,t,n){n===null?e.delete(t):e.set(t,String(n))}function z(e,t,n){return e?{heading:`Older than your retention window`,body:`There's release history for this view — it's just outside your retention window. Nothing was deleted.`}:t?{heading:`No events for “${t.name}”`,body:n?`Nothing matches “${t.name}” with the current artist and event-type filters.`:`None of the artists tagged “${t.name}” have release activity in your retention window.`}:n?{heading:`No matching events`,body:`Try a different artist or event type.`}:{heading:`No release activity yet`,body:`Add an artist to your watchlist to start tracking new releases, features, and deluxe editions.`}}function B(e,t){return c({artistId:e.artistId??void 0,eventType:e.eventType??void 0,tagId:e.tagId??void 0,cursor:t??void 0})}var V=n(function(){let{params:e,write:t}=d(),n=e.toString(),r=(0,h.useMemo)(()=>P(e),[n]),[o,c]=(0,h.useState)({status:`loading`,tags:[]}),[l,u]=(0,h.useState)([]),[f,m]=(0,h.useState)(null),[_,v]=(0,h.useState)(!1),[y,b]=(0,h.useState)(!0),[S,C]=(0,h.useState)(!1),[T,E]=(0,h.useState)(null),[D,O]=(0,h.useState)(null),[k,A]=(0,h.useState)(0),j=(0,h.useRef)(null),M=(0,h.useRef)(!1),N=(0,h.useRef)(!1),V=(0,h.useRef)(r);V.current=r,(0,h.useEffect)(()=>{let r=F(e);r.toString()!==n&&t(e=>{for(let t of[`artist`,`type`,`tag`]){let n=r.get(t);n===null?e.delete(t):e.set(t,n)}},{replace:!0})},[n,t]),(0,h.useEffect)(()=>{o.status!==`loaded`||M.current||(M.current=!0,r.tagId!==null&&!o.tags.some(e=>e.id===r.tagId)&&t(e=>e.delete(`tag`),{replace:!0}))},[o,r.tagId,t]);let H=e=>{if(N.current)return;N.current=!0;let n=V.current.artistId;n!==null&&!e.some(e=>e.artist_id===n)&&t(e=>e.delete(`artist`),{replace:!0})};(0,h.useEffect)(()=>{let e=!1;return a().then(t=>{e||c({status:`loaded`,tags:t})}).catch(()=>{e||c({status:`error`,tags:[]})}),()=>{e=!0}},[]),(0,h.useEffect)(()=>{let e=!1;return b(!0),E(null),O(null),u([]),m(null),v(!1),B(r,null).then(t=>{e||(u(t.events),m(t.next_cursor),v(t.has_older_events))}).catch(()=>{e||E(`Couldn't load release history.`)}).finally(()=>{e||b(!1)}),()=>{e=!0}},[r.artistId,r.eventType,r.tagId,k]);let U=()=>{f==null||S||(C(!0),O(null),B(r,f).then(e=>{u(t=>{let n=new Set(t.map(e=>e.id)),r=e.events.filter(e=>!n.has(e.id));return[...t,...r]}),m(e.next_cursor),v(e.has_older_events)}).catch(()=>{O(`Couldn't load release history.`)}).finally(()=>{C(!1)}))},W=()=>{A(e=>e+1)},G=r.artistId!=null||r.eventType!=null,K=r.tagId===null?null:{name:o.tags.find(e=>e.id===r.tagId)?.name??`this tag`},q=e=>{t(t=>{R(t,`artist`,e.artistId),R(t,`type`,e.eventType),R(t,`tag`,e.tagId)})},J=()=>{t(e=>e.delete(`tag`)),j.current?.focus()};return(0,g.jsxs)(`div`,{className:`flex flex-col gap-6 p-8`,children:[(0,g.jsx)(`h1`,{className:`text-display font-semibold text-foreground`,children:`History`}),(0,g.jsx)(x,{value:r,onChange:q,tags:o.tags,tagsStatus:o.status,tagTriggerRef:j,onArtistsLoaded:H}),T&&(0,g.jsx)(p,{heading:`Couldn't load release history.`,body:`Please try again.`,action:(0,g.jsx)(i,{variant:`secondary`,onClick:W,children:`Retry`})}),!T&&y&&(0,g.jsx)(`div`,{className:`flex flex-col gap-4`,children:Array.from({length:L}).map((e,t)=>(0,g.jsx)(I,{},t))}),!T&&!y&&l.length===0&&(()=>{let{heading:e,body:t}=z(_,K,G);return(0,g.jsx)(p,{heading:e,body:t,action:K&&!_?(0,g.jsx)(i,{variant:`secondary`,onClick:J,children:`Show all tags`}):void 0})})(),!T&&!y&&l.length>0&&(0,g.jsxs)(`div`,{className:`flex flex-col gap-4`,children:[l.map(e=>(0,g.jsx)(w,{event:e},e.id)),S&&Array.from({length:L}).map((e,t)=>(0,g.jsx)(I,{},`append-skeleton-${t}`)),D&&(0,g.jsx)(`p`,{className:`text-label text-destructive`,children:D}),f!=null&&(0,g.jsx)(i,{onClick:U,disabled:S,"aria-busy":S,className:`self-center`,children:S?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(s,{className:`size-4 animate-spin`,"aria-hidden":`true`}),`Loading…`]}):`Load more`})]})]})});export{V as default}; \ No newline at end of file +import{N as e,j as t,k as n,t as r}from"./jsx-runtime-D0QO-s14.js";import{C as i,d as a,f as o,ot as s,u as c}from"./api-k_NcBvGr.js";import{i as l,r as u,t as d}from"./useUrlParams-DI9KFxsS.js";import{n as f,r as p,t as m}from"./badge-B492YG3Q.js";var h=e(t(),1),g=r(),_=[{value:`new_release`,label:`New release`},{value:`guest_feature`,label:`Guest feature`},{value:`deluxe_change`,label:`Deluxe change`}],v=`flex h-9 max-w-64 min-w-40 items-center justify-between gap-2 rounded-md border border-input bg-input/30 px-3 text-body text-foreground outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50`,y=`cursor-pointer px-3 py-1.5 text-body text-popover-foreground data-[highlighted=true]:bg-accent`;function b({label:e,options:t,value:n,onSelect:r,triggerLabel:i,triggerRef:a}){let[o,s]=(0,h.useState)(!1),[c,l]=(0,h.useState)(0),u=(0,h.useRef)(null),d=(0,h.useRef)(null),f=a??d,p=(0,h.useId)(),m=t.findIndex(e=>e.value===``?n===null:e.value===n),_=t[m===-1?0:m],b=e=>`${p}-option-${e}`,x=i??_?.label;(0,h.useEffect)(()=>{o&&document.getElementById(b(c))?.scrollIntoView?.({block:`nearest`})},[o,c]),(0,h.useEffect)(()=>{if(!o)return;function e(e){u.current?.contains(e.target)||s(!1)}return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[o]);function S(e){l(e<0?0:e),s(!0)}function C(e){let n=t[e];n&&(r(n.value===``?null:n.value),s(!1),f.current?.focus())}function w(e){if(!o){(e.key===`ArrowDown`||e.key===`ArrowUp`||e.key===`Enter`||e.key===` `)&&(e.preventDefault(),S(m===-1?0:m));return}e.key===`ArrowDown`?(e.preventDefault(),l(e=>Math.min(e+1,t.length-1))):e.key===`ArrowUp`?(e.preventDefault(),l(e=>Math.max(e-1,0))):e.key===`Enter`||e.key===` `?(e.preventDefault(),C(c)):e.key===`Escape`&&(e.preventDefault(),s(!1),f.current?.focus())}return(0,g.jsxs)(`div`,{ref:u,className:`relative`,children:[(0,g.jsxs)(`button`,{ref:f,type:`button`,role:`combobox`,"aria-expanded":o,"aria-haspopup":`listbox`,"aria-controls":p,"aria-label":e,"aria-activedescendant":o?b(c):void 0,className:v,onClick:()=>o?s(!1):S(m===-1?0:m),onKeyDown:w,children:[(0,g.jsx)(`span`,{className:`truncate`,title:x,children:x}),(0,g.jsx)(`span`,{"aria-hidden":`true`,className:`text-muted-foreground`,children:`▾`})]}),o&&(0,g.jsx)(`ul`,{id:p,role:`listbox`,"aria-label":e,className:`absolute top-full left-0 z-50 mt-1 max-h-72 min-w-full overflow-y-auto rounded-md border border-input bg-popover text-popover-foreground shadow-md`,children:t.map((e,t)=>(0,g.jsx)(`li`,{id:b(t),role:`option`,"aria-selected":e.value===``?n===null:e.value===n,"data-highlighted":t===c,className:y,onMouseEnter:()=>l(t),onClick:()=>C(t),children:e.label},e.value===``?`__all__`:e.value))})]})}function x({value:e,onChange:t,tags:n,tagsStatus:r,tagTriggerRef:i,onArtistsLoaded:a}){let[s,c]=(0,h.useState)([]),[l,d]=(0,h.useState)(`loading`),f=(0,h.useRef)(a);f.current=a,(0,h.useEffect)(()=>{let e=!1;return o().then(t=>{e||(c(t),d(`loaded`),f.current?.(t))}).catch(()=>{e||(c([]),d(`error`))}),()=>{e=!0}},[]);let p=[{value:``,label:`All artists`},...s.map(e=>({value:String(e.artist_id),label:e.name}))],m;e.artistId!==null&&l===`loading`?m=`Loading artists…`:e.artistId!==null&&l===`error`&&(m=`Artist unavailable`);let v=[{value:``,label:`All event types`},..._],y;e.tagId!==null&&r===`loading`?y=`Loading tags…`:e.tagId!==null&&r===`error`&&(y=`Tag unavailable`);let x=[{value:``,label:`All tags`},...u(n).map(e=>({value:String(e.id),label:e.name}))];return(0,g.jsxs)(`div`,{className:`flex flex-wrap gap-4`,children:[(0,g.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Artist`,(0,g.jsx)(b,{label:`Artist`,options:p,value:e.artistId===null?null:String(e.artistId),triggerLabel:m,onSelect:n=>t({...e,artistId:n===null?null:Number(n)})})]}),(0,g.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Event type`,(0,g.jsx)(b,{label:`Event type`,options:v,value:e.eventType,onSelect:n=>t({...e,eventType:n})})]}),(0,g.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground`,children:[`Tag`,(0,g.jsx)(b,{label:`Tag`,options:x,value:e.tagId===null?null:String(e.tagId),triggerLabel:y,triggerRef:i,onSelect:n=>t({...e,tagId:n===null?null:Number(n)})})]})]})}var S={new_release:{label:`New release`,emoji:`🆕`,color:`var(--color-event-new-release)`},guest_feature:{label:`Guest feature`,emoji:`🎤`,color:`var(--color-event-guest-feature)`},deluxe_change:{label:`Deluxe change`,emoji:`💿`,color:`var(--color-event-deluxe-change)`}},C={label:`Unknown`,emoji:`❔`,color:`var(--color-muted-foreground)`};function w({event:e}){let t=S[e.event_type]??C;return(0,g.jsxs)(`div`,{className:`flex gap-4 rounded-2xl bg-card p-4 ring-1 ring-foreground/10`,children:[(0,g.jsx)(l,{src:e.cover_art_url,alt:e.title,size:96,className:`rounded-md`}),(0,g.jsxs)(`div`,{className:`flex min-w-0 flex-1 flex-col gap-2`,children:[(0,g.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[(0,g.jsx)(`h3`,{className:`line-clamp-2 text-heading font-semibold text-foreground`,title:e.title,children:e.title}),(0,g.jsxs)(m,{variant:`secondary`,className:`shrink-0 gap-1 text-background`,style:{backgroundColor:t.color},children:[(0,g.jsx)(`span`,{"aria-hidden":`true`,children:t.emoji}),t.label]})]}),(0,g.jsx)(`p`,{className:`text-body text-muted-foreground`,children:e.artist_name}),(0,g.jsx)(T,{event:e})]})]})}function T({event:e}){switch(e.event_type){case`new_release`:return(0,g.jsx)(E,{event:e});case`guest_feature`:return(0,g.jsx)(k,{event:e});case`deluxe_change`:return(0,g.jsx)(A,{event:e});default:return null}}function E({event:e}){let t=e.release_date??`Release date unknown`;return(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[e.release_type?`${e.release_type} · `:``,t]})}function D(e){return e.source===`musicbrainz`?`https://musicbrainz.org/recording/${encodeURIComponent(e.external_id)}`:e.source===`deezer`?`https://www.deezer.com/track/${encodeURIComponent(e.external_id)}`:null}function O(e){let t=e.watched_artist_name;return!t||t===e.artist_name?null:t}function k({event:e}){let t=D(e),n=e.release_date??`Release date unknown`,r=O(e);return(0,g.jsxs)(g.Fragment,{children:[t?(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[`Featured on`,` `,(0,g.jsx)(`a`,{href:t,target:`_blank`,rel:`noreferrer`,className:`underline underline-offset-2 hover:text-foreground`,children:e.title}),r&&` — ${r} is on your watchlist`]}):(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[e.title,r&&` — ${r} is on your watchlist`]}),(0,g.jsx)(`p`,{className:`text-label text-muted-foreground`,children:n})]})}function A({event:e}){let t=e.release_date??`Release date unknown`,n=e.track_count??`?`;return e.previous_track_count==null?(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[t,` · `,n,` tracks`]}):(0,g.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[t,` · `,e.previous_track_count,` → `,n,` tracks`]})}var j=[`new_release`,`guest_feature`,`deluxe_change`],M=/^[1-9]\d*$/;function N(e){if(e===null||!M.test(e))return null;let t=Number(e);return Number.isSafeInteger(t)?t:null}function P(e){let t=e.get(`type`);return{artistId:N(e.get(`artist`)),eventType:j.find(e=>e===t)??null,tagId:N(e.get(`tag`))}}function F(e){let t=P(e),n=new URLSearchParams(e),r=[[`artist`,t.artistId===null?null:String(t.artistId)],[`type`,t.eventType],[`tag`,t.tagId===null?null:String(t.tagId)]];for(let[e,t]of r)t===null?n.delete(e):n.set(e,t);return n}function I(){return(0,g.jsxs)(`div`,{className:`flex gap-4 rounded-2xl bg-card p-4 ring-1 ring-foreground/10`,children:[(0,g.jsx)(f,{className:`h-24 w-24 shrink-0 rounded-md`}),(0,g.jsxs)(`div`,{className:`flex flex-1 flex-col gap-2 py-1`,children:[(0,g.jsx)(f,{className:`h-5 w-2/3`}),(0,g.jsx)(f,{className:`h-4 w-1/3`}),(0,g.jsx)(f,{className:`h-4 w-1/2`})]})]})}var L=3;function R(e,t,n){n===null?e.delete(t):e.set(t,String(n))}function z(e,t,n){return e?{heading:`Older than your retention window`,body:`There's release history for this view — it's just outside your retention window. Nothing was deleted.`}:t?{heading:`No events for “${t.name}”`,body:n?`Nothing matches “${t.name}” with the current artist and event-type filters.`:`None of the artists tagged “${t.name}” have release activity in your retention window.`}:n?{heading:`No matching events`,body:`Try a different artist or event type.`}:{heading:`No release activity yet`,body:`Add an artist to your watchlist to start tracking new releases, features, and deluxe editions.`}}function B(e,t){return c({artistId:e.artistId??void 0,eventType:e.eventType??void 0,tagId:e.tagId??void 0,cursor:t??void 0})}var V=n(function(){let{params:e,write:t}=d(),n=e.toString(),r=(0,h.useMemo)(()=>P(e),[n]),[o,c]=(0,h.useState)({status:`loading`,tags:[]}),[l,u]=(0,h.useState)([]),[f,m]=(0,h.useState)(null),[_,v]=(0,h.useState)(!1),[y,b]=(0,h.useState)(!0),[S,C]=(0,h.useState)(!1),[T,E]=(0,h.useState)(null),[D,O]=(0,h.useState)(null),[k,A]=(0,h.useState)(0),j=(0,h.useRef)(null),M=(0,h.useRef)(!1),N=(0,h.useRef)(!1),V=(0,h.useRef)(r);V.current=r;let H=(0,h.useRef)(``);H.current=`${r.artistId}|${r.eventType}|${r.tagId}|${k}`,(0,h.useEffect)(()=>{let r=F(e);r.toString()!==n&&t(e=>{for(let t of[`artist`,`type`,`tag`]){let n=r.get(t);n===null?e.delete(t):e.set(t,n)}},{replace:!0})},[n,t]),(0,h.useEffect)(()=>{o.status!==`loaded`||M.current||(M.current=!0,r.tagId!==null&&!o.tags.some(e=>e.id===r.tagId)&&t(e=>e.delete(`tag`),{replace:!0}))},[o,r.tagId,t]);let U=e=>{if(N.current)return;N.current=!0;let n=V.current.artistId;n!==null&&!e.some(e=>e.artist_id===n)&&t(e=>e.delete(`artist`),{replace:!0})};(0,h.useEffect)(()=>{let e=!1;return a().then(t=>{e||c({status:`loaded`,tags:t})}).catch(()=>{e||c({status:`error`,tags:[]})}),()=>{e=!0}},[]),(0,h.useEffect)(()=>{let e=!1;return b(!0),E(null),O(null),C(!1),u([]),m(null),v(!1),B(r,null).then(t=>{e||(u(t.events),m(t.next_cursor),v(t.has_older_events))}).catch(()=>{e||E(`Couldn't load release history.`)}).finally(()=>{e||b(!1)}),()=>{e=!0}},[r.artistId,r.eventType,r.tagId,k]);let W=()=>{if(f==null||S)return;let e=H.current;C(!0),O(null),B(r,f).then(t=>{H.current===e&&(u(e=>{let n=new Set(e.map(e=>e.id)),r=t.events.filter(e=>!n.has(e.id));return[...e,...r]}),m(t.next_cursor),v(t.has_older_events))}).catch(()=>{H.current===e&&O(`Couldn't load release history.`)}).finally(()=>{H.current===e&&C(!1)})},G=()=>{A(e=>e+1)},K=r.artistId!=null||r.eventType!=null,q=r.tagId===null?null:{name:o.tags.find(e=>e.id===r.tagId)?.name??`this tag`},J=e=>{t(t=>{R(t,`artist`,e.artistId),R(t,`type`,e.eventType),R(t,`tag`,e.tagId)})},Y=()=>{t(e=>e.delete(`tag`)),j.current?.focus()};return(0,g.jsxs)(`div`,{className:`flex flex-col gap-6 p-8`,children:[(0,g.jsx)(`h1`,{className:`text-display font-semibold text-foreground`,children:`History`}),(0,g.jsx)(x,{value:r,onChange:J,tags:o.tags,tagsStatus:o.status,tagTriggerRef:j,onArtistsLoaded:U}),T&&(0,g.jsx)(p,{heading:`Couldn't load release history.`,body:`Please try again.`,action:(0,g.jsx)(i,{variant:`secondary`,onClick:G,children:`Retry`})}),!T&&y&&(0,g.jsx)(`div`,{className:`flex flex-col gap-4`,children:Array.from({length:L}).map((e,t)=>(0,g.jsx)(I,{},t))}),!T&&!y&&l.length===0&&(()=>{let{heading:e,body:t}=z(_,q,K);return(0,g.jsx)(p,{heading:e,body:t,action:q&&!_?(0,g.jsx)(i,{variant:`secondary`,onClick:Y,children:`Show all tags`}):void 0})})(),!T&&!y&&l.length>0&&(0,g.jsxs)(`div`,{className:`flex flex-col gap-4`,children:[l.map(e=>(0,g.jsx)(w,{event:e},e.id)),S&&Array.from({length:L}).map((e,t)=>(0,g.jsx)(I,{},`append-skeleton-${t}`)),D&&(0,g.jsx)(`p`,{className:`text-label text-destructive`,children:D}),f!=null&&(0,g.jsx)(i,{onClick:W,disabled:S,"aria-busy":S,className:`self-center`,children:S?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(s,{className:`size-4 animate-spin`,"aria-hidden":`true`}),`Loading…`]}):`Load more`})]})]})});export{V as default}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/manifest-645baaf8.js b/internal/webassets/build/client/assets/manifest-5df375ec.js similarity index 92% rename from internal/webassets/build/client/assets/manifest-645baaf8.js rename to internal/webassets/build/client/assets/manifest-5df375ec.js index 7cbf7b6..5d85291 100644 --- a/internal/webassets/build/client/assets/manifest-645baaf8.js +++ b/internal/webassets/build/client/assets/manifest-5df375ec.js @@ -1 +1 @@ -window.__reactRouterManifest={"entry":{"module":"/assets/entry.client-oFNB33ee.js","imports":["/assets/jsx-runtime-D0QO-s14.js","/assets/react-dom-B1AcAK82.js"],"css":[]},"routes":{"root":{"id":"root","path":"","hasAction":false,"hasLoader":false,"hasClientAction":false,"hasClientLoader":false,"hasClientMiddleware":false,"hasDefaultExport":true,"hasErrorBoundary":true,"module":"/assets/root-BiKnkZEa.js","imports":["/assets/jsx-runtime-D0QO-s14.js","/assets/react-dom-B1AcAK82.js","/assets/api-k_NcBvGr.js","/assets/input-IDkwig6l.js","/assets/triangle-alert-Br4OJW5s.js","/assets/createBaseUIEventDetails-wEMuC0RZ.js"],"css":["/assets/root-CdD2OkTO.css"]},"watchlist-index":{"id":"watchlist-index","parentId":"root","index":true,"hasAction":false,"hasLoader":false,"hasClientAction":false,"hasClientLoader":false,"hasClientMiddleware":false,"hasDefaultExport":true,"hasErrorBoundary":false,"module":"/assets/watchlist-BuIICAgv.js","imports":["/assets/jsx-runtime-D0QO-s14.js","/assets/useUrlParams-DI9KFxsS.js","/assets/badge-B492YG3Q.js","/assets/api-k_NcBvGr.js","/assets/input-IDkwig6l.js","/assets/select-BBue9CB_.js","/assets/createBaseUIEventDetails-wEMuC0RZ.js","/assets/react-dom-B1AcAK82.js"],"css":[]},"history-path":{"id":"history-path","parentId":"root","path":"history","hasAction":false,"hasLoader":false,"hasClientAction":false,"hasClientLoader":false,"hasClientMiddleware":false,"hasDefaultExport":true,"hasErrorBoundary":false,"module":"/assets/history-CiwTCwWj.js","imports":["/assets/jsx-runtime-D0QO-s14.js","/assets/useUrlParams-DI9KFxsS.js","/assets/badge-B492YG3Q.js","/assets/api-k_NcBvGr.js"],"css":[]},"system-path":{"id":"system-path","parentId":"root","path":"system","hasAction":false,"hasLoader":false,"hasClientAction":false,"hasClientLoader":false,"hasClientMiddleware":false,"hasDefaultExport":true,"hasErrorBoundary":false,"module":"/assets/system-DHkZsAhb.js","imports":["/assets/jsx-runtime-D0QO-s14.js","/assets/badge-B492YG3Q.js","/assets/api-k_NcBvGr.js","/assets/select-BBue9CB_.js","/assets/createBaseUIEventDetails-wEMuC0RZ.js","/assets/triangle-alert-Br4OJW5s.js","/assets/react-dom-B1AcAK82.js"],"css":[]}},"url":"/assets/manifest-645baaf8.js","version":"645baaf8"}; \ No newline at end of file +window.__reactRouterManifest={"entry":{"module":"/assets/entry.client-oFNB33ee.js","imports":["/assets/jsx-runtime-D0QO-s14.js","/assets/react-dom-B1AcAK82.js"],"css":[]},"routes":{"root":{"id":"root","path":"","hasAction":false,"hasLoader":false,"hasClientAction":false,"hasClientLoader":false,"hasClientMiddleware":false,"hasDefaultExport":true,"hasErrorBoundary":true,"module":"/assets/root-BiKnkZEa.js","imports":["/assets/jsx-runtime-D0QO-s14.js","/assets/react-dom-B1AcAK82.js","/assets/api-k_NcBvGr.js","/assets/input-IDkwig6l.js","/assets/triangle-alert-Br4OJW5s.js","/assets/createBaseUIEventDetails-wEMuC0RZ.js"],"css":["/assets/root-CdD2OkTO.css"]},"watchlist-index":{"id":"watchlist-index","parentId":"root","index":true,"hasAction":false,"hasLoader":false,"hasClientAction":false,"hasClientLoader":false,"hasClientMiddleware":false,"hasDefaultExport":true,"hasErrorBoundary":false,"module":"/assets/watchlist-DVr_lpXm.js","imports":["/assets/jsx-runtime-D0QO-s14.js","/assets/useUrlParams-DI9KFxsS.js","/assets/badge-B492YG3Q.js","/assets/api-k_NcBvGr.js","/assets/input-IDkwig6l.js","/assets/select-BBue9CB_.js","/assets/createBaseUIEventDetails-wEMuC0RZ.js","/assets/react-dom-B1AcAK82.js"],"css":[]},"history-path":{"id":"history-path","parentId":"root","path":"history","hasAction":false,"hasLoader":false,"hasClientAction":false,"hasClientLoader":false,"hasClientMiddleware":false,"hasDefaultExport":true,"hasErrorBoundary":false,"module":"/assets/history-CxaPqYlO.js","imports":["/assets/jsx-runtime-D0QO-s14.js","/assets/useUrlParams-DI9KFxsS.js","/assets/badge-B492YG3Q.js","/assets/api-k_NcBvGr.js"],"css":[]},"system-path":{"id":"system-path","parentId":"root","path":"system","hasAction":false,"hasLoader":false,"hasClientAction":false,"hasClientLoader":false,"hasClientMiddleware":false,"hasDefaultExport":true,"hasErrorBoundary":false,"module":"/assets/system-DHkZsAhb.js","imports":["/assets/jsx-runtime-D0QO-s14.js","/assets/badge-B492YG3Q.js","/assets/api-k_NcBvGr.js","/assets/select-BBue9CB_.js","/assets/createBaseUIEventDetails-wEMuC0RZ.js","/assets/triangle-alert-Br4OJW5s.js","/assets/react-dom-B1AcAK82.js"],"css":[]}},"url":"/assets/manifest-5df375ec.js","version":"5df375ec"}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/watchlist-BuIICAgv.js b/internal/webassets/build/client/assets/watchlist-BuIICAgv.js deleted file mode 100644 index 6f87ff6..0000000 --- a/internal/webassets/build/client/assets/watchlist-BuIICAgv.js +++ /dev/null @@ -1 +0,0 @@ -import{N as e,j as t,k as n,t as r}from"./jsx-runtime-D0QO-s14.js";import{t as i}from"./react-dom-B1AcAK82.js";import{C as a,D as o,E as s,F as c,I as l,L as u,M as d,N as f,O as p,P as m,Q as h,R as g,T as _,V as v,Y as y,Z as b,d as x,f as S,g as C,h as w,j as T,k as E,m as D,n as O,o as k,ot as A,p as j,q as ee,r as M,s as te,st as N,t as P,v as F,w as I,y as L,z as ne}from"./api-k_NcBvGr.js";import{$ as re,At as ie,B as ae,C as oe,Ct as R,Dt as se,E as ce,Et as le,F as z,G as B,H as ue,I as V,J as de,K as H,Mt as fe,N as pe,Ot as me,Q as he,R as ge,S as _e,St as ve,T as ye,Tt as be,U as xe,V as Se,W as Ce,X as we,Y as Te,Z as Ee,_ as U,_t as De,a as Oe,at as ke,b as Ae,bt as je,c as Me,ct as Ne,d as W,dt as Pe,et as Fe,f as Ie,ft as Le,g as Re,gt as ze,h as Be,ht as Ve,i as He,it as Ue,jt as We,kt as Ge,l as Ke,lt as G,m as qe,mt as Je,n as Ye,nt as Xe,o as Ze,ot as Qe,p as $e,pt as et,q as tt,r as nt,rt,s as it,st as at,t as ot,tt as K,u as st,ut as ct,v as lt,vt as ut,w as dt,wt as ft,x as pt,xt as mt,y as ht,yt as gt,z as _t}from"./select-BBue9CB_.js";import{i as vt,n as yt,r as q,t as bt}from"./useUrlParams-DI9KFxsS.js";import{r as J,t as xt}from"./input-IDkwig6l.js";import{C as St,D as Ct,E as wt,F as Tt,I as Et,M as Dt,N as Ot,O as kt,P as At,S as jt,T as Mt,a as Nt,d as Pt,f as Ft,g as It,h as Lt,i as Y,j as Rt,k as zt,l as Bt,m as Vt,n as Ht,o as Ut,p as Wt,r as Gt,s as Kt,t as X,u as qt,v as Jt}from"./createBaseUIEventDetails-wEMuC0RZ.js";import{n as Yt,r as Xt,t as Zt}from"./badge-B492YG3Q.js";var Qt=N(`funnel-x`,[[`path`,{d:`M12.531 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14v6a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341l.427-.473`,key:`ol2ft2`}],[`path`,{d:`m16.5 3.5 5 5`,key:`15e6fa`}],[`path`,{d:`m21.5 3.5-5 5`,key:`m0lwru`}]]),$t=N(`list-filter`,[[`path`,{d:`M2 5h20`,key:`1fs1ex`}],[`path`,{d:`M6 12h12`,key:`8npq4p`}],[`path`,{d:`M9 19h6`,key:`456am0`}]]),en=N(`notebook-pen`,[[`path`,{d:`M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4`,key:`re6nr2`}],[`path`,{d:`M2 6h4`,key:`aawbzj`}],[`path`,{d:`M2 10h4`,key:`l0bgd4`}],[`path`,{d:`M2 14h4`,key:`1gsvsf`}],[`path`,{d:`M2 18h4`,key:`1bu2t1`}],[`path`,{d:`M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z`,key:`pqwjuv`}]]),tn=N(`pencil`,[[`path`,{d:`M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z`,key:`1a8usu`}],[`path`,{d:`m15 5 4 4`,key:`1mk7zo`}]]),nn=N(`plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),rn=N(`tags`,[[`path`,{d:`M13.172 2a2 2 0 0 1 1.414.586l6.71 6.71a2.4 2.4 0 0 1 0 3.408l-4.592 4.592a2.4 2.4 0 0 1-3.408 0l-6.71-6.71A2 2 0 0 1 6 9.172V3a1 1 0 0 1 1-1z`,key:`16rjxf`}],[`path`,{d:`M2 7v6.172a2 2 0 0 0 .586 1.414l6.71 6.71a2.4 2.4 0 0 0 3.191.193`,key:`178nd4`}],[`circle`,{cx:`10.5`,cy:`6.5`,r:`.5`,fill:`currentColor`,key:`12ikhr`}]]),an=N(`x`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),Z=e(t(),1),on=Z.createContext({disabled:!1});function sn(){return Z.useContext(on)}function cn(){return new Qe({open:!1,transitionStatus:void 0,floatingElement:null,referenceElement:null,triggerElements:new we,floatingId:void 0,syncOnly:!1,nested:!1,onOpenChange:void 0})}function ln(){return{open:!1,openProp:void 0,mounted:!1,transitionStatus:void 0,floatingRootContext:cn(),floatingId:void 0,triggerCount:0,preventUnmountingOnClose:!1,payload:void 0,activeTriggerId:null,activeTriggerElement:null,triggerIdProp:void 0,popupElement:null,positionerElement:null,activeTriggerProps:p,inactiveTriggerProps:p,popupProps:p}}function un(e,t,n=!1){return new Qe({open:!1,transitionStatus:void 0,floatingElement:null,referenceElement:null,triggerElements:e,floatingId:t,syncOnly:!0,nested:n,onOpenChange:void 0})}var dn=e=>e.triggerIdProp??e.activeTriggerId,fn=e=>e.openProp??e.open,pn=e=>(e.popupElement?.id??e.floatingId)||void 0;function mn(e,t){return t!==void 0&&fn(e)&&dn(e)===t}function hn(e,t){return mn(e,t)?!0:t!==void 0&&fn(e)&&dn(e)==null&&e.triggerCount===1}var gn={open:fn,mounted:e=>e.mounted,transitionStatus:e=>e.transitionStatus,floatingRootContext:e=>e.floatingRootContext,triggerCount:e=>e.triggerCount,preventUnmountingOnClose:e=>e.preventUnmountingOnClose,payload:e=>e.payload,activeTriggerId:dn,activeTriggerElement:e=>e.mounted?e.activeTriggerElement:null,popupId:pn,isTriggerActive:(e,t)=>t!==void 0&&dn(e)===t,isOpenedByTrigger:(e,t)=>mn(e,t),isMountedByTrigger:(e,t)=>t!==void 0&&dn(e)===t&&e.mounted,triggerProps:(e,t)=>t?e.activeTriggerProps:e.inactiveTriggerProps,triggerPopupId:(e,t)=>hn(e,t)?pn(e):void 0,popupProps:e=>e.popupProps,popupElement:e=>e.popupElement,positionerElement:e=>e.positionerElement};function _n({store:e,parentContext:t,isDrawer:n}){let r=e.useState(`open`),i=e.useState(`disablePointerDismissal`),a=e.useState(`modal`),o=e.useState(`popupElement`),s=e.useState(`floatingRootContext`),[c,l]=Z.useState(0),[d,f]=Z.useState(0),p=c===0,m=ct(s,{outsidePressEvent(){return e.context.internalBackdropRef.current||e.context.backdropRef.current?`intentional`:{mouse:a===`trap-focus`?`sloppy`:`intentional`,touch:`sloppy`}},outsidePress(t){if(!e.context.outsidePressEnabledRef.current||`button`in t&&t.button!==0)return!1;if(`touches`in t){if(t.type===`touchend`){if(t.changedTouches.length!==1||t.touches.length!==0)return!1}else if(t.touches.length!==1)return!1}let n=wt(t);if(p&&!i){if(a){let t=e.context.internalBackdropRef.current,r=e.context.backdropRef.current;return t||r?t===n||r===n||Mt(n,o)&&!n?.hasAttribute(`data-base-ui-portal`):!0}return!0}return!1},escapeKey:p});return ut(r&&a===!0,o),e.useContextCallback(`onNestedDialogOpen`,(e,t)=>{l(e),f(t)}),u(()=>(t?.onNestedDialogOpen&&(r?t.onNestedDialogOpen(c+1,d+ +!!n):t.onNestedDialogOpen(0,0)),()=>{t?.onNestedDialogOpen&&r&&t.onNestedDialogOpen(0,0)}),[n,r,c,d,t]),rt(e,{activeTriggerProps:m.reference,inactiveTriggerProps:m.trigger,popupProps:m.floating,nestedOpenDialogCount:c,nestedOpenDrawerCount:d}),null}var vn=Z.createContext(void 0);function yn(e){let t=Z.useContext(vn);if(!e&&t===void 0)throw Error(c(27));return t}var bn={...gn,modal:e=>e.modal,nested:e=>e.nested,nestedOpenDialogCount:e=>e.nestedOpenDialogCount,nestedOpenDrawerCount:e=>e.nestedOpenDrawerCount,disablePointerDismissal:e=>e.disablePointerDismissal,openMethod:e=>e.openMethod,descriptionElementId:e=>e.descriptionElementId,titleElementId:e=>e.titleElementId,viewportElement:e=>e.viewportElement,role:e=>e.role},xn=class extends at{constructor(e,t,n){let r=new we,i=Sn(e,r,t,n);super(i,Cn(r),bn)}setOpen=(e,t)=>{if(t.preventUnmountOnClose=()=>{this.set(`preventUnmountingOnClose`,!0)},!e&&t.trigger==null&&this.state.activeTriggerId!=null&&(t.trigger=this.state.activeTriggerElement??void 0),this.context.onOpenChange?.(e,t),t.isCanceled)return;this.state.floatingRootContext.dispatchOpenChange(e,t);let n={open:e};Fe(n,e,t.trigger),this.update(n)}};function Sn(e,t,n,r=!1){let i={...ln(),modal:!0,disablePointerDismissal:!1,viewportElement:null,descriptionElementId:void 0,titleElementId:void 0,openMethod:null,nested:!1,nestedOpenDialogCount:0,nestedOpenDrawerCount:0,role:`dialog`,...e};return i.floatingRootContext=un(t,n,r),i}function Cn(e){return{popupRef:Z.createRef(),backdropRef:Z.createRef(),internalBackdropRef:Z.createRef(),outsidePressEnabledRef:{current:!0},triggerElements:e,onOpenChange:void 0,onOpenChangeComplete:void 0}}var Q=r();function wn(e,t){let{children:n,open:r,defaultOpen:i=!1,onOpenChange:a,onOpenChangeComplete:o,disablePointerDismissal:s=!1,modal:c=!0,actionsRef:l,handle:u,triggerId:d,defaultTriggerId:f=null}=t,p=e===`drawer`,m=e===`alert-dialog`,h=m?!0:c,g=m||s,_=m?`alertdialog`:`dialog`,v=yn(!0),y={modal:h,disablePointerDismissal:g,nested:v!=null,role:_},b=Ue((e,t)=>new xn({open:i,openProp:r,activeTriggerId:f,triggerIdProp:d,...y},e,t),!0);b.useControlledProp(`openProp`,r),b.useControlledProp(`triggerIdProp`,d),b.useSyncedValues(y),b.useContextCallback(`onOpenChange`,a),b.useContextCallback(`onOpenChangeComplete`,o);let x=b.useState(`open`),S=b.useState(`mounted`),C=b.useState(`payload`);ke(b,x),K(b);let{forceUnmount:w}=Xe(x,b);Z.useImperativeHandle(l,()=>({unmount:w,close:()=>b.setOpen(!1,X(Bt))}),[w,b]);let T=x||S;return(0,Q.jsxs)(vn.Provider,{value:b,children:[u&&(0,Q.jsx)(he,{handle:u,store:b}),T&&(0,Q.jsx)(_n,{store:b,parentContext:v?.context,isDrawer:p}),typeof n==`function`?n({payload:C}):n]})}function Tn(e){return wn(`alert-dialog`,e)}var En=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,forceRender:a=!1,...o}=e,c=yn(),l=c.useState(`open`),u=c.useState(`nested`),d=c.useState(`mounted`);return s(`div`,e,{state:{open:l,transitionStatus:c.useState(`transitionStatus`)},ref:[c.context.backdropRef,t],stateAttributesMapping:Ce,props:[{role:`presentation`,hidden:!d,style:{userSelect:`none`,WebkitUserSelect:`none`}},o],enabled:a||!u})}),Dn=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,disabled:a=!1,nativeButton:o=!0,...c}=e,l=yn(),u=l.useState(`open`),{getButtonProps:f,buttonRef:p}=d({disabled:a,native:o}),m={disabled:a};function h(e){u&&l.setOpen(!1,X(Ut,e.nativeEvent))}return s(`button`,e,{state:m,ref:[t,p],props:[{onClick:h},c,f]})}),On=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,id:a,...o}=e,c=yn(),l=zt(a);return c.useSyncedValueWithCleanup(`descriptionElementId`,l),s(`p`,e,{ref:t,props:[{id:l},o]})}),kn=Z.createContext(void 0);function An(){let e=Z.useContext(kn);if(e===void 0)throw Error(c(26));return e}var jn={...xe,...je,nestedDialogOpen(e){return e?{"data-nested-dialog-open":``}:null}},Mn=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,finalFocus:a,initialFocus:o,...c}=e,l=yn(),u=l.useState(`descriptionElementId`),d=l.useState(`disablePointerDismissal`),f=l.useState(`floatingRootContext`),p=l.useState(`popupProps`),m=l.useState(`modal`),h=l.useState(`mounted`),g=l.useState(`nested`),_=l.useState(`nestedOpenDialogCount`),v=l.useState(`open`),y=l.useState(`openMethod`),b=l.useState(`titleElementId`),x=l.useState(`transitionStatus`),S=l.useState(`role`),C=f.useState(`floatingId`);An(),mt({open:v,ref:l.context.popupRef,onComplete(){v&&l.context.onOpenChangeComplete?.(!0)}});let w=o===void 0?re(l.context.popupRef):o,T=_>0,E=l.useStateSetter(`popupElement`),D=s(`div`,e,{state:{open:v,nested:g,transitionStatus:x,nestedDialogOpen:T},props:[p,{id:C,"aria-labelledby":b,"aria-describedby":u,role:S,...Ee,hidden:!h,onKeyDown(e){ue.has(e.key)&&e.stopPropagation()},style:{"--nested-dialogs":_}},c],ref:[t,l.context.popupRef,E],stateAttributesMapping:jn});return(0,Q.jsx)(Le,{context:f,openInteractionType:y,disabled:!h,closeOnFocusOut:!d,initialFocus:w,returnFocus:a,modal:m!==!1,restoreFocus:`popup`,children:D})}),Nn=Z.forwardRef(function(e,t){let{keepMounted:n=!1,...r}=e,i=yn(),a=i.useState(`mounted`),o=i.useState(`modal`),s=i.useState(`open`);return a||n?(0,Q.jsx)(kn.Provider,{value:n,children:(0,Q.jsxs)(et,{ref:t,...r,children:[a&&o===!0&&(0,Q.jsx)(ae,{ref:i.context.internalBackdropRef,inert:Se(!s)}),e.children]})}):null}),Pn=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,id:a,...o}=e,c=yn(),l=zt(a);return c.useSyncedValueWithCleanup(`titleElementId`,l),s(`h2`,e,{ref:t,props:[{id:l},o]})});function Fn({...e}){return(0,Q.jsx)(Tn,{"data-slot":`alert-dialog`,...e})}function In({...e}){return(0,Q.jsx)(Nn,{"data-slot":`alert-dialog-portal`,...e})}function Ln({className:e,...t}){return(0,Q.jsx)(En,{"data-slot":`alert-dialog-overlay`,className:I(`fixed inset-0 isolate z-50 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,e),...t})}function Rn({className:e,size:t=`default`,...n}){return(0,Q.jsxs)(In,{children:[(0,Q.jsx)(Ln,{}),(0,Q.jsx)(Mn,{"data-slot":`alert-dialog-content`,"data-size":t,className:I(`group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-6 rounded-4xl bg-popover p-6 text-popover-foreground ring-1 ring-foreground/5 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...n})]})}function zn({className:e,...t}){return(0,Q.jsx)(`div`,{"data-slot":`alert-dialog-header`,className:I(`grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]`,e),...t})}function Bn({className:e,...t}){return(0,Q.jsx)(`div`,{"data-slot":`alert-dialog-footer`,className:I(`flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end`,e),...t})}function Vn({className:e,...t}){return(0,Q.jsx)(Pn,{"data-slot":`alert-dialog-title`,className:I(`text-heading font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2`,e),...t})}function Hn({className:e,...t}){return(0,Q.jsx)(On,{"data-slot":`alert-dialog-description`,className:I(`text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground`,e),...t})}function Un({className:e,...t}){return(0,Q.jsx)(a,{"data-slot":`alert-dialog-action`,className:I(e),...t})}function Wn({className:e,variant:t=`outline`,size:n=`default`,...r}){return(0,Q.jsx)(Dn,{"data-slot":`alert-dialog-cancel`,className:I(e),render:(0,Q.jsx)(a,{variant:t,size:n}),...r})}function Gn({open:e,onOpenChange:t,title:n,description:r,actionLabel:i,pendingLabel:a,variant:o,onConfirm:s,finalFocus:c}){let[l,u]=(0,Z.useState)(!1);async function d(){u(!0);try{await s()}catch{}finally{u(!1),t(!1)}}return(0,Q.jsx)(Fn,{open:e,onOpenChange:e=>{l||t(e)},children:(0,Q.jsxs)(Rn,{size:`default`,finalFocus:c,children:[(0,Q.jsxs)(zn,{children:[(0,Q.jsx)(Vn,{className:`wrap-anywhere`,children:n}),(0,Q.jsx)(Hn,{className:`wrap-anywhere`,children:r})]}),(0,Q.jsxs)(Bn,{children:[(0,Q.jsx)(Wn,{disabled:l,children:`Cancel`}),(0,Q.jsx)(Un,{variant:o,disabled:l,className:`min-w-28`,onClick:d,children:l?a:i})]})]})})}function Kn(e){return wn(`dialog`,e)}function qn({...e}){return(0,Q.jsx)(Kn,{"data-slot":`dialog`,...e})}function Jn({...e}){return(0,Q.jsx)(Nn,{"data-slot":`dialog-portal`,...e})}function Yn({className:e,...t}){return(0,Q.jsx)(En,{"data-slot":`dialog-overlay`,className:I(`fixed inset-0 isolate z-50 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,e),...t})}function Xn({className:e,children:t,showCloseButton:n=!0,...r}){return(0,Q.jsxs)(Jn,{children:[(0,Q.jsx)(Yn,{}),(0,Q.jsxs)(Mn,{"data-slot":`dialog-content`,className:I(`fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-4xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/5 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...r,children:[t,n&&(0,Q.jsxs)(Dn,{"data-slot":`dialog-close`,render:(0,Q.jsx)(a,{variant:`ghost`,className:`absolute top-4 right-4`,size:`icon-sm`}),children:[(0,Q.jsx)(an,{}),(0,Q.jsx)(`span`,{className:`sr-only`,children:`Close`})]})]})]})}function Zn({className:e,...t}){return(0,Q.jsx)(`div`,{"data-slot":`dialog-header`,className:I(`flex flex-col gap-2`,e),...t})}function Qn({className:e,...t}){return(0,Q.jsx)(Pn,{"data-slot":`dialog-title`,className:I(`text-heading font-semibold`,e),...t})}function $n({className:e,...t}){return(0,Q.jsx)(On,{"data-slot":`dialog-description`,className:I(`text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground`,e),...t})}function er(e){return Array.from(e).length}function tr(e){return er(e.normalize(`NFC`).trim().replace(/\s+/g,` `))}function nr(e){return er(e.trim())}function rr(e){return[...e].sort((e,t)=>{let n=e.name.localeCompare(t.name,void 0,{sensitivity:`base`});return n===0?e.id-t.id:n})}function ir(e,t){switch(t.kind){case`renamed`:return e.map(e=>e.id===t.tag.id?{...e,name:t.tag.name}:e);case`merged`:return e.filter(e=>e.id!==t.sourceId).map(e=>e.id===t.target.id?{...e,name:t.target.name,carrier_count:t.target.carrier_count}:e);case`deleted`:return e.filter(e=>e.id!==t.tagId)}}function ar(e,t){switch(t.kind){case`renamed`:return e.some(e=>e.id===t.tag.id)?e.map(e=>e.id===t.tag.id?t.tag:e):e;case`deleted`:return e.some(e=>e.id===t.tagId)?e.filter(e=>e.id!==t.tagId):e;case`merged`:{if(!e.some(e=>e.id===t.sourceId))return e;if(e.some(e=>e.id===t.target.id))return e.filter(e=>e.id!==t.sourceId);let n={id:t.target.id,name:t.target.name};return e.map(e=>e.id===t.sourceId?n:e)}}}var or=(0,Z.createContext)(null);function sr({onChange:e,children:t}){let[n,r]=(0,Z.useState)(null),[i,a]=(0,Z.useState)(`idle`),o=(0,Z.useRef)(null),s=(0,Z.useRef)(`idle`),c=(0,Z.useRef)(0),l=(0,Z.useRef)(!1),u=(0,Z.useRef)(e);u.current=e;let d=(0,Z.useCallback)(e=>{o.current=e,r(e)},[]),f=(0,Z.useCallback)(e=>{s.current=e,a(e)},[]),p=(0,Z.useCallback)(()=>{let e=++c.current;l.current=!0,f(`loading`),x().then(t=>{e===c.current&&(l.current=!1,d(rr(t)),f(`loaded`))}).catch(()=>{e===c.current&&(l.current=!1,f(`error`))})},[f,d]),m=(0,Z.useCallback)(()=>{c.current++,l.current&&p()},[p]),h=(0,Z.useCallback)(()=>{(s.current===`idle`||s.current===`error`)&&p()},[p]),g=(0,Z.useCallback)(e=>{let t=o.current;if(t===null){l.current&&m();return}t.some(t=>t.id===e.id)||(d(rr([...t,{...e,carrier_count:1}])),m())},[d,m]),_=(0,Z.useCallback)(e=>{let t=o.current;t!==null&&d(rr(ir(t,e))),m(),u.current?.(e)},[d,m]),v=(0,Z.useCallback)(async(e,t)=>{let n=await w(e,t);return n.kind===`renamed`&&_({kind:`renamed`,tag:n.tag}),n},[_]),y=(0,Z.useCallback)(async(e,t)=>{let n;try{n=await j(e,t)}catch(e){throw p(),e}return _({kind:`merged`,sourceId:e,target:n}),n},[_,p]),b=(0,Z.useCallback)(async e=>{let t;try{t=await k(e)}catch(e){throw p(),e}return _({kind:`deleted`,tagId:e}),t},[_,p]),S=(0,Z.useMemo)(()=>({vocabulary:n,status:i,ensureLoaded:h,reload:p,remember:g,rename:v,merge:y,remove:b}),[n,i,h,p,g,v,y,b]);return(0,Q.jsx)(or.Provider,{value:S,children:t})}function cr(){let e=(0,Z.useContext)(or);if(!e)throw Error(`useTagVocabulary must be used inside TagVocabularyProvider`);return e}var lr=[];function ur(e){return e===1?`artist`:`artists`}function dr({open:e,onOpenChange:t}){let{vocabulary:n,status:r,reload:i,rename:o,merge:s,remove:c}=cr(),l=n??lr,[u,d]=(0,Z.useState)(null),[f,p]=(0,Z.useState)(null),[m,h]=(0,Z.useState)(null),[g,_]=(0,Z.useState)(``),v=tr(g),y=v>32,[b,x]=(0,Z.useState)(!1),[S,C]=(0,Z.useState)(null),w=(0,Z.useRef)(null),T=(0,Z.useRef)(null),E=(0,Z.useRef)(null),D=(0,Z.useRef)(null),O=(0,Z.useRef)(!1);(0,Z.useEffect)(()=>{O.current=e,e&&i()},[e]),(0,Z.useLayoutEffect)(()=>{let e=u;if(!e||r!==`loaded`)return;let t=w.current?.querySelectorAll(`li`);if(e.kind===`after-delete`){if(l.some(t=>t.id===e.id))return;if(d(null),l.length===0){document.querySelector(`[data-slot="dialog-close"]`)?.focus();return}t?.[Math.min(e.index,l.length-1)]?.querySelector(`button`)?.focus();return}d(null);let n=l.findIndex(t=>t.id===e.id);t?.[n]?.querySelector(`button`)?.focus()},[l,r,u]),(0,Z.useLayoutEffect)(()=>{if(r!==`loaded`||!O.current)return;O.current=!1;let e=w.current,t=e?.querySelector(`li button`);if(!e||!t)return;let n=e.closest(`[data-slot="dialog-content"]`),i=document.activeElement,a=n?.querySelector(`[data-slot="dialog-close"]`);(!i||i===document.body||!n?.contains(i)||i===n||i===a)&&t.focus()},[r]),(0,Z.useEffect)(()=>{m&&(E.current?.focus(),E.current?.select())},[m]),(0,Z.useEffect)(()=>{if(m!==null)return;let e=T.current;e!==null&&(T.current=null,(w.current?.querySelectorAll(`li`)[e])?.querySelector(`button`)?.focus())},[m]);function k(e){h(e),_(e.name)}function A(e){T.current=e,h(null),_(``)}async function j(e){let t=g.trim();if(!(b||y||t===``||t===e.name)){x(!0);try{let n=await o(e.id,t);n.kind===`renamed`?(d({kind:`tag`,id:e.id}),h(null),_(``),x(!1),J.success(`Renamed “${e.name}” to “${n.tag.name}”.`)):(x(!1),C({sourceId:e.id,sourceName:e.name,target:n.target,carrierCountAfterMerge:n.carrierCountAfterMerge}))}catch{x(!1),J.error(`Couldn't rename “${e.name}” — try again.`)}}}async function ee(){if(!S)return;let{sourceId:e,sourceName:t,target:n}=S;try{let r=await s(e,n.id);d({kind:`tag`,id:n.id}),h(null),_(``),J.success(`Merged “${t}” into “${r.name}” — ${r.carrier_count} ${ur(r.carrier_count)} now carry it.`)}catch{J.error(`Couldn't merge “${t}” into “${n.name}” — try again.`)}}async function M(){if(!f)return;let e=f,t=l.findIndex(t=>t.id===e.id);try{let{carrier_count:n}=await c(e.id);d({kind:`after-delete`,id:e.id,index:t}),J.success(n===0?`Deleted “${e.name}”.`:`Deleted “${e.name}” from ${n} ${ur(n)}.`)}catch{J.error(`Couldn't delete “${e.name}” — try again.`)}}return(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(qn,{open:e,onOpenChange:t,children:(0,Q.jsxs)(Xn,{className:`max-h-[calc(100dvh-2rem)] grid-rows-[auto_1fr] sm:max-w-lg`,children:[(0,Q.jsxs)(Zn,{children:[(0,Q.jsx)(Qn,{children:`Manage tags`}),(0,Q.jsx)($n,{children:`Rename or delete tags across all artists. Counts include only artists on your watchlist.`})]}),(r===`idle`||r===`loading`)&&(0,Q.jsx)(`div`,{className:`flex flex-col gap-2`,children:[0,1,2].map(e=>(0,Q.jsx)(Yt,{className:`h-10 w-full`},e))}),r===`error`&&(0,Q.jsx)(Xt,{heading:`Couldn't load tags.`,body:`Please try again.`,action:(0,Q.jsx)(a,{variant:`secondary`,onClick:i,children:`Retry`})}),r===`loaded`&&l.length===0&&(0,Q.jsx)(Xt,{heading:`No tags yet`,body:`Add a tag from any artist's card on the Watchlist and it will appear here.`}),r===`loaded`&&l.length>0&&(0,Q.jsx)(`ul`,{ref:w,"aria-label":`Tags`,className:`-mx-2 max-h-[min(60vh,28rem)] divide-y divide-border overflow-y-auto overscroll-contain px-2`,children:l.map((e,t)=>m?.id===e.id?(0,Q.jsxs)(`li`,{className:`flex items-center gap-2 py-2`,children:[(0,Q.jsx)(xt,{ref:E,value:g,onChange:e=>_(e.target.value),onKeyDown:n=>{n.key===`Enter`?(n.preventDefault(),j(e)):n.key===`Escape`&&(n.preventDefault(),n.stopPropagation(),A(t))},readOnly:b,"aria-label":`New name for ${e.name}`,className:`h-8 flex-1`}),v>=25&&(0,Q.jsxs)(`span`,{className:`shrink-0 text-label tabular-nums ${y?`text-destructive`:`text-muted-foreground`}`,children:[v,`/`,32]}),(0,Q.jsx)(a,{variant:`ghost`,size:`sm`,onClick:()=>A(t),children:`Cancel`}),(0,Q.jsx)(a,{ref:D,variant:`default`,size:`sm`,className:`min-w-20`,disabled:b||y||g.trim()===``||g.trim()===e.name,onClick:()=>void j(e),children:b?`Saving…`:`Save`})]},e.id):(0,Q.jsxs)(`li`,{className:`flex items-center gap-4 py-2`,children:[(0,Q.jsxs)(`div`,{className:`flex min-w-0 flex-1 items-baseline gap-1`,children:[(0,Q.jsx)(`span`,{className:`truncate text-body text-foreground`,title:e.name,children:e.name}),(0,Q.jsxs)(`span`,{className:`shrink-0 text-label whitespace-nowrap text-muted-foreground`,children:[`· `,e.carrier_count,` `,ur(e.carrier_count)]})]}),(0,Q.jsx)(a,{variant:`ghost`,size:`sm`,"aria-label":`Rename tag ${e.name}`,onClick:()=>k(e),children:`Rename`}),(0,Q.jsx)(a,{variant:`ghost`,size:`sm`,className:`text-muted-foreground hover:text-destructive`,"aria-label":`Delete tag ${e.name}`,onClick:()=>p(e),children:`Delete`})]},e.id))})]})}),f&&(0,Q.jsx)(Gn,{open:!0,onOpenChange:e=>{e||p(null)},title:f.carrier_count===0?`Delete “${f.name}”?`:`Delete “${f.name}” from ${f.carrier_count} ${ur(f.carrier_count)}?`,description:f.carrier_count===0?`No artists on your watchlist carry this tag, and this can't be undone.`:`The tag is removed everywhere and this can't be undone; the artists stay on your watchlist.`,actionLabel:`Delete tag`,pendingLabel:`Deleting…`,variant:`destructive`,onConfirm:M}),S&&(0,Q.jsx)(Gn,{open:!0,onOpenChange:e=>{e||C(null)},title:`Merge “${S.sourceName}” into “${S.target.name}”?`,description:`${S.carrierCountAfterMerge} ${ur(S.carrierCountAfterMerge)} will carry “${S.target.name}”, and “${S.sourceName}” will be deleted.`,actionLabel:`Merge tags`,pendingLabel:`Merging…`,variant:`default`,onConfirm:ee,finalFocus:D})]})}var fr=300;function pr({onResults:e}){let[t,n]=(0,Z.useState)(``),[r,i]=(0,Z.useState)(!1),a=(0,Z.useRef)(null),o=(0,Z.useRef)(null);(0,Z.useEffect)(()=>()=>{a.current&&clearTimeout(a.current),o.current?.abort()},[]);function s(t){o.current?.abort();let n=new AbortController;o.current=n,i(!0),C(t,n.signal).then(t=>{n.signal.aborted||e(t)}).catch(()=>{n.signal.aborted||e(null)}).finally(()=>{n.signal.aborted||i(!1)})}function c(t){let r=t.target.value;n(r),a.current&&clearTimeout(a.current),o.current?.abort();let c=r.trim();if(c===``){i(!1),e(null);return}a.current=setTimeout(()=>s(c),fr)}return(0,Q.jsxs)(`label`,{className:`flex w-full max-w-xl flex-col gap-1 text-label text-muted-foreground`,children:[`Search artists`,(0,Q.jsxs)(`div`,{className:`relative`,children:[(0,Q.jsx)(xt,{type:`text`,value:t,onChange:c,placeholder:`Search for an artist…`,className:`pr-10`}),r&&(0,Q.jsx)(A,{className:`absolute top-1/2 right-3 size-4 -translate-y-1/2 animate-spin text-muted-foreground`,"aria-hidden":`true`})]})]})}var mr={musicbrainz:`MusicBrainz`,deezer:`Deezer`};function hr(e){return mr[e]??e}function gr({response:e,watchlistEntries:t,onAdd:n}){let r=Object.keys(e.sources);return(0,Q.jsx)(`div`,{className:`grid grid-cols-1 gap-6 sm:grid-cols-2`,children:r.map(i=>(0,Q.jsx)(_r,{sourceName:i,result:e.sources[i],otherSourceNames:r.filter(e=>e!==i),query:e.query,watchlistEntries:t,onAdd:n},i))})}function _r({sourceName:e,result:t,otherSourceNames:n,query:r,watchlistEntries:i,onAdd:a}){return(0,Q.jsxs)(`div`,{className:`flex flex-col gap-3`,children:[(0,Q.jsx)(`h2`,{className:`text-heading font-semibold text-foreground`,children:hr(e)}),t.status===`error`&&(0,Q.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[hr(e),` is unavailable right now — showing`,` `,n.map(hr).join(`, `),` results only.`]}),t.status!==`error`&&t.artists.length===0&&(0,Q.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[`No matches for "`,r,`"`]}),t.status!==`error`&&t.artists.length>0&&(0,Q.jsx)(`ul`,{className:`flex max-h-96 flex-col gap-3 overflow-y-auto`,children:t.artists.map(t=>(0,Q.jsx)(vr,{sourceName:e,artist:t,alreadyWatching:i?.some(n=>n[z(e)]===t.id)??!1,onAdd:a},t.id))})]})}function vr({sourceName:e,artist:t,alreadyWatching:n,onAdd:r}){let[i,o]=(0,Z.useState)(!1),s=V(e),c=t.disambiguation??t.country;async function l(){o(!0);try{await r(e,t)}finally{o(!1)}}return(0,Q.jsxs)(`li`,{className:`flex items-center gap-3 rounded-md bg-card p-3`,children:[(0,Q.jsx)(vt,{src:t.image_url,alt:t.name,size:48}),(0,Q.jsxs)(`div`,{className:`flex min-w-0 flex-1 flex-col gap-0.5`,children:[(0,Q.jsx)(`span`,{className:`truncate text-body font-medium text-foreground`,children:t.name}),(0,Q.jsx)(`span`,{className:`text-label text-muted-foreground`,children:t.type}),c!==null&&(0,Q.jsx)(`span`,{className:`truncate text-label text-muted-foreground`,children:c})]}),n?(0,Q.jsx)(a,{variant:`secondary`,size:`sm`,disabled:!0,className:`shrink-0`,children:`Already watching`}):s?(0,Q.jsx)(a,{size:`sm`,disabled:i,"aria-busy":i,onClick:l,className:`shrink-0`,children:i?(0,Q.jsx)(A,{className:`size-4 animate-spin`,"aria-hidden":`true`}):`Add to Watchlist`}):(0,Q.jsx)(a,{variant:`secondary`,size:`sm`,disabled:!0,title:`This project can't yet match a Deezer artist to its MusicBrainz identity -- search MusicBrainz to add this artist and get release detection.`,className:`shrink-0`,children:`Search MusicBrainz to add`})]})}function yr({className:e,...t}){return(0,Q.jsx)(`textarea`,{"data-slot":`textarea`,className:I(`flex field-sizing-content min-h-16 w-full resize-none rounded-xl border border-input bg-input/30 px-3 py-3 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40`,e),...t})}function br(e){return e instanceof P&&e.code===`note_too_long`?`Notes can be at most 500 characters.`:`Couldn't save the note — your text is still here. Try again.`}function xr({entry:e,onEntryChange:t,announce:n}){let[r,i]=(0,Z.useState)(!1),[o,s]=(0,Z.useState)(e.note??``),[c,l]=(0,Z.useState)(``),[u,d]=(0,Z.useState)(!1),[f,p]=(0,Z.useState)(!1),[m,h]=(0,Z.useState)(!1),[g,_]=(0,Z.useState)(``),v=(0,Z.useRef)(null),y=(0,Z.useRef)(null),b=(0,Z.useRef)(null),x=(0,Z.useRef)(null),S=(0,Z.useRef)(null),C=(0,Z.useRef)(nr(e.note??``));(0,Z.useLayoutEffect)(()=>{if(!r)return;let e=v.current;if(!e)return;e.focus();let t=e.value.length;e.setSelectionRange(t,t)},[r]),(0,Z.useLayoutEffect)(()=>{if(r)return;let e=S.current;e&&(S.current=null,(e.hasNote?y.current:b.current)?.focus())},[r]),(0,Z.useLayoutEffect)(()=>{if(r||e.note===null){h(!1);return}if(f)return;let t=x.current;if(!t)return;let n=()=>h(t.scrollHeight>t.clientHeight);if(n(),typeof ResizeObserver>`u`)return;let i=new ResizeObserver(n);return i.observe(t),()=>i.disconnect()},[r,e.note,f]);function w(){s(e.note??``),l(``),p(!1),C.current=nr(e.note??``),i(!0)}function T(){u||(S.current={hasNote:e.note!==null},i(!1),l(``))}function E(e){let t=C.current,n=nr(e);C.current=n,s(e),n===500&&t!==500?_(`Note limit reached — 500 characters.`):t<450&&n>=450&&_(`50 characters left.`)}let D=o.trim(),O=e.note??``,k=nr(o),A=k>500,j=D===O||A;async function ee(){if(j||u)return;let r=D===``?null:D;d(!0),l(``);try{let a=await F(e.id,r);S.current={hasNote:!!a.note},t(e.id,{note:a.note}),n(a.note?`Note saved for ${e.name}.`:`Note cleared for ${e.name}.`),i(!1),l(``)}catch(e){l(br(e))}finally{d(!1)}}if(r){let t=`note-counter-${e.id}`;return(0,Q.jsxs)(`div`,{className:`mt-1 flex flex-col gap-1`,children:[(0,Q.jsx)(yr,{ref:v,value:o,onChange:e=>E(e.target.value),onKeyDown:e=>{if(e.key===`Escape`){if(u)return;e.preventDefault(),T()}else e.key===`Enter`&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),ee())},readOnly:u,"aria-busy":u,"aria-describedby":t,className:`max-h-40 overflow-y-auto md:text-label`,"aria-label":`Note for ${e.name}`,placeholder:`Add a note about this artist`}),(0,Q.jsxs)(`div`,{className:`flex items-center justify-between gap-2`,children:[(0,Q.jsxs)(`span`,{id:t,className:`text-label tabular-nums ${A?`text-destructive`:k>=450?`text-foreground`:`text-muted-foreground`}`,children:[k,`/`,500]}),(0,Q.jsx)(`span`,{"aria-live":`polite`,className:`sr-only`,children:g}),(0,Q.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Q.jsx)(a,{variant:`ghost`,size:`sm`,disabled:u,onClick:T,children:`Cancel`}),(0,Q.jsx)(a,{variant:`default`,size:`sm`,className:`min-w-20`,disabled:j||u,onClick:()=>void ee(),children:u?`Saving…`:`Save`})]})]}),(0,Q.jsx)(`p`,{"aria-live":`polite`,className:`text-label text-destructive`,children:c})]})}return e.note===null?(0,Q.jsx)(`div`,{className:`mt-1`,children:(0,Q.jsxs)(a,{ref:b,variant:`ghost`,size:`xs`,className:`h-6 text-label text-muted-foreground hover:text-foreground`,"aria-label":`Add note for ${e.name}`,onClick:w,children:[(0,Q.jsx)(en,{"aria-hidden":`true`,className:`size-3`}),`add note`]})}):(0,Q.jsxs)(`div`,{className:`mt-1 flex items-start gap-1`,children:[(0,Q.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,Q.jsx)(`p`,{ref:x,id:`note-${e.id}`,className:`text-label break-words whitespace-pre-line text-muted-foreground ${f?``:`line-clamp-2`}`,children:e.note}),m&&(0,Q.jsx)(`button`,{type:`button`,className:`rounded-sm text-label text-muted-foreground underline underline-offset-4 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50`,"aria-expanded":f,"aria-controls":`note-${e.id}`,onClick:()=>p(e=>!e),children:f?`less`:`more`})]}),(0,Q.jsx)(a,{ref:y,variant:`ghost`,size:`icon-xs`,"aria-label":`Edit note for ${e.name}`,onClick:w,children:(0,Q.jsx)(tn,{"aria-hidden":`true`,className:`size-3`})})]})}function Sr(e){if(!e)return null;for(let t of e.elements){let e=t.tagName;if(e===`BUTTON`||e===`INPUT`){let e=t;if(e.type===`submit`)return e}}return null}function Cr(e){return{checked(t){return e.indeterminate?{}:t?{"data-checked":``}:{"data-unchecked":``}},...Et}}var wr=Z.createContext(void 0);function Tr(){return Z.useContext(wr)}var Er=Z.createContext(void 0);function Dr(){let e=Z.useContext(Er);if(e===void 0)throw Error(c(14));return e}var Or=`data-parent`,kr=Z.forwardRef(function(e,t){let{checked:n,className:r,defaultChecked:i=!1,"aria-labelledby":a,disabled:o=!1,form:c,id:m,indeterminate:h=!1,inputRef:g,name:_,onCheckedChange:v,parent:b=!1,readOnly:x=!1,render:S,required:C=!1,uncheckedValue:w,value:D,nativeButton:O=!1,style:k,...A}=e,{clearErrors:j}=Rt(),{disabled:ee,name:M,setDirty:te,setFilled:N,setFocused:P,setTouched:F,state:I,validationMode:L,validityData:re,validation:ae}=At(),oe=sn(),{labelId:R,controlId:se,registerControlId:ce,getDescriptionProps:le}=kt(),z=Tr(),B=z?.allValues===void 0?void 0:z.parent,ue=B!==void 0,V=ee||oe.disabled||z?.disabled||o,de=M??_,H=D??de,fe=zt(),pe=zt(),me=m||se;ue&&(b?me=pe:H===void 0?me||=pe:me=`${B.id}-${H}`);let he={};ue&&(b?he=B.getParentProps():H!==void 0&&(he=B.getChildProps(H)));let{checked:ge=n,indeterminate:_e=h,onCheckedChange:ve,...ye}=he,be=z?.value,xe=Z.useRef(null),Se=ne(()=>Symbol()),Ce=Z.useRef(!1),{getButtonProps:we,buttonRef:Te}=d({disabled:V,native:O}),Ee=z?.validation??ae,[U,De]=St({controlled:H!==void 0&&be!==void 0&&!b?be.includes(H):ge,default:i,name:`Checkbox`,state:`checked`}),Oe=ue?!!ge:U,ke=ue&&_e||h;u(()=>{ce!==E&&(Ce.current=!0,ce(Se.current,me))},[me,ce,Se]),Z.useEffect(()=>{let e=Se.current;return()=>{!Ce.current||ce===E||(Ce.current=!1,ce(e,void 0))}},[ce,Se]),jt(xe,fe,U,void 0,!z&&!V,_);let Ae=Z.useRef(null),je=Ee.registerInput,Me=z?H:void 0,Ne=Z.useCallback(e=>je(e,{controlRef:xe,value:Me}),[je,Me]),W=T(g,Ae,b?void 0:Ne),Pe=ie(a,R,Ae,!O,me??void 0);u(()=>{Ae.current&&(Ae.current.indeterminate=ke,U&&N(!0))},[U,ke,N]),_t(U,()=>{z||(j(de),N(U),te(U!==re.initialValue),Ee.change(U))});let Fe=l({checked:U,disabled:V,form:c,name:b?void 0:de,id:O?void 0:me??void 0,required:C,ref:W,style:de?ze:Ve,tabIndex:-1,type:`checkbox`,"aria-hidden":!0,onChange(e){if(e.nativeEvent.defaultPrevented)return;if(x){e.preventDefault();return}let t=e.currentTarget.checked,n=X(It,e.nativeEvent);if(v?.(t,n),!n.isCanceled&&(ve?.(t,n),!n.isCanceled&&(De(t),H!==void 0&&z!==void 0&&!b&&!ue))){let e=t?[...z.value,H]:z.value.filter(e=>e!==H);z.setValue(e,n)}},onClick(e){e.stopPropagation()},onFocus(){xe.current?.focus()}},D===void 0?p:{value:(z?U&&D:D)||``},le,e=>Ee.getValidationProps(V,e));Z.useEffect(()=>{if(!B||H===void 0)return;let e=B.disabledStatesRef.current;return e.set(H,V),()=>{e.delete(H)}},[B,V,H]);let Ie=Z.useMemo(()=>({...I,checked:Oe,disabled:V,readOnly:x,required:C,indeterminate:ke}),[I,Oe,V,x,C,ke]),Le=Cr(Ie),Re=s(`span`,e,{state:Ie,ref:[Te,xe,t],props:[{id:O?me??void 0:fe,role:`checkbox`,"aria-checked":ke?`mixed`:Oe,"aria-readonly":x||void 0,"aria-required":C||void 0,"aria-labelledby":Pe,[Or]:b?``:void 0,onFocus(){V||P(!0)},onBlur(){let e=Ae.current;e&&(F(!0),P(!1),L===`onBlur`&&Ee.commit(z?be:e.checked))},onKeyDown(e){if(e.key!==`Enter`||(e.preventBaseUIHandler(),e.defaultPrevented))return;let t=Ae.current?.form??null,n=e.currentTarget,r=e.nativeEvent,i=e.preventDefault,a=r.preventDefault,o=!1;e.preventDefault=()=>{o=!0,i.call(e)},r.preventDefault=()=>{o=!0,a.call(r)},a.call(r),y(n).queueMicrotask(()=>{e.preventDefault=i,r.preventDefault=a,o||Sr(t)?.click()})},onClick(e){if(x||V)return;e.preventDefault();let t=Ae.current;t&&f(t,e)}},A,ye,we,le,e=>Ee.getValidationProps(V,e)],stateAttributesMapping:Le});return(0,Q.jsxs)(Er.Provider,{value:Ie,children:[Re,!U&&!z&&de&&!b&&w!==void 0&&(0,Q.jsx)(`input`,{type:`hidden`,form:c,name:de,value:w,disabled:V}),(0,Q.jsx)(`input`,{...Fe,suppressHydrationWarning:!0})]})}),Ar=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,keepMounted:a=!1,...o}=e,c=Dr(),l=c.checked||c.indeterminate,{mounted:u,transitionStatus:d,setMounted:f}=gt(l),p=Z.useRef(null),m={...c,transitionStatus:d};mt({open:l,ref:p,onComplete(){l||f(!1)}});let h={...Cr(c),...je},g=a||u,_=s(`span`,e,{ref:[t,p],state:m,stateAttributesMapping:h,props:o});return g?_:null});function jr({className:e,...t}){return(0,Q.jsx)(kr,{"data-slot":`checkbox`,className:I(`peer relative flex size-4 shrink-0 items-center justify-center rounded-[6px] border border-input transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary`,e),...t,children:(0,Q.jsx)(Ar,{"data-slot":`checkbox-indicator`,className:`grid place-content-center text-current transition-none [&>svg]:size-3.5`,children:(0,Q.jsx)(fe,{})})})}function Mr({entry:e,onEntryChange:t}){let[n,r]=(0,Z.useState)(!1),[i,a]=(0,Z.useState)(!1);async function o(n,i){let a=e.release_types,o=i?[...a,n]:a.filter(e=>e!==n);t(e.id,{release_types:o}),r(!0);try{let n=await L(e.id,{releaseTypes:o});t(e.id,{release_types:n.release_types})}catch{t(e.id,{release_types:a}),J.error(`Couldn't update preferences — try again.`)}finally{r(!1)}}async function s(n,r){let i=e.muted_event_types,o=r?[...i,n]:i.filter(e=>e!==n);t(e.id,{muted_event_types:o}),a(!0);try{let n=await L(e.id,{mutedEventTypes:o});t(e.id,{muted_event_types:n.muted_event_types})}catch{t(e.id,{muted_event_types:i}),J.error(`Couldn't update preferences — try again.`)}finally{a(!1)}}return(0,Q.jsxs)(`div`,{className:`flex flex-col gap-3 sm:flex-row sm:gap-6`,children:[(0,Q.jsxs)(`fieldset`,{className:`flex flex-col gap-1.5`,disabled:n,children:[(0,Q.jsx)(`legend`,{className:`text-label text-muted-foreground`,children:`Release types`}),(0,Q.jsxs)(`div`,{className:`flex flex-wrap gap-3`,children:[(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.release_types.includes(`album`),onCheckedChange:e=>o(`album`,e),"aria-label":`Album release type`}),`Album`]}),(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.release_types.includes(`single`),onCheckedChange:e=>o(`single`,e),"aria-label":`Single release type`}),`Single`]}),(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.release_types.includes(`ep`),onCheckedChange:e=>o(`ep`,e),"aria-label":`EP release type`}),`EP`]}),(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.release_types.includes(`deluxe`),onCheckedChange:e=>o(`deluxe`,e),"aria-label":`Deluxe release type`}),`Deluxe`]})]})]}),(0,Q.jsxs)(`fieldset`,{className:`flex flex-col gap-1.5`,disabled:i,children:[(0,Q.jsx)(`legend`,{className:`text-label text-muted-foreground`,children:`Mute alerts`}),(0,Q.jsxs)(`div`,{className:`flex flex-wrap gap-3`,children:[(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.muted_event_types.includes(`new_release`),onCheckedChange:e=>s(`new_release`,e),"aria-label":`Mute new release alerts`}),`New release`]}),(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.muted_event_types.includes(`guest_feature`),onCheckedChange:e=>s(`guest_feature`,e),"aria-label":`Mute guest feature alerts`}),`Guest feature`]}),(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.muted_event_types.includes(`deluxe_change`),onCheckedChange:e=>s(`deluxe_change`,e),"aria-label":`Mute deluxe change alerts`}),`Deluxe change`]})]})]})]})}function Nr(e,t,n,r,i,a,o,s,c,l=2){let u=R(n.current,{event:e,orientation:r,loopFocus:i,rtl:a,cols:l,disabledIndices:o,minIndex:s,maxIndex:c,prevIndex:t>c?s:t,stopEvent:!0});return ft(n.current,u)?void 0:u}var Pr=Z.createContext(void 0),Fr=Z.createContext(void 0),Ir=Z.createContext(void 0),Lr=Z.createContext(!1),Rr=Z.createContext(``);function zr(){let e=Z.useContext(Pr);if(!e)throw Error(c(22));return e}function Br(){let e=Z.useContext(Fr);if(!e)throw Error(c(23));return e}function Vr(){let e=Z.useContext(Ir);if(!e)throw Error(c(24));return e}function Hr(){return Z.useContext(Rr)}function Ur(){return Z.useContext(Lr)}var $={id:e=>e.id,labelId:e=>e.labelId,items:e=>e.items,selectedValue:e=>e.selectedValue,hasSelectionChips:e=>{let t=e.selectedValue;return Array.isArray(t)&&t.length>0},hasSelectedValue:e=>{let{selectedValue:t,selectionMode:n}=e;return t==null?!1:n===`multiple`&&Array.isArray(t)?t.length>0:!0},hasNullItemLabel:(e,t)=>t?qe(e.items):!1,open:e=>e.open,mounted:e=>e.mounted,forceMounted:e=>e.forceMounted,inline:e=>e.inline,activeIndex:e=>e.activeIndex,selectedIndex:e=>e.selectedIndex,isActive:(e,t)=>e.activeIndex===t,isSelected:(e,t)=>{let n=e.isItemEqualToValue,r=e.selectedValue;return Array.isArray(r)?r.some(e=>lt(t,e,n)):lt(t,r,n)},transitionStatus:e=>e.transitionStatus,popupProps:e=>e.popupProps,listProps:e=>e.listProps,inputProps:e=>e.inputProps,triggerProps:e=>e.triggerProps,itemProps:e=>e.itemProps,positionerElement:e=>e.positionerElement,listElement:e=>e.listElement,popupId:e=>e.popupId,triggerElement:e=>e.triggerElement,inputElement:e=>e.inputElement,inputGroupElement:e=>e.inputGroupElement,popupSide:e=>e.popupSide,openMethod:e=>e.openMethod,inputInsidePopup:e=>e.inputInsidePopup,inputOwnsFormValue:e=>e.inputOwnsFormValue,selectionMode:e=>e.selectionMode,name:e=>e.name,form:e=>e.form,disabled:e=>e.disabled,readOnly:e=>e.readOnly,required:e=>e.required,grid:e=>e.grid,virtualized:e=>e.virtualized,itemToStringLabel:e=>e.itemToStringLabel,isItemEqualToValue:e=>e.isItemEqualToValue,modal:e=>e.modal,autoHighlight:e=>e.autoHighlight};function Wr(e){return e==null?void 0:`${e}-popup`}function Gr(e,t){return(n,r)=>n==null?!1:e.contains(n,r,t)}function Kr(e){return Array.isArray(e)?e.map(e=>Kr(e)).join(`,`):e==null?``:String(e)}var qr=new Map;function Jr(e={}){let t={usage:`search`,sensitivity:`base`,ignorePunctuation:!0,...e},n=`${Kr(e.locale)}|${JSON.stringify(t)}`,r=qr.get(n);if(r)return r;let i=new Intl.Collator(e.locale,t),a={contains(e,t,n){if(!t)return!0;let r=Re(e,n);for(let e=0;e<=r.length-t.length;e+=1)if(i.compare(r.slice(e,e+t.length),t)===0)return!0;return!1},startsWith(e,t,n){if(!t)return!0;let r=Re(e,n);return i.compare(r.slice(0,t.length),t)===0},endsWith(e,t,n){if(!t)return!0;let r=Re(e,n),a=t.length;return r.length>=a&&i.compare(r.slice(r.length-a),t)===0}};return qr.set(n,a),a}var Yr=Jr;function Xr(e,t=!1){let{overflowY:n}=v(e);return n!==`auto`&&n!==`scroll`?!1:t?e.clientHeight>0:e.scrollHeight>e.clientHeight}var Zr=Symbol(`none`),Qr={value:Zr,index:-1};function $r(e){let{id:t,onOpenChangeComplete:n,defaultSelectedValue:r=null,selectedValue:i,onSelectedValueChange:a,defaultInputValue:s,inputValue:c,open:d,defaultOpen:f=!1,selectionMode:m,onItemHighlighted:_,name:v,form:y,disabled:b=!1,readOnly:x=!1,required:S=!1,inputRef:C,grid:w=!1,items:D,filteredItems:O,filter:k,openOnInputClick:A=!0,autoHighlight:j=!1,keepHighlight:M=!1,highlightItemOnHover:te=!0,loopFocus:N=!0,itemToStringLabel:P,itemToStringValue:F,isItemEqualToValue:I=ht,virtualized:L=!1,inline:re=!1,fillInputOnItemPress:ie=!0,modal:ae=!1,limit:R=-1,autoComplete:se=`list`,formAutoComplete:ce,locale:le,submitOnItemClick:z=!1}=e,{clearErrors:B}=Rt(),{setDirty:ue,validityData:V,setFilled:H,name:fe,disabled:pe,setTouched:me,setFocused:he,validationMode:ve,validation:ye}=At(),be=Ie(),xe=Ct({id:t}),Se=Yr({locale:le}),[Ce,we]=Z.useState(!1),[Oe,ke]=Z.useState(null),Ae=Z.useRef([]),je=Z.useRef([]),Me=Z.useRef(null),W=Z.useRef(null),Fe=Z.useRef(null),Le=Z.useRef(null),He=Z.useRef(null),Ue=Z.useRef(!0),We=Z.useRef(!1),Ge=Z.useRef(null),Ke=Z.useRef(null),qe=Z.useRef(null),Je=Z.useRef(Qr),Ye=Z.useRef(null),Xe=Z.useRef([]),Ze=Z.useRef(null),Qe=pe||b,et=fe??v,tt=m===`multiple`,nt=m===`single`,rt=c!==void 0||s!==void 0,it=D!==void 0,at=O!==void 0,ot;ot=j===`always`?`always`:j?`input-change`:!1;let[K,st]=St({controlled:i,default:tt?r??o:r,name:`Combobox`,state:`selectedValue`}),ut=Z.useMemo(()=>k===null?()=>!0:k===void 0?Gr(Se,P):k,[k,Se,P]),ft=ne(()=>rt?s??``:nt?Re(K,P):``).current,[vt,yt]=St({controlled:c,default:ft,name:`Combobox`,state:`inputValue`}),[q,bt]=St({controlled:d,default:f,name:`Combobox`,state:`open`}),J=Be(D),xt=Oe??String(vt).trim(),Tt=nt?Re(K,P):``,Et=nt&&!Ce&&xt!==``&&Tt.length===xt.length&&Se.contains(Tt,xt),Dt=Et?``:xt,Ot=it&&at&&Et,kt=Z.useMemo(()=>D?J?D.flatMap(e=>e.items):D:o,[D,J]),Nt=Z.useMemo(()=>{if(O&&!Ot)return O;if(!D)return o;if(J){let e=D,t=[],n=0;for(let r of e){if(R>-1&&n>=R)break;let e=R>-1?R-n:1/0,i=Dt===``?r.items.slice(0,e):[];if(Dt!==``)for(let t of r.items){if(i.length>=e)break;ut(t,Dt,P)&&i.push(t)}if(i.length>0){let e={...r,items:i};t.push(e),n+=i.length}}return t}if(Dt===``)return R>-1?kt.slice(0,R):kt;let e=[];for(let t of kt){if(R>-1&&e.length>=R)break;ut(t,Dt,P)&&e.push(t)}return e},[O,Ot,D,J,Dt,R,ut,P,kt]),Lt=Z.useMemo(()=>J?Nt.flatMap(e=>e.items):Nt,[Nt,J]),Y=ne(()=>{let e=null;return re&&q&&it&&m!==`none`&&(e=pt(Lt,K,I,tt)),new Ne({id:xe,labelId:void 0,selectedValue:K,open:q,items:D,selectionMode:m,listRef:Ae,labelsRef:je,popupRef:Me,emptyRef:He,inputRef:W,startDismissRef:Fe,endDismissRef:Le,keyboardActiveRef:Ue,chipsContainerRef:Ge,clearRef:Ke,valuesRef:Xe,pointerDownItemRef:Ze,selectionEventRef:qe,name:et,form:y,disabled:Qe,readOnly:x,required:S,grid:w,virtualized:L,openOnInputClick:A,itemToStringLabel:P,isItemEqualToValue:I,modal:ae,autoHighlight:ot,submitOnItemClick:z,hasInputValue:rt,mounted:!1,forceMounted:!1,transitionStatus:`idle`,inline:re,activeIndex:null,selectedIndex:e,popupProps:{},listProps:{},inputProps:{},triggerProps:{},itemProps:p,positionerElement:null,listElement:null,popupId:void 0,triggerElement:null,inputElement:null,inputGroupElement:null,popupSide:null,openMethod:null,inputInsidePopup:!0,inputOwnsFormValue:m===`none`,onOpenChangeComplete:E,setOpen:E,setInputValue:E,setSelectedValue:E,setIndices:E,handleSelection:E,forceMount:E,requestSubmit:E})}).current,zt=m===`none`?vt:K,Bt=Z.useMemo(()=>m===`none`?zt:Array.isArray(K)?K.map(e=>U(e,F)):U(K,F),[zt,F,m,K]),Ut=g(_),Gt=g(n),Kt=G(Y,$.activeIndex),qt=G(Y,$.selectedIndex),Yt=G(Y,$.positionerElement),Xt=G(Y,$.listElement),Zt=G(Y,$.triggerElement),Qt=G(Y,$.inputElement),$t=G(Y,$.inputGroupElement),en=G(Y,$.inline),tn=G(Y,$.inputInsidePopup),nn=G(Y,$.inputOwnsFormValue),rn=De(Zt),{mounted:an,setMounted:on,transitionStatus:sn}=gt(q),{openMethod:cn,triggerProps:ln}=ge(q),un=g(()=>Bt);jt(tn?rn:W,xe,zt,un,!Qe,v);let dn=g(()=>{D?je.current=Lt.map(e=>Re(e,P)):Y.set(`forceMounted`,!0)}),fn=g((e,t,n)=>{if(t===-1){if(Je.current===Qr)return;Je.current=Qr}else Je.current={value:e,index:t};Ut(e,Ht(n,void 0,{index:t}))}),pn=g(e=>{Y.update(e);let t=e.activeIndex;if(t===void 0)return;let n=e.type||`none`;t===null?fn(void 0,-1,n):fn(Xe.current[t],t,n)}),mn=g((t,n)=>{if(We.current=n.reason===Pt,e.onInputValueChange?.(t,n),!n.isCanceled){if(n.reason===`input-change`){q&&Oe!==null&&ke(null);let e=n.event,r=e.inputType;if(e.type===`compositionend`||r!=null&&r!==``&&r!==`insertReplacementText`){let e=t.trim()!==``;e&&we(!0),Ye.current={hasQuery:e};let n=Y.state.listElement;if(!Y.state.virtualized&&n){let e=Me.current;for(let t of ee(n.firstElementChild??n)){if(!h(t)||(e?!Mt(e,t):t.getAttribute(`role`)===`dialog`))break;if(Xr(t)){t.scrollTop=0;break}}}e&&ot&&Y.state.activeIndex==null&&(q||en)&&Y.set(`activeIndex`,0)}}else n.reason===`input-clear`&&t===``&&Y.state.inputInsidePopup&&(Ye.current={hasQuery:!1,selection:!0});yt(t)}}),hn=g((t,n)=>{if(q!==t&&(n.reason===`escape-key`&&it&&Lt.length===0&&!He.current&&n.allowPropagation(),e.onOpenChange?.(t,n),!n.isCanceled&&(t&&tn&&!en&&Oe!==null&&(we(!1),ke(null),vt!==``&&n.reason!==`input-change`&&mn(``,X(Pt,n.event))),!t&&Ce&&(nt?(en||ke(xt),xt===``&&we(!1)):tt&&(en||ke(xt),tn&&pn({activeIndex:null}),(!tn||en)&&mn(``,X(Pt,n.event)))),bt(t),!t&&tn&&(n.reason===`focus-out`||n.reason===`outside-press`)&&(me(!0),he(!1),ve===`onBlur`)))){let e=m===`none`?vt:K;ye.commit(e)}}),gn=g((e,t)=>{a?.(e,t),!t.isCanceled&&(st(e),(m===`none`&&Me.current&&ie||nt&&!Y.state.inputInsidePopup)&&mn(Re(e,P),X(t.reason,t.event)))}),_n=g((e,t)=>{let n=wt(e),r=qe.current??e;qe.current=null;let i=X(Wt,r),a=n?.closest(`a`)?.getAttribute(`href`);if(a){a.startsWith(`#`)&&hn(!1,i);return}if(tt){let e=Array.isArray(K)?K:[];if(gn(oe(e,t,I)?_e(e,t,I):[...e,t],i),i.isCanceled||!(W.current&&W.current.value.trim()!==``))return;Y.state.inputInsidePopup?mn(``,X(Pt,i.event)):hn(!1,i)}else{if(gn(t,i),i.isCanceled)return;hn(!1,i)}}),vn=g(()=>{let e=ye.inputRef.current?.form??Y.state.inputElement?.form;e&&typeof e.requestSubmit==`function`&&e.requestSubmit()}),yn=g(()=>{if(on(!1),Gt?.(!1),we(!1),ke(null),pn(m===`none`?{activeIndex:null,selectedIndex:null}:{activeIndex:null}),tt&&W.current&&W.current.value!==``&&!We.current&&mn(``,X(Pt)),nt)if(Y.state.inputInsidePopup)W.current&&W.current.value!==``&&mn(``,X(Pt));else{let e=Re(K,P);W.current&&W.current.value!==e&&mn(e,X(e===``?Pt:It))}}),bn=Z.useMemo(()=>en&&Yt?{current:Yt.closest(`[role="dialog"]`)}:Me,[en,Yt]);mt({enabled:!e.actionsRef,open:q,ref:bn,onComplete(){q||yn()}}),Z.useImperativeHandle(e.actionsRef,()=>({unmount:yn}),[yn]),u(function(){q||(Ze.current=null,m!==`none`&&pn({selectedIndex:pt(it?kt:Xe.current,K,I,tt)}))},[q,K,m,tt,it,kt,I,pn]),u(()=>{D&&(Xe.current=Lt,Ae.current.length=Lt.length)},[D,Lt]),u(()=>{let e=Ye.current;if(e){let t=q||en||Y.state.positionerElement?.hidden===!1;if(e.hasQuery)ot&&t&&Y.set(`activeIndex`,0),Ye.current=null;else if(String(vt).trim()===``&&(Ye.current=null,t)){let t=e.selection;ot===`always`&&!t&&Y.state.selectionMode===`none`&&Y.set(`activeIndex`,0),queueMicrotask(()=>{if(!Y.state.open&&!Y.state.inline||W.current&&W.current.value.trim()!==``)return;let e=Y.state.selectedValue,n=Y.state.selectionMode===`multiple`,r=n&&Array.isArray(e)?e[e.length-1]:e,i=Y.state.selectionMode!==`none`&&r!=null;if(i||t){let t=it||at?Lt:Xe.current;Y.set(`activeIndex`,i?pt(t,e,Y.state.isItemEqualToValue,n):null)}else ot===`always`&&Y.set(`activeIndex`,0)})}}if(!q&&!en)return;let t=it||at?Lt:Xe.current,n=Y.state.activeIndex;if(n==null){if(ot===`always`&&t.length>0){Y.set(`activeIndex`,0);return}fn(void 0,-1,It);return}if(n>=t.length){fn(void 0,-1,It),Y.set(`activeIndex`,null);return}let r=t[n],i=Je.current.value,a=i!==Zr&<(r,i,Y.state.isItemEqualToValue);(Je.current.index!==n||!a)&&fn(r,n,It)},[Kt,ot,fn,at,it,Lt,en,q,Y,vt]),u(()=>{if(m===`none`){H(String(vt)!==``);return}H(tt?Array.isArray(K)&&K.length>0:K!=null)},[H,m,vt,K,tt]),Z.useEffect(()=>{it&&ot&&Lt.length===0&&pn({activeIndex:null})},[it,ot,Lt.length,pn]);function xn(e){let t=V.initialValue;return Array.isArray(e)&&Array.isArray(t)?!$e(e,t,(e,t)=>lt(e,t,I)):e!==t}_t(xt,()=>{!q||xt===``||xt===String(ft)||we(!0)});function Sn(){let e=Re(K,P);vt!==e&&mn(e,X(It))}_t(K,()=>{m!==`none`&&(B(et),ue(xn(K)),ye.change(K),nt&&!rt&&!tn&&Sn())}),_t(vt,()=>{m===`none`&&(B(et),ue(vt!==V.initialValue),ye.change(vt))}),_t(D,()=>{!nt||rt||tn||Ce||Sn()});let Cn=Te({open:en?!0:q,onOpenChange:hn,elements:{reference:tn?Zt:Qt,floating:Yt}}),wn=w?`grid`:`listbox`,Tn=q||en,En=Tn?`true`:`false`,Dn=Z.useMemo(()=>{let e=Qt?.tagName===`INPUT`,t=Qt==null||e,n=t||Tn,r=t?{autoComplete:`off`,spellCheck:`false`,autoCorrect:`off`,autoCapitalize:`none`}:{};return n&&(r.role=`combobox`,r[`aria-expanded`]=En,r[`aria-haspopup`]=wn,r[`aria-controls`]=Tn?Xt?.id:void 0,r[`aria-autocomplete`]=se),{reference:r,floating:{role:`presentation`}}},[Qt,Tn,En,wn,Xt?.id,se]),On=Pe(Cn,{enabled:!x&&!Qe&&A,event:`mousedown-only`,toggle:!1,touchOpenDelay:tn?0:100,reason:Ft}),kn=ct(Cn,{enabled:!x&&!Qe&&!en,outsidePressEvent:{mouse:`sloppy`,touch:`intentional`},bubbles:en?!0:void 0,outsidePress(e){let t=wt(e);return!Mt(Zt,t)&&!Mt(Ke.current,t)&&!Mt(Ge.current,t)&&!Mt($t,t)}}),An=de(Cn,{enabled:!x&&!Qe,id:xe,listRef:Ae,activeIndex:Kt,selectedIndex:qt,virtual:!0,loopFocus:N,allowEscape:N&&!ot,focusItemOnOpen:Ce||m===`none`&&!ot?!1:`auto`,focusItemOnHover:te,resetOnPointerLeave:!M,orientation:w?`horizontal`:void 0,rtl:be===`rtl`,disabledIndices:o,grid:w?Nr:void 0,onNavigate(e,t){!t&&!q||sn===`ending`||pn(t?{activeIndex:e,type:Ue.current?Vt:Jt}:{activeIndex:e})}}),jn=Z.useMemo(()=>l(An.reference,{onKeyDown(e){w&&Y.state.activeIndex==null&&(e.key===`ArrowLeft`||e.key===`ArrowRight`)&&e.preventBaseUIHandler()}},kn.reference,On.reference,Dn.reference),[An.reference,kn.reference,On.reference,Dn.reference,w,Y]),Mn=Z.useMemo(()=>l(Ee,kn.floating),[kn.floating]),Nn=Z.useMemo(()=>l(An.floating,Dn.floating),[An.floating,Dn.floating]),Pn=Z.useMemo(()=>{let e=An.item;return e?{...e,onFocus:void 0}:p},[An.item]);dt(()=>{Y.update({inline:re,popupProps:Mn,listProps:Nn,inputProps:jn,triggerProps:ln,itemProps:Pn,setOpen:hn,setInputValue:mn,setSelectedValue:gn,setIndices:pn,handleSelection:_n,forceMount:dn,requestSubmit:vn,onOpenChangeComplete:Gt})}),u(()=>{Y.update({id:xe,selectedValue:K,open:q,mounted:an,transitionStatus:sn,items:D,inline:re,popupProps:Mn,listProps:Nn,inputProps:jn,triggerProps:ln,openMethod:cn,itemProps:Pn,selectionMode:m,name:et,form:y,disabled:Qe,readOnly:x,required:S,grid:w,virtualized:L,openOnInputClick:A,itemToStringLabel:P,modal:ae,autoHighlight:ot,isItemEqualToValue:I,submitOnItemClick:z,hasInputValue:rt,inputOwnsFormValue:m===`none`&&(re||!Y.state.inputInsidePopup)})},[Y,xe,K,q,an,sn,D,Mn,Nn,jn,Pn,cn,ln,m,et,Qe,x,S,w,L,A,P,ae,I,z,rt,re,ot,y]);let Fn=T(C,ye.inputRef),In=Z.useMemo(()=>({query:xt,hasItems:it,filteredItems:Nt,flatFilteredItems:Lt}),[xt,it,Nt,Lt]),Ln=Z.useMemo(()=>Array.isArray(zt)?``:U(zt,F),[zt,F]),Rn=tt&&Array.isArray(K)&&K.length>0,zn=tt||m===`none`&&nn?void 0:et,Bn=Z.useMemo(()=>!tt||!Array.isArray(K)||!et?null:K.map(e=>{let t=U(e,F);return(0,Q.jsx)(`input`,{type:`hidden`,form:y,name:et,value:t,disabled:Qe},t)}),[tt,K,y,et,F,Qe]),Vn=(0,Q.jsxs)(Z.Fragment,{children:[e.children,(0,Q.jsx)(`input`,{...ye.getValidationProps(Qe,{onFocus(){if(tn){Zt?.focus();return}(W.current||Zt)?.focus()},onChange(e){if(e.nativeEvent.defaultPrevented||Qe||x)return;let t=e.currentTarget.value,n=t.toLowerCase(),r=X(It,e.nativeEvent),i=()=>Xe.current.findIndex(e=>U(e,F).toLowerCase()===n||Re(e,P).toLowerCase()===n);function a(){if(tt)return;if(m===`none`){mn(t,r);return}let e=i();e===-1&&(e=Xe.current.findIndex((e,t)=>{let r=je.current[t];return r!=null&&r.toLowerCase()===n}));let a=e===-1?void 0:Xe.current[e];a!=null&&gn?.(a,r)}nt&&(dn(),D&&i()===-1&&Y.set(`forceMounted`,!0)),queueMicrotask(a)}}),id:xe&&zn==null?`${xe}-hidden-input`:void 0,form:y,name:zn,autoComplete:ce,disabled:Qe,required:S&&!Rn,readOnly:x,value:Ln,ref:Fn,style:zn?ze:Ve,tabIndex:-1,"aria-hidden":!0,suppressHydrationWarning:!0}),Bn]});return(0,Q.jsx)(Pr.Provider,{value:Y,children:(0,Q.jsx)(Fr.Provider,{value:Cn,children:(0,Q.jsx)(Lr.Provider,{value:it,children:(0,Q.jsx)(Ir.Provider,{value:In,children:(0,Q.jsx)(Rr.Provider,{value:vt,children:Vn})})})})})}var ei={...B,...Et,popupSide:e=>e?{"data-popup-side":e}:null,listEmpty:e=>e?{"data-list-empty":``}:null};function ti(e){let t=G(e,$.mounted),n=G(e,$.popupSide),r=G(e,$.positionerElement);return t&&r?n:null}function ni(){return Vr().filteredItems.length===0}function ri(e){return e===`rtl`?[`ArrowRight`,`ArrowLeft`]:[`ArrowLeft`,`ArrowRight`]}function ii(e,t){let n=e>=t-1?t-2:e;return n>=0?n:void 0}function ai(e,t,n){let r=e.state.listRef.current[t];r&&(e.state.selectionEventRef.current=n,r.click(),e.state.selectionEventRef.current=null)}var oi=Z.forwardRef(function(e,t){let{render:n,className:r,nativeButton:i=!0,disabled:a=!1,id:o,style:c,...l}=e,{state:u,disabled:f,setTouched:p,setFocused:h,validationMode:_,validation:v}=At(),{labelId:y}=kt(),b=zr(),x=G(b,$.selectionMode),S=G(b,$.disabled),C=G(b,$.readOnly),w=G(b,$.required),T=G(b,$.positionerElement),E=G(b,$.listElement),D=G(b,$.popupId),O=G(b,$.triggerProps),k=G(b,$.inputInsidePopup),A=G(b,$.id),j=G(b,$.labelId),ee=G(b,$.open),M=G(b,$.selectedValue),te=G(b,$.activeIndex),N=G(b,$.selectedIndex),P=G(b,$.hasSelectedValue),F=Br(),I=Hr(),L=ve(),ne=f||S||a,re=ni(),ie=ti(b);Ct({id:k?o:void 0});let ae=k?o??A:o,oe=st(y,j),R;ee&&k?R=D??Wr(A):ee&&(R=E?.id);let se=Z.useRef(``);function ce(e){se.current=e.pointerType}let{reference:le}=tt(F,{enabled:!ee&&!C&&!S&&x===`single`,listRef:b.state.labelsRef,activeIndex:te,selectedIndex:N,onMatch(e){let t=b.state.valuesRef.current[e];t!==void 0&&b.state.setSelectedValue(t,X(It))}}),{reference:z}=Pe(F,{enabled:!C&&!S,event:`mousedown`}),{buttonRef:B,getButtonProps:ue}=d({native:i,disabled:ne}),V={...u,open:ee,disabled:ne,popupSide:ie,listEmpty:re,placeholder:x===`none`?!1:!P};return s(`button`,e,{ref:[t,B,g(e=>{b.set(`triggerElement`,e)})],state:V,props:[O,z,le,{id:ae,tabIndex:k?0:-1,role:k?`combobox`:void 0,"aria-expanded":ee,"aria-haspopup":k?`dialog`:`listbox`,"aria-controls":R,"aria-required":k&&w||void 0,"aria-labelledby":oe,onPointerDown:ce,onPointerEnter:ce,onFocus(){h(!0),!(ne||C)&&L.start(0,b.state.forceMount)},onBlur(e){if(!Mt(T,e.relatedTarget)&&(p(!0),h(!1),_===`onBlur`)){let e=x===`none`?I:M;v.commit(e)}},onMouseDown(e){if(ne||C||(k||F.set(`domReferenceElement`,e.currentTarget),b.state.forceMount(),se.current!==`touch`&&(b.state.inputRef.current?.focus(),k||e.preventDefault()),ee))return;let t=m(e.currentTarget);function n(e){let t=b.state.triggerElement;if(!t)return;let n=wt(e),r=b.state.positionerElement,i=b.state.listElement;Mt(t,n)||Mt(r,n)||Mt(i,n)||W(e,t)||b.state.setOpen(!1,X(Gt,e))}k&&t.addEventListener(`mouseup`,n,{once:!0})},onKeyDown(e){C||(e.key===`ArrowDown`||e.key===`ArrowUp`)&&(be(e),b.state.setOpen(!0,X(Lt,e.nativeEvent)),b.state.inputRef.current?.focus())}},v.getValidationProps(ne,l),ue],stateAttributesMapping:ei})}),si=Z.createContext(void 0);function ci(){return Z.useContext(si)}var li=Z.createContext(void 0);function ui(e){let t=Z.useContext(li);if(t===void 0&&!e)throw Error(c(21));return t}var di=Z.forwardRef(function(e,t){let n=zr(),{buttonRef:r,getButtonProps:i}=d({native:!1}),a=T(t,r);function o(e){n.state.setOpen(!1,X(Ut,e.nativeEvent,e.currentTarget))}return(0,Q.jsx)(`span`,{ref:a,...i({onClick:o}),"aria-label":`Dismiss`,tabIndex:void 0,style:ze})}),fi=Z.forwardRef(function(e,t){let{render:n,className:r,disabled:i=!1,id:a,style:o,...c}=e,{state:l,disabled:u,setTouched:d,setFocused:f,validationMode:p,validation:m}=At(),{labelId:h}=kt(),_=ci(),v=!!ui(!0),y=zr(),b=Hr(),x=Ie(),S=G(y,$.required),C=G(y,$.disabled),w=G(y,$.readOnly),T=G(y,$.name),E=G(y,$.form),D=G(y,$.selectionMode),O=G(y,$.autoHighlight),k=G(y,$.inputProps),A=G(y,$.triggerProps),j=G(y,$.open),ee=G(y,$.mounted),M=G(y,$.selectedValue),te=G(y,$.id),N=G(y,$.inline),P=G(y,$.modal),F=!!O,I=ti(y),L=u||C||i,ne=ni(),re=v||N,ie=!re||P,ae=zt(a??(re?void 0:te)),oe=v?Tt:l,[R,ce]=Z.useState(null),le=Z.useRef(!1),z=Z.useRef(null),B=Z.useRef(!1),ue=D===`none`&&!v,V=g(e=>{let t=v||y.state.inline;t&&!y.state.hasInputValue&&y.state.setInputValue(``,X(It)),y.update({inputElement:e,inputInsidePopup:t,inputOwnsFormValue:ue})}),de=v?c:m.getValidationProps(L,c);function H(){y.state.setIndices({activeIndex:null,selectedIndex:null,type:y.state.keyboardActiveRef.current?Vt:Jt})}function fe(){y.state.keyboardActiveRef.current=!1}let pe={...oe,open:j,disabled:L,readOnly:w,popupSide:I,listEmpty:ne};function he(e){if(!_)return;let t,{highlightedChipIndex:n}=_,r=_.chipsRef.current.length,[i,a]=ri(x);return n===void 0?(e.key===i&&(e.currentTarget.selectionStart??0)===0&&M.length>0&&(e.preventDefault(),t=r>0?r-1:void 0),t):(e.key===i?(e.preventDefault(),t=n>0?n-1:void 0):e.key===a?(e.preventDefault(),t=n0){let t=_.chipsRef.current.length,n=t>0?t-1:M.length-1,r=M.filter((e,t)=>t!==n);H(),y.state.setSelectedValue(r,X(It,e.nativeEvent));return}let i=_?.highlightedChipIndex!==void 0,a=he(e);if(_?.setHighlightedChipIndex(a),a===void 0?i&&y.state.inputRef.current?.focus():_?.chipsRef.current[a]?.focus(),e.which!==229&&e.key===`Enter`&&j){let t=y.state.activeIndex,n=e.nativeEvent;if(t===null){if(N)return;y.state.setOpen(!1,X(It,n));return}be(e),ai(y,t,n)}},onPointerMove:fe,onPointerDown:fe},de],stateAttributesMapping:ei}),_e=v?(0,Q.jsx)(Ot.Provider,{value:Dt,children:ge}):ge;return(0,Q.jsxs)(Z.Fragment,{children:[j&&ie&&(0,Q.jsx)(di,{ref:y.state.startDismissRef}),_e]})});function pi(e,t,n,r,i){if(e.baseUIHandlerPrevented||r)return;let a=wt(e.nativeEvent),o=b(a)?a:null;o!==e.currentTarget&&(i?.(o)||le(o))||(e.preventDefault(),!n&&(t.state.inputRef.current?.focus(),t.state.openOnInputClick&&t.state.setOpen(!0,X(Ft,e.nativeEvent))))}var mi={...je,...H},hi=Z.forwardRef(function(e,t){let{render:n,className:r,disabled:i=!1,nativeButton:a=!0,keepMounted:o=!1,style:c,...l}=e,{disabled:u}=At(),f=zr(),p=G(f,$.selectionMode),m=G(f,$.disabled),h=G(f,$.readOnly),g=G(f,$.open),_=G(f,$.selectedValue),v=G(f,$.hasSelectionChips),y=Hr(),b=!1;b=p===`none`?y!==``:p===`single`?_!=null:v;let x=u||m||i,{buttonRef:S,getButtonProps:C}=d({native:a,disabled:x}),{mounted:w,transitionStatus:T,setMounted:E}=gt(b),D={disabled:x,visible:b,open:g,transitionStatus:T};mt({open:b,ref:f.state.clearRef,onComplete(){b||E(!1)}});let O=s(`button`,e,{state:D,ref:[t,S,f.state.clearRef],props:[{tabIndex:-1,children:`x`,onMouseDown(e){e.preventDefault()},onClick(e){if(x||h)return;let t=f.state.keyboardActiveRef.current?Vt:Jt;f.state.setInputValue(``,X(Nt,e.nativeEvent)),p===`none`?f.state.setIndices({activeIndex:null,type:t}):(f.state.setSelectedValue(Array.isArray(_)?[]:null,X(Nt,e.nativeEvent)),f.state.setIndices({activeIndex:null,selectedIndex:null,type:t})),f.state.inputRef.current?.focus()}},l,C],stateAttributesMapping:mi});return o||w?O:null}),gi=Z.createContext(null);function _i(){return Z.useContext(gi)}function vi(e){let{children:t}=e,{filteredItems:n}=Vr(),r=_i(),i=r?r.items:n;return(0,Q.jsx)(Z.Fragment,{children:i.map(t)})}var yi=Z.forwardRef(function(e,t){var n;let{render:r,className:i,style:a,children:o,...c}=e,l=zr(),u=Br(),d=!!ui(!0),{filteredItems:f,hasItems:p}=Vr(),m=G(l,$.selectionMode),h=G(l,$.grid),_=G(l,$.listProps),v=G(l,$.virtualized),y=G(l,$.forceMounted),b=m===`multiple`,x=f.length===0,S=g(e=>{l.set(`positionerElement`,e)}),C=g(e=>{l.set(`listElement`,e)}),w=Z.useMemo(()=>typeof o==`function`?n||=(0,Q.jsx)(vi,{children:o}):o,[o]),T={empty:x},E=u.useState(`floatingId`),D=s(`div`,e,{state:T,ref:[t,C,d?null:S],props:[_,{children:w,tabIndex:-1,id:E,role:h?`grid`:`listbox`,"aria-multiselectable":b?`true`:void 0,onKeyDown(e){if(!(l.state.disabled||l.state.readOnly)&&e.key===`Enter`){let t=l.state.activeIndex;if(t==null)return;be(e),ai(l,t,e.nativeEvent)}},onKeyDownCapture(){l.state.keyboardActiveRef.current=!0},onPointerMoveCapture(){l.state.keyboardActiveRef.current=!1}},c]});if(v)return D;let O=p&&!y?void 0:l.state.labelsRef;return(0,Q.jsx)(ce,{elementsRef:l.state.listRef,labelsRef:O,children:D})}),bi=`⁠`;function xi(e){let t=e.ownerDocument.createTreeWalker(e,NodeFilter.SHOW_TEXT),n=null;for(;t.nextNode();){let e=t.currentNode;e.nodeValue!==``&&(n=e)}return n}function Si(){let e=ve(),t=Z.useRef(null);return Z.useEffect(()=>{if(Ge)return;let n=t.current;if(n==null)return;let r=xi(n);if(r==null)return;let i=r.data,a=`${i}${bi}`;return r.nodeValue=a,e.start(200,()=>{r.nodeValue===a&&(r.nodeValue=i)}),()=>{e.clear(),r.nodeValue===a&&(r.nodeValue=i)}},[t,e]),t}var Ci=Z.createContext(void 0);function wi(){let e=Z.useContext(Ci);if(e===void 0)throw Error(c(20));return e}var Ti=Z.forwardRef(function(e,t){let{keepMounted:n=!1,...r}=e,i=zr(),a=G(i,$.mounted),o=G(i,$.forceMounted);return a||n||o?(0,Q.jsx)(Ci.Provider,{value:n,children:(0,Q.jsx)(et,{ref:t,...r})}):null}),Ei=Z.forwardRef(function(e,t){let{render:n,className:r,anchor:i,positionMethod:a,side:o,align:s,sideOffset:c,alignOffset:l,collisionBoundary:d=`clipping-ancestors`,collisionPadding:f,arrowPadding:p,sticky:m,disableAnchorTracking:h=!1,collisionAvoidance:_=Je,style:v,...y}=e,b=zr(),x=Br(),S=wi(),C=G(b,$.modal),w=G(b,$.open),T=G(b,$.mounted),E=G(b,$.openMethod),D=G(b,$.positionerElement),O=G(b,$.triggerElement),k=G(b,$.inputElement),A=G(b,$.inputGroupElement),j=G(b,$.inputInsidePopup),ee=G(b,$.transitionStatus),M=ni(),te=Ke({anchor:i??(j?O:A??k),floatingRootContext:x,positionMethod:a,mounted:T,side:o,sideOffset:c,align:s,alignOffset:l,arrowPadding:p,collisionBoundary:d,collisionPadding:f,sticky:m,disableAnchorTracking:h,keepMounted:S,collisionAvoidance:_,lazyFlip:!0});Ze(w&&C,E===`touch`,D,O);let N={open:w,side:te.side,align:te.align,anchorHidden:te.anchorHidden,empty:M};u(()=>{b.set(`popupSide`,te.side)},[b,te.side]);let P=g(e=>{b.set(`positionerElement`,e)}),F=it(e,N,{styles:te.positionerStyles,transitionStatus:ee,props:y,refs:[t,P],hidden:!T,inert:!w});return(0,Q.jsxs)(li.Provider,{value:te,children:[T&&C&&(0,Q.jsx)(ae,{inert:Se(!w),cutout:A??k??O}),F]})}),Di={...xe,...je},Oi=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,initialFocus:a,finalFocus:o,...c}=e,l=zr(),d=ui(),f=Br(),p=G(l,$.mounted),m=G(l,$.open),h=G(l,$.openMethod),g=G(l,$.popupProps),_=G(l,$.transitionStatus),v=G(l,$.inputInsidePopup),y=G(l,$.inputElement),b=G(l,$.modal),x=G(l,$.id),S=ni(),C=c.id??(v?Wr(x):void 0);u(()=>(l.set(`popupId`,l.state.popupRef.current?.id||C),()=>{l.set(`popupId`,void 0)}),[l,C]),mt({open:m,ref:l.state.popupRef,onComplete(){m&&l.state.onOpenChangeComplete(!0)}});let w=s(`div`,e,{state:{open:m,side:d.side,align:d.align,anchorHidden:d.anchorHidden,transitionStatus:_,empty:S},ref:[t,l.state.popupRef],props:[g,{id:C,role:v?`dialog`:`presentation`,onFocus(e){let t=wt(e.nativeEvent);h!==`touch`&&(Mt(l.state.listElement,t)||t===e.currentTarget)&&l.state.inputRef.current?.focus()}},Me(_),c],stateAttributesMapping:Di}),T=a===void 0?v?e=>e===`touch`?l.state.popupRef.current:y:!1:a,E;E=o??(v?void 0:!1);let D=!v||b;return(0,Q.jsx)(Le,{context:f,disabled:!p,modal:D,openInteractionType:h,initialFocus:T,returnFocus:E,getInsideElements:()=>[l.state.startDismissRef.current,l.state.endDismissRef.current],children:(0,Q.jsxs)(Z.Fragment,{children:[w,D&&(0,Q.jsx)(di,{ref:l.state.endDismissRef})]})})}),ki=Z.createContext(void 0);function Ai(){let e=Z.useContext(ki);if(!e)throw Error(c(19));return e}var ji=Z.createContext(!1);function Mi(){return Z.useContext(ji)}var Ni=e(i(),1);function Pi(e){let{componentProps:t,forwardedRef:n,virtualized:r,indexFromFilter:i}=e,{render:a,className:o,style:c,value:l=null,index:f,disabled:p=!1,nativeButton:m=!1,...h}=t,g=Z.useRef(null),_=ye({guess:!0,index:f,textRef:g}),v=zr(),y=Mi(),b=Ur(),x=G(v,$.selectionMode),S=G(v,$.disabled),C=G(v,$.readOnly),w=G(v,$.isItemEqualToValue),T=S||p,E=x!==`none`,D=f??i??_.index,O=D!==-1,k=G(v,$.id),A=G(v,$.isActive,D),j=G(v,$.isSelected,l),ee=G(v,$.itemProps),M=Z.useRef(null),te=k!=null&&O?`${k}-${D}`:void 0,N=j&&E;u(()=>{if(!(O&&(r||f!=null)))return;let e=v.state.listRef.current;return e[D]=M.current,()=>{delete e[D]}},[O,r,D,f,v]),u(()=>{if(!O||b)return;let e=v.state.valuesRef.current;return e[D]=l,()=>{delete e[D]}},[O,b,D,l,v]),u(()=>{if(!O||b)return;let e=v.state.selectedValue;lt(l,Array.isArray(e)?e[e.length-1]:e,w)&&v.set(`selectedIndex`,D)},[O,b,v,D,l,w]);let{getButtonProps:P,buttonRef:F}=d({disabled:T,focusableWhenDisabled:!0,native:m,composite:!0}),I={disabled:T,selected:N,highlighted:A};function L(e){function t(){v.state.handleSelection(e,l)}v.state.submitOnItemClick?(Ni.flushSync(t),v.state.requestSubmit()):t()}let ne={id:te,role:y?`gridcell`:`option`,"aria-selected":E?N:void 0,tabIndex:void 0,onPointerDownCapture(e){e.isPrimary&&(v.state.pointerDownItemRef.current=e.currentTarget),e.preventDefault()},onMouseDown(e){e.preventDefault()},onClick(e){T||C||L(e.nativeEvent)},onMouseUp(e){let t=v.state.pointerDownItemRef.current===e.currentTarget;v.state.pointerDownItemRef.current=null,!(T||C||e.button!==0||t||!A)&&L(e.nativeEvent)}},re=s(`div`,t,{ref:[F,n,_.ref,M],state:I,props:[ee,ne,h,P]}),ie=Z.useMemo(()=>({selected:N,textRef:g}),[N,g]);return(0,Q.jsx)(ki.Provider,{value:ie,children:re})}function Fi(e){let{componentProps:t,forwardedRef:n}=e,r=G(zr(),$.isItemEqualToValue),{flatFilteredItems:i}=Vr();return(0,Q.jsx)(Pi,{componentProps:t,forwardedRef:n,virtualized:!0,indexFromFilter:Ae(i,t.value??null,r)})}var Ii=Z.memo(Z.forwardRef(function(e,t){let n=G(zr(),$.virtualized);return n&&e.index==null?(0,Q.jsx)(Fi,{componentProps:e,forwardedRef:t}):(0,Q.jsx)(Pi,{componentProps:e,forwardedRef:t,virtualized:n,indexFromFilter:void 0})})),Li=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,children:a,...o}=e,{filteredItems:c}=Vr(),l=zr(),u=Si(),d=c.length===0?a:null;return s(`div`,e,{ref:[t,l.state.emptyRef,u],props:[{children:d,role:`status`,"aria-live":`polite`,"aria-atomic":!0},o]})});function Ri(e){let{multiple:t=!1,defaultValue:n,value:r,onValueChange:i,autoComplete:a,...o}=e;return(0,Q.jsx)($r,{...o,selectionMode:t?`multiple`:`single`,selectedValue:r,defaultSelectedValue:n,onSelectedValueChange:i,formAutoComplete:a})}var zi=Z.forwardRef(function(e,t){let{selected:n}=Ai();return e.keepMounted||n?(0,Q.jsx)(Bi,{...e,ref:t}):null}),Bi=Z.memo(Z.forwardRef((e,t)=>{let{render:n,className:r,style:i,keepMounted:a,...o}=e,{selected:c}=Ai(),l=Z.useRef(null),{transitionStatus:u,setMounted:d}=gt(c),f=s(`span`,e,{ref:[t,l],state:{selected:c,transitionStatus:u},props:[{"aria-hidden":!0,children:`✔️`},o],stateAttributesMapping:je});return mt({open:c,ref:l,onComplete(){c||d(!1)}}),f})),Vi=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,...a}=e,o=zr(),c=G(o,$.open),l=G(o,$.hasSelectionChips),[u,d]=Z.useState(void 0);c&&u!==void 0&&d(void 0);let f=Z.useRef([]),m=s(`div`,e,{ref:[t,o.state.chipsContainerRef],props:[l?{role:`toolbar`}:p,{onMouseDown(e){pi(e,o,o.state.disabled,o.state.readOnly)}},a]}),h=Z.useMemo(()=>({highlightedChipIndex:u,setHighlightedChipIndex:d,chipsRef:f}),[u,d,f]);return(0,Q.jsx)(si.Provider,{value:h,children:(0,Q.jsx)(ce,{elementsRef:f,children:m})})}),Hi=Z.createContext(void 0);function Ui(){let e=Z.useContext(Hi);if(!e)throw Error(c(17));return e}var Wi=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,...a}=e,o=zr(),{setHighlightedChipIndex:c,chipsRef:l}=ci(),u=Ie(),d=G(o,$.disabled),f=G(o,$.readOnly),p=G(o,$.selectedValue),{ref:m,index:h}=ye();function g(e){let t=h,[n,r]=ri(u);return e.key===n?(e.preventDefault(),t=h>0?h-1:void 0):e.key===r?(e.preventDefault(),t=ht!==h),X(It,e.nativeEvent))):e.key===`Enter`||e.key===` `?(be(e),t=void 0):e.key===`ArrowDown`||e.key===`ArrowUp`?(be(e),o.state.setOpen(!0,X(Lt,e.nativeEvent)),t=void 0):e.key.length===1&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&(t=void 0),t}let _=s(`div`,e,{ref:[t,m],state:{disabled:d},props:[{tabIndex:-1,"aria-disabled":d||void 0,"aria-readonly":f||void 0,onKeyDown(e){if(d||f)return;let t=g(e);Ni.flushSync(()=>{c(t)}),t===void 0?o.state.inputRef.current?.focus():l.current[t]?.focus()}},a]}),v=Z.useMemo(()=>({index:h}),[h]);return(0,Q.jsx)(Hi.Provider,{value:v,children:_})}),Gi=Z.forwardRef(function(e,t){let{render:n,className:r,disabled:i=!1,nativeButton:a=!0,style:o,...c}=e,l=zr(),{index:u}=Ui(),f=G(l,$.disabled),p=G(l,$.readOnly),m=G(l,$.selectedValue),h=G(l,$.isItemEqualToValue),g=f||i,{buttonRef:_,getButtonProps:v}=d({native:a,disabled:g||p,focusableWhenDisabled:!0}),y={disabled:g};function b(e){let t=l.state.activeIndex;if(t==null)return;let n=Ae(l.state.valuesRef.current,e,h);n!==-1&&t===n&&l.state.setIndices({activeIndex:null,type:l.state.keyboardActiveRef.current?Vt:Jt})}function x(e){let t=X(Y,e.nativeEvent),n=m[u];return b(n),l.state.setSelectedValue(m.filter((e,t)=>t!==u),t),l.state.inputRef.current?.focus(),t}return s(`button`,e,{ref:[t,_],state:y,props:[{tabIndex:-1,onMouseDown(e){e.preventDefault()},onClick(e){x(e).isPropagationAllowed||e.stopPropagation()},onKeyDown(e){(e.key===`Enter`||e.key===` `)&&(x(e).isPropagationAllowed||be(e))}},c,v]})});function Ki({className:e,...t}){return(0,Q.jsx)(`div`,{"data-slot":`input-group`,role:`group`,className:I(`group/input-group relative flex h-9 w-full min-w-0 items-center rounded-4xl border border-input bg-input/30 transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-2xl has-data-[align=block-start]:rounded-2xl has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-[3px] has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[textarea]:rounded-xl has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5`,e),...t})}var qi=_(`flex h-auto cursor-text items-center justify-center gap-2 py-2 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 **:data-[slot=kbd]:rounded-4xl **:data-[slot=kbd]:bg-muted-foreground/10 **:data-[slot=kbd]:px-1.5 [&>svg:not([class*='size-'])]:size-4`,{variants:{align:{"inline-start":`order-first pl-3 has-[>button]:-ml-1 has-[>kbd]:ml-[-0.15rem]`,"inline-end":`order-last pr-3 has-[>button]:-mr-1 has-[>kbd]:mr-[-0.15rem]`,"block-start":`order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-3 [.border-b]:pb-3`,"block-end":`order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-3 [.border-t]:pt-3`}},defaultVariants:{align:`inline-start`}});function Ji({className:e,align:t=`inline-start`,...n}){return(0,Q.jsx)(`div`,{role:`group`,"data-slot":`input-group-addon`,"data-align":t,className:I(qi({align:t}),e),onClick:e=>{e.target.closest(`button`)||e.currentTarget.parentElement?.querySelector(`input`)?.focus()},...n})}var Yi=_(`flex items-center gap-2 rounded-4xl text-sm shadow-none`,{variants:{size:{xs:`h-6 gap-1 px-1.5 [&>svg:not([class*='size-'])]:size-3.5`,sm:``,"icon-xs":`size-6 p-0 has-[>svg]:p-0`,"icon-sm":`size-8 p-0 has-[>svg]:p-0`}},defaultVariants:{size:`xs`}});function Xi({className:e,type:t=`button`,variant:n=`ghost`,size:r=`xs`,...i}){return(0,Q.jsx)(a,{type:t,"data-size":r,variant:n,className:I(Yi({size:r}),e),...i})}function Zi({className:e,...t}){return(0,Q.jsx)(xt,{"data-slot":`input-group-control`,className:I(`flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent`,e),...t})}var Qi=Ri;function $i({className:e,children:t,...n}){return(0,Q.jsxs)(oi,{"data-slot":`combobox-trigger`,className:I(`[&_svg:not([class*='size-'])]:size-4`,e),...n,children:[t,(0,Q.jsx)(We,{className:`pointer-events-none size-4 text-muted-foreground`})]})}function ea({className:e,...t}){return(0,Q.jsx)(hi,{"data-slot":`combobox-clear`,render:(0,Q.jsx)(Xi,{variant:`ghost`,size:`icon-xs`}),className:I(e),...t,children:(0,Q.jsx)(an,{className:`pointer-events-none`})})}function ta({className:e,children:t,disabled:n=!1,showTrigger:r=!0,showClear:i=!1,...a}){return(0,Q.jsxs)(Ki,{className:I(`w-auto`,e),children:[(0,Q.jsx)(fi,{render:(0,Q.jsx)(Zi,{disabled:n}),...a}),(0,Q.jsxs)(Ji,{align:`inline-end`,children:[r&&(0,Q.jsx)(Xi,{size:`icon-xs`,variant:`ghost`,render:(0,Q.jsx)($i,{}),"data-slot":`input-group-button`,className:`group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent`,disabled:n}),i&&(0,Q.jsx)(ea,{disabled:n})]}),t]})}function na({className:e,side:t=`bottom`,sideOffset:n=6,align:r=`start`,alignOffset:i=0,anchor:a,...o}){return(0,Q.jsx)(Ti,{children:(0,Q.jsx)(Ei,{side:t,sideOffset:n,align:r,alignOffset:i,anchor:a,className:`isolate z-50`,children:(0,Q.jsx)(Oi,{"data-slot":`combobox-content`,"data-chips":!!a,className:I(`group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-2xl bg-popover text-popover-foreground shadow-2xl ring-1 ring-foreground/5 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-9 *:data-[slot=input-group]:border-none *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...o})})})}function ra({className:e,...t}){return(0,Q.jsx)(yi,{"data-slot":`combobox-list`,className:I(`no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0`,e),...t})}function ia({className:e,children:t,...n}){return(0,Q.jsxs)(Ii,{"data-slot":`combobox-item`,className:I(`relative flex w-full cursor-default items-center gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,e),...n,children:[t,(0,Q.jsx)(zi,{render:(0,Q.jsx)(`span`,{className:`pointer-events-none absolute right-2 flex size-4 items-center justify-center`}),children:(0,Q.jsx)(fe,{className:`pointer-events-none`})})]})}function aa({className:e,...t}){return(0,Q.jsx)(Li,{"data-slot":`combobox-empty`,className:I(`hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex`,e),...t})}function oa({className:e,...t}){return(0,Q.jsx)(Vi,{"data-slot":`combobox-chips`,className:I(`flex min-h-9 flex-wrap items-center gap-1.5 rounded-4xl border border-input bg-input/30 bg-clip-padding px-2.5 py-1.5 text-sm transition-colors focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-[3px] has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1.5 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40`,e),...t})}function sa({className:e,children:t,showRemove:n=!0,removeLabel:r,...i}){return(0,Q.jsxs)(Wi,{"data-slot":`combobox-chip`,className:I(`flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-4xl bg-muted-foreground/10 px-2 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0`,e),...i,children:[t,n&&(0,Q.jsx)(Gi,{render:(0,Q.jsx)(a,{variant:`ghost`,size:`icon-xs`}),className:`-ml-1 text-muted-foreground hover:text-foreground`,"aria-label":r,"data-slot":`combobox-chip-remove`,children:(0,Q.jsx)(an,{className:`pointer-events-none`})})]})}function ca({className:e,...t}){return(0,Q.jsx)(fi,{"data-slot":`combobox-chip-input`,className:I(`min-w-16 flex-1 outline-none`,e),...t})}function la(){return Z.useRef(null)}function ua(e){return e.kind===`create`?e.name:e.tag.name}var da=new Set([`escape-key`,`outside-press`,`focus-out`,`input-blur`]);function fa({entry:e,vocabulary:t,onArtist:n,pendingNames:r,onCommit:i,onClose:a}){let[o,s]=(0,Z.useState)(``),[c,l]=(0,Z.useState)(!0),[u,d]=(0,Z.useState)(``),f=(0,Z.useId)(),p=(0,Z.useRef)(null),m=yt({query:o,vocabulary:t,onArtist:n,pendingNames:r}),h=tr(o),g=h>32,_=m.state===`items`?m.items.filter(e=>!(g&&e.kind===`create`)):[];function v(e){let t=ua(e);i(t),p.current=t,s(``)}function y(e){if(p.current!==null){let t=p.current;if(p.current=null,e===t)return}let t=tr(o),n=tr(e);t<32&&n>=32?d(`Tag name limit reached — 32 characters.`):t<25&&n>=25&&d(`7 characters left.`),s(e)}return(0,Q.jsxs)(Qi,{items:_,value:null,onValueChange:e=>{e&&v(e)},inputValue:o,onInputValueChange:y,itemToStringLabel:ua,autoHighlight:!0,open:c,onOpenChange:(e,t)=>{if(e){l(!0);return}da.has(t.reason)&&(l(!1),a(t.reason===`escape-key`?`escape`:`blur`))},children:[(0,Q.jsxs)(`div`,{className:`flex items-center gap-1`,children:[(0,Q.jsx)(ta,{placeholder:`Tag name`,"aria-label":`Add tag to ${e.name}`,"aria-describedby":f,className:`h-8 w-full max-w-60 text-label`,showTrigger:!1,autoFocus:!0}),h>=25&&(0,Q.jsxs)(`span`,{id:f,className:`text-label tabular-nums ${g?`text-destructive`:h>=32?`text-foreground`:`text-muted-foreground`}`,children:[h,`/`,32]})]}),(0,Q.jsx)(`span`,{"aria-live":`polite`,className:`sr-only`,children:u}),(0,Q.jsxs)(na,{children:[(0,Q.jsx)(ra,{children:_.map(e=>(0,Q.jsx)(ia,{value:e,children:e.kind===`create`?(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(nn,{"aria-hidden":`true`,className:`size-3`}),`Create “`,e.name,`”`]}):e.tag.name},e.kind===`create`?`__create__`:e.tag.id))}),(0,Q.jsx)(aa,{children:m.state===`already-on`?`“${m.name}” is already on this artist`:g?`Tags can be at most 32 characters.`:`Type a name to create a tag`})]})]})}var pa=0;function ma(){return pa+=1,`pending-${pa}`}function ha({entry:e,actions:t,announce:n}){let{vocabulary:r,ensureLoaded:i,remember:o}=cr(),s=(0,Z.useRef)(null),c=(0,Z.useRef)(null),l=(0,Z.useRef)(null),u=(0,Z.useRef)(e.tags.length),[d,f]=(0,Z.useState)([]),[p,m]=(0,Z.useState)(!1),h=e.tags.length+d.length,g=h>=10;(0,Z.useLayoutEffect)(()=>{let t=e.tags.length!==u.current;u.current=e.tags.length;let n=l.current;if(!t||!n||n.kind===`add-button`)return;l.current=null;let r=n.kind===`grow`?e.tags.length-1:n.index;(s.current?.querySelectorAll(`[data-chip-remove]`))?.[r]?.focus()},[e.tags]),(0,Z.useLayoutEffect)(()=>{l.current?.kind!==`add-button`||!c.current||(l.current=null,c.current.focus())},[d,p,e.tags]);function _(r,i){let a=e.tags.length===10,o=e.tags.length-1;l.current=o===0?{kind:`add-button`}:{kind:`chip`,index:Math.min(i,o-1)},t.removeTag(e.id,r.id),n(a?`Removed “${r.name}” from ${e.name}. You can add tags again.`:`Removed “${r.name}” from ${e.name}.`),te(e.id,r.id).catch(()=>{l.current=null,t.addTag(e.id,r,i),J.error(`Couldn't remove “${r.name}” from ${e.name} — try again.`)})}function v(){i(),m(!0)}function y(t,n){if(t instanceof P){if(t.code===`tag_cap_reached`)return`${e.name} already has 10 tags — remove one first.`;if(t.code===`tag_name_too_long`)return`Tags can be at most 32 characters.`}return`Couldn't add “${n}” to ${e.name} — try again.`}function b(r){let i=h+1>=10,a=ma();f(e=>[...e,{tempId:a,name:r}]),i&&(m(!1),l.current={kind:`grow`}),M(e.id,r).then(r=>{f(e=>e.filter(e=>e.tempId!==a)),t.addTag(e.id,r),o(r),n(i?`Added “${r.name}” to ${e.name}. Max 10 tags reached.`:`Added “${r.name}” to ${e.name}.`)}).catch(e=>{f(e=>e.filter(e=>e.tempId!==a)),i&&(l.current={kind:`add-button`}),J.error(y(e,r))})}return(0,Q.jsxs)(`div`,{ref:s,className:`mt-1 flex flex-wrap items-center gap-2`,children:[e.tags.map((n,r)=>{let i=t.filterTagIds.has(n.id);return(0,Q.jsxs)(Zt,{variant:`secondary`,className:I(`group/chip h-6 max-w-full gap-1 pr-0 pl-2 text-label font-normal focus-within:h-auto focus-within:min-h-6 focus-within:py-0.5`,i&&`ring-1 ring-foreground/30 ring-inset`),children:[(0,Q.jsxs)(`button`,{type:`button`,"aria-pressed":i,"aria-label":`Filter by ${n.name}`,className:`-ml-2 flex h-6 min-w-0 cursor-pointer items-center gap-1 rounded-4xl pr-1 pl-2 hover:bg-foreground/10 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none`,onClick:()=>t.toggleFilterTag(n),children:[i&&(0,Q.jsx)(fe,{"aria-hidden":`true`,className:`size-3 shrink-0`}),(0,Q.jsx)(`span`,{className:`min-w-0 truncate group-focus-within/chip:break-all group-focus-within/chip:whitespace-normal`,title:n.name,children:n.name})]}),(0,Q.jsx)(a,{variant:`ghost`,size:`icon-xs`,className:`-ml-1 text-muted-foreground hover:text-foreground`,"data-chip-remove":!0,"aria-label":`Remove tag ${n.name} from ${e.name}`,onClick:()=>_(n,r),children:(0,Q.jsx)(an,{"aria-hidden":`true`,className:`size-3`})})]},n.id)}),d.map(t=>(0,Q.jsxs)(Zt,{variant:`secondary`,className:`group/chip h-6 max-w-full gap-1 pr-0 pl-2 text-label font-normal focus-within:h-auto focus-within:min-h-6 focus-within:py-0.5`,children:[(0,Q.jsx)(`span`,{className:`min-w-0 truncate group-focus-within/chip:break-all group-focus-within/chip:whitespace-normal`,title:t.name,children:t.name}),(0,Q.jsx)(a,{variant:`ghost`,size:`icon-xs`,className:`-ml-1 text-muted-foreground hover:text-foreground`,"data-chip-remove":!0,"aria-label":`Remove tag ${t.name} from ${e.name}`,"aria-disabled":`true`,children:(0,Q.jsx)(an,{"aria-hidden":`true`,className:`size-3`})})]},t.tempId)),p?(0,Q.jsx)(fa,{entry:e,vocabulary:r,onArtist:e.tags,pendingNames:d.map(e=>e.name),onCommit:b,onClose:e=>{m(!1),e===`escape`&&(l.current={kind:`add-button`})}}):g?(0,Q.jsxs)(`span`,{className:`text-label text-muted-foreground`,children:[`max `,10,` tags`]}):(0,Q.jsxs)(a,{ref:c,variant:`ghost`,size:`xs`,className:`h-6 text-label text-muted-foreground hover:text-foreground`,"aria-label":`Add tag to ${e.name}`,onClick:v,children:[(0,Q.jsx)(nn,{"aria-hidden":`true`,className:`size-3`}),`tag`]})]})}function ga({entry:e,onEntryChange:t,onRemove:n,tagActions:r,announce:i}){return(0,Q.jsxs)(`li`,{className:`flex flex-col gap-4 rounded-md bg-card p-4 sm:flex-row sm:items-center`,children:[(0,Q.jsx)(vt,{src:e.image_url,alt:e.name,size:64}),(0,Q.jsxs)(`div`,{className:`flex min-w-0 flex-1 flex-col gap-1`,children:[(0,Q.jsx)(`span`,{className:`truncate text-heading font-semibold text-foreground`,title:e.name,children:e.name}),e.disambiguation!==null&&(0,Q.jsx)(`span`,{className:`truncate text-label text-muted-foreground`,children:e.disambiguation}),(0,Q.jsxs)(`p`,{className:`flex flex-wrap gap-x-2 text-label text-muted-foreground`,children:[(0,Q.jsx)(`span`,{children:e.latest_release_date===null?`No releases yet`:`Latest release: ${pe(e.latest_release_date)}`}),e.next_release_date!==null&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(`span`,{"aria-hidden":`true`,children:`·`}),(0,Q.jsxs)(`span`,{children:[`Upcoming: `,pe(e.next_release_date)]})]})]}),(0,Q.jsx)(ha,{entry:e,actions:r,announce:i}),(0,Q.jsx)(xr,{entry:e,onEntryChange:t,announce:i})]}),(0,Q.jsx)(Mr,{entry:e,onEntryChange:t}),(0,Q.jsx)(a,{variant:`ghost`,size:`icon`,className:`h-11 w-11 shrink-0 text-muted-foreground hover:text-destructive`,"aria-label":`Remove ${e.name} from watchlist`,onClick:()=>n(e),children:(0,Q.jsx)(an,{"aria-hidden":`true`})})]})}function _a({options:e,selected:t,onChange:n}){let r=la(),[i,a]=(0,Z.useState)(``);return(0,Q.jsxs)(Qi,{multiple:!0,items:e,value:t,onValueChange:(e,t)=>{if(t.reason===`escape-key`){t.cancel();return}n(e.map(e=>e.id).sort((e,t)=>e-t))},onOpenChange:(e,t)=>{!e&&t.reason===`item-press`&&t.cancel()},onInputValueChange:a,isItemEqualToValue:(e,t)=>e.id===t.id,itemToStringLabel:e=>e.name,children:[(0,Q.jsxs)(oa,{ref:r,children:[t.map(e=>(0,Q.jsx)(sa,{className:`h-6 max-w-full bg-secondary text-label font-normal text-secondary-foreground`,removeLabel:`Remove ${e.name} from tag filter`,children:(0,Q.jsx)(`span`,{className:`min-w-0 truncate`,title:e.name,children:e.name})},e.id)),(0,Q.jsx)(ca,{"aria-labelledby":`watchlist-tags-label`,"aria-label":`Filter by tags, any of`,placeholder:t.length===0?`All tags`:``})]}),(0,Q.jsxs)(na,{anchor:r,children:[(0,Q.jsx)(aa,{children:e.length===0?`No tags yet — add one with “+ tag” on any artist's card.`:`No tags match “${i}”`}),(0,Q.jsx)(ra,{children:e=>(0,Q.jsxs)(ia,{value:e,children:[e.name,(0,Q.jsx)(`span`,{className:`ml-auto text-muted-foreground tabular-nums`,children:e.count})]},e.id)})]})]})}var va={$:`s`,ø:`o`,æ:`ae`,ß:`ss`};function ya(e){return e.normalize(`NFD`).replace(/\p{M}/gu,``).toLowerCase().replace(/[$øæß]/g,e=>va[e])}function ba(e,t){let n=t.trim();return n===``?!0:ya(e).includes(ya(n))}var xa=`name-asc`,Sa=[{value:`name-asc`,label:`Name (A–Z)`},{value:`name-desc`,label:`Name (Z–A)`},{value:`added-desc`,label:`Date added (newest)`},{value:`added-asc`,label:`Date added (oldest)`},{value:`latest-desc`,label:`Latest release (newest)`},{value:`latest-asc`,label:`Latest release (oldest)`}];function Ca(e){return Sa.some(t=>t.value===e)}var wa=[`album`,`single`,`ep`,`deluxe`],Ta=200;function Ea(e){if(!e)return[];let t=new Set;for(let n of e.split(`,`).slice(0,Ta)){if(!/^[1-9]\d*$/.test(n))continue;let e=Number(n);Number.isSafeInteger(e)&&t.add(e)}return[...t].sort((e,t)=>e-t)}function Da(e){let t=e.get(`sort`);return{q:e.get(`q`)??``,sort:Ca(t)?t:xa,tagIds:Ea(e.get(`tags`)),muted:e.get(`muted`)===`1`,custom:e.get(`custom`)===`1`}}function Oa(e){let t=new URLSearchParams(e),{sort:n,tagIds:r,muted:i,custom:a}=Da(e);return n===`name-asc`?t.delete(`sort`):t.set(`sort`,n),r.length===0?t.delete(`tags`):t.set(`tags`,r.join(`,`)),i?t.set(`muted`,`1`):t.delete(`muted`),a?t.set(`custom`,`1`):t.delete(`custom`),t}var ka=new Intl.Collator(void 0,{sensitivity:`base`}),Aa=(e,t)=>e.artist_id-t.artist_id;function ja(e){return(t,n)=>{let r=t.latest_release_date,i=n.latest_release_date;return r===null&&i===null?Aa(t,n):r===null?1:i===null?-1:r===i?Aa(t,n):(rka.compare(e.name,t.name)||Aa(e,t);case`name-desc`:return(e,t)=>-ka.compare(e.name,t.name)||Aa(e,t);case`added-asc`:return(e,t)=>Date.parse(e.created_at)-Date.parse(t.created_at)||Aa(e,t);case`added-desc`:return(e,t)=>Date.parse(t.created_at)-Date.parse(e.created_at)||Aa(e,t);case`latest-asc`:return ja(1);case`latest-desc`:return ja(-1)}}function Na(e,t){return t.length===0||e.tags.some(e=>t.includes(e.id))}function Pa(e){let t=new Map;for(let n of e)for(let e of n.tags){let n=t.get(e.id);n?n.count+=1:t.set(e.id,{id:e.id,name:e.name,count:1})}return q([...t.values()])}function Fa(e){return e.muted_event_types.length>0}function Ia(e){let t=new Set(e.release_types);return!(t.size===wa.length&&wa.every(e=>t.has(e)))}function La(e,t,n){return e.filter(e=>n.has(e.id)||ba(e.name,t.q)&&Na(e,t.tagIds)&&(!t.muted||Fa(e))&&(!t.custom||Ia(e))).sort(Ma(t.sort))}function Ra(e){return e.q.trim()!==``||e.tagIds.length>0||e.muted||e.custom}var za=[`q`,`tags`,`muted`,`custom`];function Ba(e,t){return`${e} of ${t} artist${t===1?``:`s`}`}function Va({on:e,label:t,onToggle:n}){return(0,Q.jsxs)(a,{variant:e?`secondary`:`outline`,"aria-pressed":e,onClick:()=>n(!e),children:[e&&(0,Q.jsx)(fe,{"aria-hidden":`true`,className:`size-4`}),t]})}function Ha({query:e,onQueryChange:t,filterInputRef:n,showClear:r,onClearFilters:i,sort:o,onSortChange:s,tagOptions:c,selectedTags:l,onSelectedTagsChange:u,muted:d,onMutedChange:f,custom:p,onCustomChange:m}){return(0,Q.jsxs)(`div`,{role:`group`,"aria-label":`Filter and sort artists`,className:`flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-end`,children:[(0,Q.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground sm:w-64`,children:[`Filter watchlist`,(0,Q.jsxs)(Ki,{children:[(0,Q.jsx)(Ji,{children:(0,Q.jsx)($t,{"aria-hidden":`true`})}),(0,Q.jsx)(Zi,{ref:n,type:`text`,placeholder:`Filter by name…`,enterKeyHint:`search`,autoComplete:`off`,spellCheck:!1,value:e,onChange:e=>t(e.target.value),onKeyDown:n=>{n.key===`Escape`&&e!==``&&(n.preventDefault(),t(``))}}),e!==``&&(0,Q.jsx)(Ji,{align:`inline-end`,children:(0,Q.jsx)(Xi,{size:`icon-xs`,"aria-label":`Clear name filter`,onClick:()=>{t(``),n.current?.focus()},children:(0,Q.jsx)(an,{"aria-hidden":`true`})})})]})]}),(0,Q.jsxs)(`div`,{className:`flex flex-col gap-1 sm:w-56`,children:[(0,Q.jsx)(`span`,{id:`watchlist-sort-label`,className:`text-label text-muted-foreground`,children:`Sort by`}),(0,Q.jsxs)(ot,{value:o,onValueChange:e=>s(e),children:[(0,Q.jsx)(He,{"aria-labelledby":`watchlist-sort-label`,className:`w-full`,children:(0,Q.jsx)(Oe,{children:e=>Sa.find(t=>t.value===e)?.label})}),(0,Q.jsx)(Ye,{children:Sa.map(e=>(0,Q.jsx)(nt,{value:e.value,children:e.label},e.value))})]})]}),(0,Q.jsxs)(`div`,{className:`flex flex-col gap-1 sm:max-w-md sm:min-w-60 sm:flex-1`,children:[(0,Q.jsx)(`span`,{id:`watchlist-tags-label`,className:`text-label text-muted-foreground`,children:`Tags: any of`}),(0,Q.jsx)(_a,{options:c,selected:l,onChange:u})]}),(0,Q.jsxs)(`fieldset`,{className:`flex flex-col gap-1`,children:[(0,Q.jsx)(`legend`,{className:`text-label text-muted-foreground`,children:`Show only`}),(0,Q.jsxs)(`div`,{className:`flex flex-wrap gap-2`,children:[(0,Q.jsx)(Va,{on:d,label:`Has muted events`,onToggle:f}),(0,Q.jsx)(Va,{on:p,label:`Custom release types`,onToggle:m})]})]}),r&&(0,Q.jsxs)(a,{variant:`ghost`,className:`h-9 text-muted-foreground hover:text-foreground`,onClick:i,children:[(0,Q.jsx)(Qt,{"aria-hidden":`true`}),`Clear filters`]})]})}var Ua=300,Wa=500,Ga=n(function(){let[e,t]=(0,Z.useState)(null),[n,r]=(0,Z.useState)(!1),[i,o]=(0,Z.useState)(null),[s,c]=(0,Z.useState)(``),[l,u]=(0,Z.useState)(!1),{params:d,write:f}=bt(),p=Da(d),[m,h]=(0,Z.useState)(p.q),g=(0,Z.useRef)(p.q),_=(0,Z.useRef)(null),v=(0,Z.useRef)(null),y=(0,Z.useRef)(null),b=(0,Z.useRef)({n:0,m:0}),[x,C]=(0,Z.useState)(()=>new Set),w=(0,Z.useRef)(new Map),T=(0,Z.useRef)(new Map),E=(0,Z.useRef)(!1);function k(e){C(t=>t.has(e)?t:new Set(t).add(e))}function A(){C(e=>e.size===0?e:new Set)}function j(e){let t=new Set;for(let n of e){let e=T.current,r=e.has(n)?e.get(n):n;r!=null&&w.current.has(r)&&t.add(r)}return[...t].sort((e,t)=>e-t)}function ee(e){f(t=>{e.length===0?t.delete(`tags`):t.set(`tags`,e.join(`,`))},{replace:!0})}function M(){y.current&&clearTimeout(y.current),y.current=setTimeout(()=>{let{n:e,m:t}=b.current;ie(e>0?`Showing ${Ba(e,t)}.`:`No artists match these filters.`)},Wa)}function te(e){h(e),A(),M(),_.current&&clearTimeout(_.current),_.current=setTimeout(()=>{g.current=e,f(t=>{e===``?t.delete(`q`):t.set(`q`,e)},{replace:!0})},Ua)}(0,Z.useEffect)(()=>{p.q!==g.current&&(_.current&&clearTimeout(_.current),g.current=p.q,h(p.q))},[p.q]),(0,Z.useEffect)(()=>{let e=Oa(d);if(E.current){let t=j(Da(e).tagIds);t.length===0?e.delete(`tags`):e.set(`tags`,t.join(`,`))}e.toString()!==d.toString()&&f(t=>{for(let n of[`sort`,`tags`,`muted`,`custom`]){let r=e.get(n);r===null?t.delete(n):t.set(n,r)}},{replace:!0})},[d,f]),(0,Z.useEffect)(()=>()=>{_.current&&clearTimeout(_.current),y.current&&clearTimeout(y.current)},[]);let N=(0,Z.useCallback)(()=>(r(!1),S().then(e=>(t(e),e)).catch(()=>(r(!0),null))),[]);(0,Z.useEffect)(()=>{N()},[N]),(0,Z.useEffect)(()=>{if(!e||E.current)return;E.current=!0;let t=new Set(e.flatMap(e=>e.tags.map(e=>e.id))),n=p.tagIds.filter(e=>t.has(e));n.length!==p.tagIds.length&&ee(n)},[e]);function F(e,n){k(e),t(t=>t&&t.map(t=>t.id===e?{...t,...n}:t))}function I(e,n,r){k(e),t(t=>t&&t.map(t=>{if(t.id!==e||t.tags.some(e=>e.id===n.id))return t;let i=[...t.tags],a=r===void 0?i.length:Math.max(0,Math.min(r,i.length));return i.splice(a,0,n),{...t,tags:i}}))}function L(e,n){k(e),t(t=>t&&t.map(t=>t.id===e?{...t,tags:t.tags.filter(e=>e.id!==n)}:t))}function ne(e,t){f(n=>{let r=Da(n).tagIds;if(!r.includes(e))return;let i=new Set(r.filter(t=>t!==e));t!==null&&i.add(t),i.size===0?n.delete(`tags`):n.set(`tags`,[...i].sort((e,t)=>e-t).join(`,`))},{replace:!0})}function re(e){e.kind===`renamed`?w.current.set(e.tag.id,e.tag.name):e.kind===`deleted`?(T.current.set(e.tagId,null),ne(e.tagId,null)):(T.current.set(e.sourceId,e.target.id),w.current.set(e.target.id,e.target.name),ne(e.sourceId,e.target.id)),t(t=>t&&t.map(t=>{let n=ar(t.tags,e);return n===t.tags?t:{...t,tags:n}}))}function ie(e){c(e)}async function ae(e,t){if(!V(e)){J.error(`Can't add this artist yet -- search MusicBrainz instead.`);return}try{k((await O({mbid:t.id,name:t.name,imageUrl:t.image_url??void 0,disambiguation:t.disambiguation??void 0,deezerId:void 0})).id),N()}catch(e){if(e instanceof P&&e.status===409){let e=(await N())?.find(e=>e.mbid===t.id);e&&k(e.id);return}J.error(`Couldn't add this artist. Try again in a moment.`)}}async function oe(e){t(t=>t&&t.filter(t=>t.id!==e.id));try{await D(e.id)}catch{J.error(`Couldn't remove ${e.name} — it may already be gone.`),N();return}J.success(`Removed ${e.name} from your watchlist.`,{description:`Undo re-adds this artist with default preferences -- its previous release-type and mute settings are not restored.`,duration:5e3,action:{label:`Undo`,onClick:()=>{O({mbid:e.mbid,name:e.name,deezerId:e.deezer_id??void 0,disambiguation:e.disambiguation??void 0,imageUrl:e.image_url??void 0,note:e.note??void 0}).then(e=>(k(e.id),N())).catch(()=>{J.error(`Couldn't restore ${e.name}. Try adding it again.`)})}}})}if(e)for(let t of e)for(let e of t.tags)w.current.set(e.id,e.name);let R=e?Pa(e):[],se=q(p.tagIds.flatMap(e=>{let t=w.current.get(e);return t===void 0?[]:[{id:e,name:t,count:R.find(t=>t.id===e)?.count??0}]})),ce=new Set(p.tagIds),le=e?La(e,{...p,q:m},x):[];b.current={n:le.length,m:e?.length??0};function z(e){f(t=>{e.length===0?t.delete(`tags`):t.set(`tags`,e.join(`,`))}),A(),M()}function B(e,t){f(n=>{t?n.set(e,`1`):n.delete(e)}),A(),M()}function ue(e){w.current.set(e.id,e.name);let t=new Set(p.tagIds);t.delete(e.id)||t.add(e.id),z([...t].sort((e,t)=>e-t))}function de(e){f(t=>{e===`name-asc`?t.delete(`sort`):t.set(`sort`,e)}),A(),y.current&&clearTimeout(y.current);let t=Sa.find(t=>t.value===e).label;ie(`Sorted by ${t}.`)}function H(){_.current&&clearTimeout(_.current),y.current&&clearTimeout(y.current),h(``),g.current=``,f(e=>{for(let t of za)e.delete(t)}),A(),v.current?.focus();let t=e?.length??0;ie(`Filters cleared. Showing ${Ba(t,t)}.`)}return(0,Q.jsx)(sr,{onChange:re,children:(0,Q.jsxs)(`div`,{className:`flex flex-col gap-6 p-8`,children:[(0,Q.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-4`,children:[(0,Q.jsx)(`h1`,{className:`text-display font-semibold text-foreground`,children:`Watchlist`}),(0,Q.jsxs)(a,{variant:`secondary`,onClick:()=>u(!0),children:[(0,Q.jsx)(rn,{"aria-hidden":`true`}),`Manage tags`]})]}),(0,Q.jsx)(`div`,{role:`status`,"aria-atomic":`true`,className:`sr-only`,children:s}),(0,Q.jsx)(dr,{open:l,onOpenChange:u}),(0,Q.jsxs)(`div`,{className:`flex flex-col gap-6`,children:[(0,Q.jsx)(pr,{onResults:o}),i&&(0,Q.jsx)(gr,{response:i,watchlistEntries:e,onAdd:ae})]}),n&&(0,Q.jsx)(Xt,{heading:`Couldn't load your watchlist.`,body:`Please try again.`,action:(0,Q.jsx)(a,{onClick:N,children:`Retry`})}),!n&&e===null&&(0,Q.jsx)(`div`,{className:`flex flex-col gap-4`,children:[0,1,2].map(e=>(0,Q.jsx)(Yt,{className:`h-24 w-full rounded-md`},e))}),!n&&e!==null&&e.length===0&&(0,Q.jsx)(Xt,{heading:`No artists yet`,body:`Search above to add one.`}),!n&&e!==null&&e.length>0&&(0,Q.jsxs)(`section`,{"aria-labelledby":`watchlist-artists-heading`,className:`flex flex-col gap-4`,children:[(0,Q.jsxs)(`div`,{className:`flex flex-wrap items-baseline justify-between gap-2`,children:[(0,Q.jsx)(`h2`,{id:`watchlist-artists-heading`,className:`text-heading font-semibold text-foreground outline-none`,children:`Your artists`}),(0,Q.jsx)(`p`,{className:`text-label text-muted-foreground tabular-nums`,children:Ba(le.length,e.length)})]}),(0,Q.jsx)(Ha,{query:m,onQueryChange:te,filterInputRef:v,showClear:Ra({...p,q:m})&&le.length>0,onClearFilters:H,sort:p.sort,onSortChange:de,tagOptions:R,selectedTags:se,onSelectedTagsChange:z,muted:p.muted,onMutedChange:e=>B(`muted`,e),custom:p.custom,onCustomChange:e=>B(`custom`,e)}),le.length===0?(0,Q.jsx)(Xt,{heading:`No artists match these filters`,body:`Try a different name or fewer filters, or clear them to see all ${e.length} artist${e.length===1?``:`s`}.`,action:(0,Q.jsx)(a,{onClick:H,children:`Clear filters`})}):(0,Q.jsx)(`ul`,{className:`flex flex-col gap-4`,children:le.map(e=>(0,Q.jsx)(ga,{entry:e,onEntryChange:F,onRemove:oe,tagActions:{addTag:I,removeTag:L,filterTagIds:ce,toggleFilterTag:ue},announce:ie},e.id))})]})]})})});export{Ga as default}; \ No newline at end of file diff --git a/internal/webassets/build/client/assets/watchlist-DVr_lpXm.js b/internal/webassets/build/client/assets/watchlist-DVr_lpXm.js new file mode 100644 index 0000000..9be9ecc --- /dev/null +++ b/internal/webassets/build/client/assets/watchlist-DVr_lpXm.js @@ -0,0 +1 @@ +import{N as e,j as t,k as n,t as r}from"./jsx-runtime-D0QO-s14.js";import{t as i}from"./react-dom-B1AcAK82.js";import{C as a,D as o,E as s,F as c,I as l,L as u,M as d,N as f,O as p,P as m,Q as h,R as g,T as _,V as v,Y as y,Z as b,d as x,f as S,g as C,h as w,j as T,k as E,m as D,n as O,o as k,ot as A,p as j,q as M,r as N,s as ee,st as P,t as F,v as I,w as L,y as te,z as ne}from"./api-k_NcBvGr.js";import{$ as re,At as ie,B as ae,C as oe,Ct as R,Dt as se,E as z,Et as ce,F as B,G as V,H as le,I as H,J as ue,K as U,Mt as de,N as fe,Ot as pe,Q as me,R as he,S as ge,St as _e,T as ve,Tt as ye,U as be,V as xe,W as Se,X as Ce,Y as we,Z as Te,_ as W,_t as Ee,a as De,at as Oe,b as ke,bt as Ae,c as je,ct as Me,d as Ne,dt as Pe,et as Fe,f as Ie,ft as Le,g as Re,gt as ze,h as Be,ht as Ve,i as He,it as Ue,jt as We,kt as Ge,l as Ke,lt as G,m as qe,mt as Je,n as Ye,nt as Xe,o as Ze,ot as Qe,p as $e,pt as et,q as tt,r as nt,rt,s as it,st as at,t as ot,tt as K,u as st,ut as ct,v as lt,vt as ut,w as dt,wt as ft,x as pt,xt as mt,y as ht,yt as gt,z as _t}from"./select-BBue9CB_.js";import{i as vt,n as yt,r as q,t as bt}from"./useUrlParams-DI9KFxsS.js";import{r as J,t as xt}from"./input-IDkwig6l.js";import{C as St,D as Ct,E as wt,F as Tt,I as Et,M as Dt,N as Ot,O as kt,P as At,S as jt,T as Mt,a as Nt,d as Pt,f as Ft,g as It,h as Lt,i as Y,j as Rt,k as zt,l as Bt,m as Vt,n as Ht,o as Ut,p as Wt,r as Gt,s as Kt,t as X,u as qt,v as Jt}from"./createBaseUIEventDetails-wEMuC0RZ.js";import{n as Yt,r as Xt,t as Zt}from"./badge-B492YG3Q.js";var Qt=P(`funnel-x`,[[`path`,{d:`M12.531 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14v6a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341l.427-.473`,key:`ol2ft2`}],[`path`,{d:`m16.5 3.5 5 5`,key:`15e6fa`}],[`path`,{d:`m21.5 3.5-5 5`,key:`m0lwru`}]]),$t=P(`list-filter`,[[`path`,{d:`M2 5h20`,key:`1fs1ex`}],[`path`,{d:`M6 12h12`,key:`8npq4p`}],[`path`,{d:`M9 19h6`,key:`456am0`}]]),en=P(`notebook-pen`,[[`path`,{d:`M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4`,key:`re6nr2`}],[`path`,{d:`M2 6h4`,key:`aawbzj`}],[`path`,{d:`M2 10h4`,key:`l0bgd4`}],[`path`,{d:`M2 14h4`,key:`1gsvsf`}],[`path`,{d:`M2 18h4`,key:`1bu2t1`}],[`path`,{d:`M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z`,key:`pqwjuv`}]]),tn=P(`pencil`,[[`path`,{d:`M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z`,key:`1a8usu`}],[`path`,{d:`m15 5 4 4`,key:`1mk7zo`}]]),nn=P(`plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),rn=P(`tags`,[[`path`,{d:`M13.172 2a2 2 0 0 1 1.414.586l6.71 6.71a2.4 2.4 0 0 1 0 3.408l-4.592 4.592a2.4 2.4 0 0 1-3.408 0l-6.71-6.71A2 2 0 0 1 6 9.172V3a1 1 0 0 1 1-1z`,key:`16rjxf`}],[`path`,{d:`M2 7v6.172a2 2 0 0 0 .586 1.414l6.71 6.71a2.4 2.4 0 0 0 3.191.193`,key:`178nd4`}],[`circle`,{cx:`10.5`,cy:`6.5`,r:`.5`,fill:`currentColor`,key:`12ikhr`}]]),an=P(`x`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),Z=e(t(),1),on=Z.createContext({disabled:!1});function sn(){return Z.useContext(on)}function cn(){return new Qe({open:!1,transitionStatus:void 0,floatingElement:null,referenceElement:null,triggerElements:new Ce,floatingId:void 0,syncOnly:!1,nested:!1,onOpenChange:void 0})}function ln(){return{open:!1,openProp:void 0,mounted:!1,transitionStatus:void 0,floatingRootContext:cn(),floatingId:void 0,triggerCount:0,preventUnmountingOnClose:!1,payload:void 0,activeTriggerId:null,activeTriggerElement:null,triggerIdProp:void 0,popupElement:null,positionerElement:null,activeTriggerProps:p,inactiveTriggerProps:p,popupProps:p}}function un(e,t,n=!1){return new Qe({open:!1,transitionStatus:void 0,floatingElement:null,referenceElement:null,triggerElements:e,floatingId:t,syncOnly:!0,nested:n,onOpenChange:void 0})}var dn=e=>e.triggerIdProp??e.activeTriggerId,fn=e=>e.openProp??e.open,pn=e=>(e.popupElement?.id??e.floatingId)||void 0;function mn(e,t){return t!==void 0&&fn(e)&&dn(e)===t}function hn(e,t){return mn(e,t)?!0:t!==void 0&&fn(e)&&dn(e)==null&&e.triggerCount===1}var gn={open:fn,mounted:e=>e.mounted,transitionStatus:e=>e.transitionStatus,floatingRootContext:e=>e.floatingRootContext,triggerCount:e=>e.triggerCount,preventUnmountingOnClose:e=>e.preventUnmountingOnClose,payload:e=>e.payload,activeTriggerId:dn,activeTriggerElement:e=>e.mounted?e.activeTriggerElement:null,popupId:pn,isTriggerActive:(e,t)=>t!==void 0&&dn(e)===t,isOpenedByTrigger:(e,t)=>mn(e,t),isMountedByTrigger:(e,t)=>t!==void 0&&dn(e)===t&&e.mounted,triggerProps:(e,t)=>t?e.activeTriggerProps:e.inactiveTriggerProps,triggerPopupId:(e,t)=>hn(e,t)?pn(e):void 0,popupProps:e=>e.popupProps,popupElement:e=>e.popupElement,positionerElement:e=>e.positionerElement};function _n({store:e,parentContext:t,isDrawer:n}){let r=e.useState(`open`),i=e.useState(`disablePointerDismissal`),a=e.useState(`modal`),o=e.useState(`popupElement`),s=e.useState(`floatingRootContext`),[c,l]=Z.useState(0),[d,f]=Z.useState(0),p=c===0,m=ct(s,{outsidePressEvent(){return e.context.internalBackdropRef.current||e.context.backdropRef.current?`intentional`:{mouse:a===`trap-focus`?`sloppy`:`intentional`,touch:`sloppy`}},outsidePress(t){if(!e.context.outsidePressEnabledRef.current||`button`in t&&t.button!==0)return!1;if(`touches`in t){if(t.type===`touchend`){if(t.changedTouches.length!==1||t.touches.length!==0)return!1}else if(t.touches.length!==1)return!1}let n=wt(t);if(p&&!i){if(a){let t=e.context.internalBackdropRef.current,r=e.context.backdropRef.current;return t||r?t===n||r===n||Mt(n,o)&&!n?.hasAttribute(`data-base-ui-portal`):!0}return!0}return!1},escapeKey:p});return ut(r&&a===!0,o),e.useContextCallback(`onNestedDialogOpen`,(e,t)=>{l(e),f(t)}),u(()=>(t?.onNestedDialogOpen&&(r?t.onNestedDialogOpen(c+1,d+ +!!n):t.onNestedDialogOpen(0,0)),()=>{t?.onNestedDialogOpen&&r&&t.onNestedDialogOpen(0,0)}),[n,r,c,d,t]),rt(e,{activeTriggerProps:m.reference,inactiveTriggerProps:m.trigger,popupProps:m.floating,nestedOpenDialogCount:c,nestedOpenDrawerCount:d}),null}var vn=Z.createContext(void 0);function yn(e){let t=Z.useContext(vn);if(!e&&t===void 0)throw Error(c(27));return t}var bn={...gn,modal:e=>e.modal,nested:e=>e.nested,nestedOpenDialogCount:e=>e.nestedOpenDialogCount,nestedOpenDrawerCount:e=>e.nestedOpenDrawerCount,disablePointerDismissal:e=>e.disablePointerDismissal,openMethod:e=>e.openMethod,descriptionElementId:e=>e.descriptionElementId,titleElementId:e=>e.titleElementId,viewportElement:e=>e.viewportElement,role:e=>e.role},xn=class extends at{constructor(e,t,n){let r=new Ce,i=Sn(e,r,t,n);super(i,Cn(r),bn)}setOpen=(e,t)=>{if(t.preventUnmountOnClose=()=>{this.set(`preventUnmountingOnClose`,!0)},!e&&t.trigger==null&&this.state.activeTriggerId!=null&&(t.trigger=this.state.activeTriggerElement??void 0),this.context.onOpenChange?.(e,t),t.isCanceled)return;this.state.floatingRootContext.dispatchOpenChange(e,t);let n={open:e};Fe(n,e,t.trigger),this.update(n)}};function Sn(e,t,n,r=!1){let i={...ln(),modal:!0,disablePointerDismissal:!1,viewportElement:null,descriptionElementId:void 0,titleElementId:void 0,openMethod:null,nested:!1,nestedOpenDialogCount:0,nestedOpenDrawerCount:0,role:`dialog`,...e};return i.floatingRootContext=un(t,n,r),i}function Cn(e){return{popupRef:Z.createRef(),backdropRef:Z.createRef(),internalBackdropRef:Z.createRef(),outsidePressEnabledRef:{current:!0},triggerElements:e,onOpenChange:void 0,onOpenChangeComplete:void 0}}var Q=r();function wn(e,t){let{children:n,open:r,defaultOpen:i=!1,onOpenChange:a,onOpenChangeComplete:o,disablePointerDismissal:s=!1,modal:c=!0,actionsRef:l,handle:u,triggerId:d,defaultTriggerId:f=null}=t,p=e===`drawer`,m=e===`alert-dialog`,h=m?!0:c,g=m||s,_=m?`alertdialog`:`dialog`,v=yn(!0),y={modal:h,disablePointerDismissal:g,nested:v!=null,role:_},b=Ue((e,t)=>new xn({open:i,openProp:r,activeTriggerId:f,triggerIdProp:d,...y},e,t),!0);b.useControlledProp(`openProp`,r),b.useControlledProp(`triggerIdProp`,d),b.useSyncedValues(y),b.useContextCallback(`onOpenChange`,a),b.useContextCallback(`onOpenChangeComplete`,o);let x=b.useState(`open`),S=b.useState(`mounted`),C=b.useState(`payload`);Oe(b,x),K(b);let{forceUnmount:w}=Xe(x,b);Z.useImperativeHandle(l,()=>({unmount:w,close:()=>b.setOpen(!1,X(Bt))}),[w,b]);let T=x||S;return(0,Q.jsxs)(vn.Provider,{value:b,children:[u&&(0,Q.jsx)(me,{handle:u,store:b}),T&&(0,Q.jsx)(_n,{store:b,parentContext:v?.context,isDrawer:p}),typeof n==`function`?n({payload:C}):n]})}function Tn(e){return wn(`alert-dialog`,e)}var En=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,forceRender:a=!1,...o}=e,c=yn(),l=c.useState(`open`),u=c.useState(`nested`),d=c.useState(`mounted`);return s(`div`,e,{state:{open:l,transitionStatus:c.useState(`transitionStatus`)},ref:[c.context.backdropRef,t],stateAttributesMapping:Se,props:[{role:`presentation`,hidden:!d,style:{userSelect:`none`,WebkitUserSelect:`none`}},o],enabled:a||!u})}),Dn=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,disabled:a=!1,nativeButton:o=!0,...c}=e,l=yn(),u=l.useState(`open`),{getButtonProps:f,buttonRef:p}=d({disabled:a,native:o}),m={disabled:a};function h(e){u&&l.setOpen(!1,X(Ut,e.nativeEvent))}return s(`button`,e,{state:m,ref:[t,p],props:[{onClick:h},c,f]})}),On=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,id:a,...o}=e,c=yn(),l=zt(a);return c.useSyncedValueWithCleanup(`descriptionElementId`,l),s(`p`,e,{ref:t,props:[{id:l},o]})}),kn=Z.createContext(void 0);function An(){let e=Z.useContext(kn);if(e===void 0)throw Error(c(26));return e}var jn={...be,...Ae,nestedDialogOpen(e){return e?{"data-nested-dialog-open":``}:null}},Mn=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,finalFocus:a,initialFocus:o,...c}=e,l=yn(),u=l.useState(`descriptionElementId`),d=l.useState(`disablePointerDismissal`),f=l.useState(`floatingRootContext`),p=l.useState(`popupProps`),m=l.useState(`modal`),h=l.useState(`mounted`),g=l.useState(`nested`),_=l.useState(`nestedOpenDialogCount`),v=l.useState(`open`),y=l.useState(`openMethod`),b=l.useState(`titleElementId`),x=l.useState(`transitionStatus`),S=l.useState(`role`),C=f.useState(`floatingId`);An(),mt({open:v,ref:l.context.popupRef,onComplete(){v&&l.context.onOpenChangeComplete?.(!0)}});let w=o===void 0?re(l.context.popupRef):o,T=_>0,E=l.useStateSetter(`popupElement`),D=s(`div`,e,{state:{open:v,nested:g,transitionStatus:x,nestedDialogOpen:T},props:[p,{id:C,"aria-labelledby":b,"aria-describedby":u,role:S,...Te,hidden:!h,onKeyDown(e){le.has(e.key)&&e.stopPropagation()},style:{"--nested-dialogs":_}},c],ref:[t,l.context.popupRef,E],stateAttributesMapping:jn});return(0,Q.jsx)(Le,{context:f,openInteractionType:y,disabled:!h,closeOnFocusOut:!d,initialFocus:w,returnFocus:a,modal:m!==!1,restoreFocus:`popup`,children:D})}),Nn=Z.forwardRef(function(e,t){let{keepMounted:n=!1,...r}=e,i=yn(),a=i.useState(`mounted`),o=i.useState(`modal`),s=i.useState(`open`);return a||n?(0,Q.jsx)(kn.Provider,{value:n,children:(0,Q.jsxs)(et,{ref:t,...r,children:[a&&o===!0&&(0,Q.jsx)(ae,{ref:i.context.internalBackdropRef,inert:xe(!s)}),e.children]})}):null}),Pn=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,id:a,...o}=e,c=yn(),l=zt(a);return c.useSyncedValueWithCleanup(`titleElementId`,l),s(`h2`,e,{ref:t,props:[{id:l},o]})});function Fn({...e}){return(0,Q.jsx)(Tn,{"data-slot":`alert-dialog`,...e})}function In({...e}){return(0,Q.jsx)(Nn,{"data-slot":`alert-dialog-portal`,...e})}function Ln({className:e,...t}){return(0,Q.jsx)(En,{"data-slot":`alert-dialog-overlay`,className:L(`fixed inset-0 isolate z-50 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,e),...t})}function Rn({className:e,size:t=`default`,...n}){return(0,Q.jsxs)(In,{children:[(0,Q.jsx)(Ln,{}),(0,Q.jsx)(Mn,{"data-slot":`alert-dialog-content`,"data-size":t,className:L(`group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-6 rounded-4xl bg-popover p-6 text-popover-foreground ring-1 ring-foreground/5 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...n})]})}function zn({className:e,...t}){return(0,Q.jsx)(`div`,{"data-slot":`alert-dialog-header`,className:L(`grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]`,e),...t})}function Bn({className:e,...t}){return(0,Q.jsx)(`div`,{"data-slot":`alert-dialog-footer`,className:L(`flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end`,e),...t})}function Vn({className:e,...t}){return(0,Q.jsx)(Pn,{"data-slot":`alert-dialog-title`,className:L(`text-heading font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2`,e),...t})}function Hn({className:e,...t}){return(0,Q.jsx)(On,{"data-slot":`alert-dialog-description`,className:L(`text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground`,e),...t})}function Un({className:e,...t}){return(0,Q.jsx)(a,{"data-slot":`alert-dialog-action`,className:L(e),...t})}function Wn({className:e,variant:t=`outline`,size:n=`default`,...r}){return(0,Q.jsx)(Dn,{"data-slot":`alert-dialog-cancel`,className:L(e),render:(0,Q.jsx)(a,{variant:t,size:n}),...r})}function Gn({open:e,onOpenChange:t,title:n,description:r,actionLabel:i,pendingLabel:a,variant:o,onConfirm:s,finalFocus:c}){let[l,u]=(0,Z.useState)(!1);async function d(){u(!0);try{await s()}catch{}finally{u(!1),t(!1)}}return(0,Q.jsx)(Fn,{open:e,onOpenChange:e=>{l||t(e)},children:(0,Q.jsxs)(Rn,{size:`default`,finalFocus:c,children:[(0,Q.jsxs)(zn,{children:[(0,Q.jsx)(Vn,{className:`wrap-anywhere`,children:n}),(0,Q.jsx)(Hn,{className:`wrap-anywhere`,children:r})]}),(0,Q.jsxs)(Bn,{children:[(0,Q.jsx)(Wn,{disabled:l,children:`Cancel`}),(0,Q.jsx)(Un,{variant:o,disabled:l,className:`min-w-28`,onClick:d,children:l?a:i})]})]})})}function Kn(e){return wn(`dialog`,e)}function qn({...e}){return(0,Q.jsx)(Kn,{"data-slot":`dialog`,...e})}function Jn({...e}){return(0,Q.jsx)(Nn,{"data-slot":`dialog-portal`,...e})}function Yn({className:e,...t}){return(0,Q.jsx)(En,{"data-slot":`dialog-overlay`,className:L(`fixed inset-0 isolate z-50 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,e),...t})}function Xn({className:e,children:t,showCloseButton:n=!0,...r}){return(0,Q.jsxs)(Jn,{children:[(0,Q.jsx)(Yn,{}),(0,Q.jsxs)(Mn,{"data-slot":`dialog-content`,className:L(`fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-4xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/5 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...r,children:[t,n&&(0,Q.jsxs)(Dn,{"data-slot":`dialog-close`,render:(0,Q.jsx)(a,{variant:`ghost`,className:`absolute top-4 right-4`,size:`icon-sm`}),children:[(0,Q.jsx)(an,{}),(0,Q.jsx)(`span`,{className:`sr-only`,children:`Close`})]})]})]})}function Zn({className:e,...t}){return(0,Q.jsx)(`div`,{"data-slot":`dialog-header`,className:L(`flex flex-col gap-2`,e),...t})}function Qn({className:e,...t}){return(0,Q.jsx)(Pn,{"data-slot":`dialog-title`,className:L(`text-heading font-semibold`,e),...t})}function $n({className:e,...t}){return(0,Q.jsx)(On,{"data-slot":`dialog-description`,className:L(`text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground`,e),...t})}function er(e){return Array.from(e).length}function tr(e){return er(e.normalize(`NFC`).trim().replace(/\s+/g,` `))}function nr(e){return er(e.trim())}function rr(e){return[...e].sort((e,t)=>{let n=e.name.localeCompare(t.name,void 0,{sensitivity:`base`});return n===0?e.id-t.id:n})}function ir(e,t){switch(t.kind){case`renamed`:return e.map(e=>e.id===t.tag.id?{...e,name:t.tag.name}:e);case`merged`:return e.filter(e=>e.id!==t.sourceId).map(e=>e.id===t.target.id?{...e,name:t.target.name,carrier_count:t.target.carrier_count}:e);case`deleted`:return e.filter(e=>e.id!==t.tagId)}}function ar(e,t){switch(t.kind){case`renamed`:return e.some(e=>e.id===t.tag.id)?e.map(e=>e.id===t.tag.id?t.tag:e):e;case`deleted`:return e.some(e=>e.id===t.tagId)?e.filter(e=>e.id!==t.tagId):e;case`merged`:{if(!e.some(e=>e.id===t.sourceId))return e;if(e.some(e=>e.id===t.target.id))return e.filter(e=>e.id!==t.sourceId);let n={id:t.target.id,name:t.target.name};return e.map(e=>e.id===t.sourceId?n:e)}}}var or=(0,Z.createContext)(null);function sr({onChange:e,children:t}){let[n,r]=(0,Z.useState)(null),[i,a]=(0,Z.useState)(`idle`),o=(0,Z.useRef)(null),s=(0,Z.useRef)(`idle`),c=(0,Z.useRef)(0),l=(0,Z.useRef)(!1),u=(0,Z.useRef)(e);u.current=e;let d=(0,Z.useCallback)(e=>{o.current=e,r(e)},[]),f=(0,Z.useCallback)(e=>{s.current=e,a(e)},[]),p=(0,Z.useCallback)(()=>{let e=++c.current;l.current=!0,f(`loading`),x().then(t=>{e===c.current&&(l.current=!1,d(rr(t)),f(`loaded`))}).catch(()=>{e===c.current&&(l.current=!1,f(`error`))})},[f,d]),m=(0,Z.useCallback)(()=>{c.current++,l.current&&p()},[p]),h=(0,Z.useCallback)(()=>{(s.current===`idle`||s.current===`error`)&&p()},[p]),g=(0,Z.useCallback)(e=>{let t=o.current;if(t===null){l.current&&m();return}t.some(t=>t.id===e.id)||(d(rr([...t,{...e,carrier_count:1}])),m())},[d,m]),_=(0,Z.useCallback)(e=>{let t=o.current;t!==null&&d(rr(ir(t,e))),m(),u.current?.(e)},[d,m]),v=(0,Z.useCallback)(async(e,t)=>{let n=await w(e,t);return n.kind===`renamed`&&_({kind:`renamed`,tag:n.tag}),n},[_]),y=(0,Z.useCallback)(async(e,t)=>{let n;try{n=await j(e,t)}catch(e){throw p(),e}return _({kind:`merged`,sourceId:e,target:n}),n},[_,p]),b=(0,Z.useCallback)(async e=>{let t;try{t=await k(e)}catch(e){throw p(),e}return _({kind:`deleted`,tagId:e}),t},[_,p]),S=(0,Z.useMemo)(()=>({vocabulary:n,status:i,ensureLoaded:h,reload:p,remember:g,rename:v,merge:y,remove:b}),[n,i,h,p,g,v,y,b]);return(0,Q.jsx)(or.Provider,{value:S,children:t})}function cr(){let e=(0,Z.useContext)(or);if(!e)throw Error(`useTagVocabulary must be used inside TagVocabularyProvider`);return e}var lr=[];function ur(e){return e===1?`artist`:`artists`}function dr({open:e,onOpenChange:t}){let{vocabulary:n,status:r,reload:i,rename:o,merge:s,remove:c}=cr(),l=n??lr,[u,d]=(0,Z.useState)(null),[f,p]=(0,Z.useState)(null),[m,h]=(0,Z.useState)(null),[g,_]=(0,Z.useState)(``),v=tr(g),y=v>32,[b,x]=(0,Z.useState)(!1),[S,C]=(0,Z.useState)(null),w=(0,Z.useRef)(null),T=(0,Z.useRef)(null),E=(0,Z.useRef)(null),D=(0,Z.useRef)(null),O=(0,Z.useRef)(!1);(0,Z.useEffect)(()=>{O.current=e,e&&i()},[e]),(0,Z.useLayoutEffect)(()=>{let e=u;if(!e||r!==`loaded`)return;let t=w.current?.querySelectorAll(`li`);if(e.kind===`after-delete`){if(l.some(t=>t.id===e.id))return;if(d(null),l.length===0){document.querySelector(`[data-slot="dialog-close"]`)?.focus();return}t?.[Math.min(e.index,l.length-1)]?.querySelector(`button`)?.focus();return}d(null);let n=l.findIndex(t=>t.id===e.id);t?.[n]?.querySelector(`button`)?.focus()},[l,r,u]),(0,Z.useLayoutEffect)(()=>{if(r!==`loaded`||!O.current)return;O.current=!1;let e=w.current,t=e?.querySelector(`li button`);if(!e||!t)return;let n=e.closest(`[data-slot="dialog-content"]`),i=document.activeElement,a=n?.querySelector(`[data-slot="dialog-close"]`);(!i||i===document.body||!n?.contains(i)||i===n||i===a)&&t.focus()},[r]),(0,Z.useEffect)(()=>{m&&(E.current?.focus(),E.current?.select())},[m]),(0,Z.useEffect)(()=>{if(m!==null)return;let e=T.current;e!==null&&(T.current=null,(w.current?.querySelectorAll(`li`)[e])?.querySelector(`button`)?.focus())},[m]);function k(e){h(e),_(e.name)}function A(e){T.current=e,h(null),_(``)}async function j(e){let t=g.trim();if(!(b||y||t===``||t===e.name)){x(!0);try{let n=await o(e.id,t);n.kind===`renamed`?(d({kind:`tag`,id:e.id}),h(null),_(``),x(!1),J.success(`Renamed “${e.name}” to “${n.tag.name}”.`)):(x(!1),C({sourceId:e.id,sourceName:e.name,target:n.target,carrierCountAfterMerge:n.carrierCountAfterMerge}))}catch{x(!1),J.error(`Couldn't rename “${e.name}” — try again.`)}}}async function M(){if(!S)return;let{sourceId:e,sourceName:t,target:n}=S;try{let r=await s(e,n.id);d({kind:`tag`,id:n.id}),h(null),_(``),J.success(`Merged “${t}” into “${r.name}” — ${r.carrier_count} ${ur(r.carrier_count)} now carry it.`)}catch{J.error(`Couldn't merge “${t}” into “${n.name}” — try again.`)}}async function N(){if(!f)return;let e=f,t=l.findIndex(t=>t.id===e.id);try{let{carrier_count:n}=await c(e.id);d({kind:`after-delete`,id:e.id,index:t}),J.success(n===0?`Deleted “${e.name}”.`:`Deleted “${e.name}” from ${n} ${ur(n)}.`)}catch{J.error(`Couldn't delete “${e.name}” — try again.`)}}return(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(qn,{open:e,onOpenChange:t,children:(0,Q.jsxs)(Xn,{className:`max-h-[calc(100dvh-2rem)] grid-rows-[auto_1fr] sm:max-w-lg`,children:[(0,Q.jsxs)(Zn,{children:[(0,Q.jsx)(Qn,{children:`Manage tags`}),(0,Q.jsx)($n,{children:`Rename or delete tags across all artists. Counts include only artists on your watchlist.`})]}),(r===`idle`||r===`loading`)&&(0,Q.jsx)(`div`,{className:`flex flex-col gap-2`,children:[0,1,2].map(e=>(0,Q.jsx)(Yt,{className:`h-10 w-full`},e))}),r===`error`&&(0,Q.jsx)(Xt,{heading:`Couldn't load tags.`,body:`Please try again.`,action:(0,Q.jsx)(a,{variant:`secondary`,onClick:i,children:`Retry`})}),r===`loaded`&&l.length===0&&(0,Q.jsx)(Xt,{heading:`No tags yet`,body:`Add a tag from any artist's card on the Watchlist and it will appear here.`}),r===`loaded`&&l.length>0&&(0,Q.jsx)(`ul`,{ref:w,"aria-label":`Tags`,className:`-mx-2 max-h-[min(60vh,28rem)] divide-y divide-border overflow-y-auto overscroll-contain px-2`,children:l.map((e,t)=>m?.id===e.id?(0,Q.jsxs)(`li`,{className:`flex items-center gap-2 py-2`,children:[(0,Q.jsx)(xt,{ref:E,value:g,onChange:e=>_(e.target.value),onKeyDown:n=>{n.key===`Enter`?(n.preventDefault(),j(e)):n.key===`Escape`&&(n.preventDefault(),n.stopPropagation(),A(t))},readOnly:b,"aria-label":`New name for ${e.name}`,className:`h-8 flex-1`}),v>=25&&(0,Q.jsxs)(`span`,{className:`shrink-0 text-label tabular-nums ${y?`text-destructive`:`text-muted-foreground`}`,children:[v,`/`,32]}),(0,Q.jsx)(a,{variant:`ghost`,size:`sm`,onClick:()=>A(t),children:`Cancel`}),(0,Q.jsx)(a,{ref:D,variant:`default`,size:`sm`,className:`min-w-20`,disabled:b||y||g.trim()===``||g.trim()===e.name,onClick:()=>void j(e),children:b?`Saving…`:`Save`})]},e.id):(0,Q.jsxs)(`li`,{className:`flex items-center gap-4 py-2`,children:[(0,Q.jsxs)(`div`,{className:`flex min-w-0 flex-1 items-baseline gap-1`,children:[(0,Q.jsx)(`span`,{className:`truncate text-body text-foreground`,title:e.name,children:e.name}),(0,Q.jsxs)(`span`,{className:`shrink-0 text-label whitespace-nowrap text-muted-foreground`,children:[`· `,e.carrier_count,` `,ur(e.carrier_count)]})]}),(0,Q.jsx)(a,{variant:`ghost`,size:`sm`,"aria-label":`Rename tag ${e.name}`,onClick:()=>k(e),children:`Rename`}),(0,Q.jsx)(a,{variant:`ghost`,size:`sm`,className:`text-muted-foreground hover:text-destructive`,"aria-label":`Delete tag ${e.name}`,onClick:()=>p(e),children:`Delete`})]},e.id))})]})}),f&&(0,Q.jsx)(Gn,{open:!0,onOpenChange:e=>{e||p(null)},title:f.carrier_count===0?`Delete “${f.name}”?`:`Delete “${f.name}” from ${f.carrier_count} ${ur(f.carrier_count)}?`,description:f.carrier_count===0?`No artists on your watchlist carry this tag, and this can't be undone.`:`The tag is removed everywhere and this can't be undone; the artists stay on your watchlist.`,actionLabel:`Delete tag`,pendingLabel:`Deleting…`,variant:`destructive`,onConfirm:N}),S&&(0,Q.jsx)(Gn,{open:!0,onOpenChange:e=>{e||C(null)},title:`Merge “${S.sourceName}” into “${S.target.name}”?`,description:`${S.carrierCountAfterMerge} ${ur(S.carrierCountAfterMerge)} will carry “${S.target.name}”, and “${S.sourceName}” will be deleted.`,actionLabel:`Merge tags`,pendingLabel:`Merging…`,variant:`default`,onConfirm:M,finalFocus:D})]})}var fr=300;function pr({onResults:e}){let[t,n]=(0,Z.useState)(``),[r,i]=(0,Z.useState)(!1),a=(0,Z.useRef)(null),o=(0,Z.useRef)(null);(0,Z.useEffect)(()=>()=>{a.current&&clearTimeout(a.current),o.current?.abort()},[]);function s(t){o.current?.abort();let n=new AbortController;o.current=n,i(!0),C(t,n.signal).then(t=>{n.signal.aborted||e(t)}).catch(()=>{n.signal.aborted||e(null)}).finally(()=>{n.signal.aborted||i(!1)})}function c(t){let r=t.target.value;n(r),a.current&&clearTimeout(a.current),o.current?.abort();let c=r.trim();if(c===``){i(!1),e(null);return}a.current=setTimeout(()=>s(c),fr)}return(0,Q.jsxs)(`label`,{className:`flex w-full max-w-xl flex-col gap-1 text-label text-muted-foreground`,children:[`Search artists`,(0,Q.jsxs)(`div`,{className:`relative`,children:[(0,Q.jsx)(xt,{type:`text`,value:t,onChange:c,placeholder:`Search for an artist…`,className:`pr-10`}),r&&(0,Q.jsx)(A,{className:`absolute top-1/2 right-3 size-4 -translate-y-1/2 animate-spin text-muted-foreground`,"aria-hidden":`true`})]})]})}var mr={musicbrainz:`MusicBrainz`,deezer:`Deezer`};function hr(e){return mr[e]??e}function gr({response:e,watchlistEntries:t,onAdd:n}){let r=Object.keys(e.sources);return(0,Q.jsx)(`div`,{className:`grid grid-cols-1 gap-6 sm:grid-cols-2`,children:r.map(i=>(0,Q.jsx)(_r,{sourceName:i,result:e.sources[i],otherSourceNames:r.filter(e=>e!==i),query:e.query,watchlistEntries:t,onAdd:n},i))})}function _r({sourceName:e,result:t,otherSourceNames:n,query:r,watchlistEntries:i,onAdd:a}){return(0,Q.jsxs)(`div`,{className:`flex flex-col gap-3`,children:[(0,Q.jsx)(`h2`,{className:`text-heading font-semibold text-foreground`,children:hr(e)}),t.status===`error`&&(0,Q.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[hr(e),` is unavailable right now — showing`,` `,n.map(hr).join(`, `),` results only.`]}),t.status!==`error`&&t.artists.length===0&&(0,Q.jsxs)(`p`,{className:`text-label text-muted-foreground`,children:[`No matches for "`,r,`"`]}),t.status!==`error`&&t.artists.length>0&&(0,Q.jsx)(`ul`,{className:`flex max-h-96 flex-col gap-3 overflow-y-auto`,children:t.artists.map(t=>(0,Q.jsx)(vr,{sourceName:e,artist:t,alreadyWatching:i?.some(n=>n[B(e)]===t.id)??!1,onAdd:a},t.id))})]})}function vr({sourceName:e,artist:t,alreadyWatching:n,onAdd:r}){let[i,o]=(0,Z.useState)(!1),s=H(e),c=t.disambiguation??t.country;async function l(){o(!0);try{await r(e,t)}finally{o(!1)}}return(0,Q.jsxs)(`li`,{className:`flex items-center gap-3 rounded-md bg-card p-3`,children:[(0,Q.jsx)(vt,{src:t.image_url,alt:t.name,size:48}),(0,Q.jsxs)(`div`,{className:`flex min-w-0 flex-1 flex-col gap-0.5`,children:[(0,Q.jsx)(`span`,{className:`truncate text-body font-medium text-foreground`,children:t.name}),(0,Q.jsx)(`span`,{className:`text-label text-muted-foreground`,children:t.type}),c!==null&&(0,Q.jsx)(`span`,{className:`truncate text-label text-muted-foreground`,children:c})]}),n?(0,Q.jsx)(a,{variant:`secondary`,size:`sm`,disabled:!0,className:`shrink-0`,children:`Already watching`}):s?(0,Q.jsx)(a,{size:`sm`,disabled:i,"aria-busy":i,onClick:l,className:`shrink-0`,children:i?(0,Q.jsx)(A,{className:`size-4 animate-spin`,"aria-hidden":`true`}):`Add to Watchlist`}):(0,Q.jsx)(a,{variant:`secondary`,size:`sm`,disabled:!0,title:`This project can't yet match a Deezer artist to its MusicBrainz identity -- search MusicBrainz to add this artist and get release detection.`,className:`shrink-0`,children:`Search MusicBrainz to add`})]})}function yr({className:e,...t}){return(0,Q.jsx)(`textarea`,{"data-slot":`textarea`,className:L(`flex field-sizing-content min-h-16 w-full resize-none rounded-xl border border-input bg-input/30 px-3 py-3 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40`,e),...t})}function br(e){return e instanceof F&&e.code===`note_too_long`?`Notes can be at most 500 characters.`:`Couldn't save the note — your text is still here. Try again.`}function xr({entry:e,onEntryChange:t,announce:n}){let[r,i]=(0,Z.useState)(!1),[o,s]=(0,Z.useState)(e.note??``),[c,l]=(0,Z.useState)(``),[u,d]=(0,Z.useState)(!1),[f,p]=(0,Z.useState)(!1),[m,h]=(0,Z.useState)(!1),[g,_]=(0,Z.useState)(``),v=(0,Z.useRef)(null),y=(0,Z.useRef)(null),b=(0,Z.useRef)(null),x=(0,Z.useRef)(null),S=(0,Z.useRef)(null),C=(0,Z.useRef)(nr(e.note??``));(0,Z.useLayoutEffect)(()=>{if(!r)return;let e=v.current;if(!e)return;e.focus();let t=e.value.length;e.setSelectionRange(t,t)},[r]),(0,Z.useLayoutEffect)(()=>{if(r)return;let e=S.current;e&&(S.current=null,(e.hasNote?y.current:b.current)?.focus())},[r]),(0,Z.useLayoutEffect)(()=>{if(r||e.note===null){h(!1);return}if(f)return;let t=x.current;if(!t)return;let n=()=>h(t.scrollHeight>t.clientHeight);if(n(),typeof ResizeObserver>`u`)return;let i=new ResizeObserver(n);return i.observe(t),()=>i.disconnect()},[r,e.note,f]);function w(){s(e.note??``),l(``),p(!1),C.current=nr(e.note??``),i(!0)}function T(){u||(S.current={hasNote:e.note!==null},i(!1),l(``))}function E(e){let t=C.current,n=nr(e);C.current=n,s(e),n===500&&t!==500?_(`Note limit reached — 500 characters.`):t<450&&n>=450&&_(`50 characters left.`)}let D=o.trim(),O=e.note??``,k=nr(o),A=k>500,j=D===O||A;async function M(){if(j||u)return;let r=D===``?null:D;d(!0),l(``);try{let a=await I(e.id,r);S.current={hasNote:!!a.note},t(e.id,{note:a.note}),n(a.note?`Note saved for ${e.name}.`:`Note cleared for ${e.name}.`),i(!1),l(``)}catch(e){l(br(e))}finally{d(!1)}}if(r){let t=`note-counter-${e.id}`;return(0,Q.jsxs)(`div`,{className:`mt-1 flex flex-col gap-1`,children:[(0,Q.jsx)(yr,{ref:v,value:o,onChange:e=>E(e.target.value),onKeyDown:e=>{if(e.key===`Escape`){if(u)return;e.preventDefault(),T()}else e.key===`Enter`&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),M())},readOnly:u,"aria-busy":u,"aria-describedby":t,className:`max-h-40 overflow-y-auto md:text-label`,"aria-label":`Note for ${e.name}`,placeholder:`Add a note about this artist`}),(0,Q.jsxs)(`div`,{className:`flex items-center justify-between gap-2`,children:[(0,Q.jsxs)(`span`,{id:t,className:`text-label tabular-nums ${A?`text-destructive`:k>=450?`text-foreground`:`text-muted-foreground`}`,children:[k,`/`,500]}),(0,Q.jsx)(`span`,{"aria-live":`polite`,className:`sr-only`,children:g}),(0,Q.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Q.jsx)(a,{variant:`ghost`,size:`sm`,disabled:u,onClick:T,children:`Cancel`}),(0,Q.jsx)(a,{variant:`default`,size:`sm`,className:`min-w-20`,disabled:j||u,onClick:()=>void M(),children:u?`Saving…`:`Save`})]})]}),(0,Q.jsx)(`p`,{"aria-live":`polite`,className:`text-label text-destructive`,children:c})]})}return e.note===null?(0,Q.jsx)(`div`,{className:`mt-1`,children:(0,Q.jsxs)(a,{ref:b,variant:`ghost`,size:`xs`,className:`h-6 text-label text-muted-foreground hover:text-foreground`,"aria-label":`Add note for ${e.name}`,onClick:w,children:[(0,Q.jsx)(en,{"aria-hidden":`true`,className:`size-3`}),`add note`]})}):(0,Q.jsxs)(`div`,{className:`mt-1 flex items-start gap-1`,children:[(0,Q.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,Q.jsx)(`p`,{ref:x,id:`note-${e.id}`,className:`text-label break-words whitespace-pre-line text-muted-foreground ${f?``:`line-clamp-2`}`,children:e.note}),m&&(0,Q.jsx)(`button`,{type:`button`,className:`rounded-sm text-label text-muted-foreground underline underline-offset-4 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50`,"aria-expanded":f,"aria-controls":`note-${e.id}`,onClick:()=>p(e=>!e),children:f?`less`:`more`})]}),(0,Q.jsx)(a,{ref:y,variant:`ghost`,size:`icon-xs`,"aria-label":`Edit note for ${e.name}`,onClick:w,children:(0,Q.jsx)(tn,{"aria-hidden":`true`,className:`size-3`})})]})}function Sr(e){if(!e)return null;for(let t of e.elements){let e=t.tagName;if(e===`BUTTON`||e===`INPUT`){let e=t;if(e.type===`submit`)return e}}return null}function Cr(e){return{checked(t){return e.indeterminate?{}:t?{"data-checked":``}:{"data-unchecked":``}},...Et}}var wr=Z.createContext(void 0);function Tr(){return Z.useContext(wr)}var Er=Z.createContext(void 0);function Dr(){let e=Z.useContext(Er);if(e===void 0)throw Error(c(14));return e}var Or=`data-parent`,kr=Z.forwardRef(function(e,t){let{checked:n,className:r,defaultChecked:i=!1,"aria-labelledby":a,disabled:o=!1,form:c,id:m,indeterminate:h=!1,inputRef:g,name:_,onCheckedChange:v,parent:b=!1,readOnly:x=!1,render:S,required:C=!1,uncheckedValue:w,value:D,nativeButton:O=!1,style:k,...A}=e,{clearErrors:j}=Rt(),{disabled:M,name:N,setDirty:ee,setFilled:P,setFocused:F,setTouched:I,state:L,validationMode:te,validityData:re,validation:ae}=At(),oe=sn(),{labelId:R,controlId:se,registerControlId:z,getDescriptionProps:ce}=kt(),B=Tr(),V=B?.allValues===void 0?void 0:B.parent,le=V!==void 0,H=M||oe.disabled||B?.disabled||o,ue=N??_,U=D??ue,de=zt(),fe=zt(),pe=m||se;le&&(b?pe=fe:U===void 0?pe||=fe:pe=`${V.id}-${U}`);let me={};le&&(b?me=V.getParentProps():U!==void 0&&(me=V.getChildProps(U)));let{checked:he=n,indeterminate:ge=h,onCheckedChange:_e,...ve}=me,ye=B?.value,be=Z.useRef(null),xe=ne(()=>Symbol()),Se=Z.useRef(!1),{getButtonProps:Ce,buttonRef:we}=d({disabled:H,native:O}),Te=B?.validation??ae,[W,Ee]=St({controlled:U!==void 0&&ye!==void 0&&!b?ye.includes(U):he,default:i,name:`Checkbox`,state:`checked`}),De=le?!!he:W,Oe=le&&ge||h;u(()=>{z!==E&&(Se.current=!0,z(xe.current,pe))},[pe,z,xe]),Z.useEffect(()=>{let e=xe.current;return()=>{!Se.current||z===E||(Se.current=!1,z(e,void 0))}},[z,xe]),jt(be,de,W,void 0,!B&&!H,_);let ke=Z.useRef(null),Ae=Te.registerInput,je=B?U:void 0,Me=Z.useCallback(e=>Ae(e,{controlRef:be,value:je}),[Ae,je]),Ne=T(g,ke,b?void 0:Me),Pe=ie(a,R,ke,!O,pe??void 0);u(()=>{ke.current&&(ke.current.indeterminate=Oe,W&&P(!0))},[W,Oe,P]),_t(W,()=>{B||(j(ue),P(W),ee(W!==re.initialValue),Te.change(W))});let Fe=l({checked:W,disabled:H,form:c,name:b?void 0:ue,id:O?void 0:pe??void 0,required:C,ref:Ne,style:ue?ze:Ve,tabIndex:-1,type:`checkbox`,"aria-hidden":!0,onChange(e){if(e.nativeEvent.defaultPrevented)return;if(x){e.preventDefault();return}let t=e.currentTarget.checked,n=X(It,e.nativeEvent);if(v?.(t,n),!n.isCanceled&&(_e?.(t,n),!n.isCanceled&&(Ee(t),U!==void 0&&B!==void 0&&!b&&!le))){let e=t?[...B.value,U]:B.value.filter(e=>e!==U);B.setValue(e,n)}},onClick(e){e.stopPropagation()},onFocus(){be.current?.focus()}},D===void 0?p:{value:(B?W&&D:D)||``},ce,e=>Te.getValidationProps(H,e));Z.useEffect(()=>{if(!V||U===void 0)return;let e=V.disabledStatesRef.current;return e.set(U,H),()=>{e.delete(U)}},[V,H,U]);let Ie=Z.useMemo(()=>({...L,checked:De,disabled:H,readOnly:x,required:C,indeterminate:Oe}),[L,De,H,x,C,Oe]),Le=Cr(Ie),Re=s(`span`,e,{state:Ie,ref:[we,be,t],props:[{id:O?pe??void 0:de,role:`checkbox`,"aria-checked":Oe?`mixed`:De,"aria-readonly":x||void 0,"aria-required":C||void 0,"aria-labelledby":Pe,[Or]:b?``:void 0,onFocus(){H||F(!0)},onBlur(){let e=ke.current;e&&(I(!0),F(!1),te===`onBlur`&&Te.commit(B?ye:e.checked))},onKeyDown(e){if(e.key!==`Enter`||(e.preventBaseUIHandler(),e.defaultPrevented))return;let t=ke.current?.form??null,n=e.currentTarget,r=e.nativeEvent,i=e.preventDefault,a=r.preventDefault,o=!1;e.preventDefault=()=>{o=!0,i.call(e)},r.preventDefault=()=>{o=!0,a.call(r)},a.call(r),y(n).queueMicrotask(()=>{e.preventDefault=i,r.preventDefault=a,o||Sr(t)?.click()})},onClick(e){if(x||H)return;e.preventDefault();let t=ke.current;t&&f(t,e)}},A,ve,Ce,ce,e=>Te.getValidationProps(H,e)],stateAttributesMapping:Le});return(0,Q.jsxs)(Er.Provider,{value:Ie,children:[Re,!W&&!B&&ue&&!b&&w!==void 0&&(0,Q.jsx)(`input`,{type:`hidden`,form:c,name:ue,value:w,disabled:H}),(0,Q.jsx)(`input`,{...Fe,suppressHydrationWarning:!0})]})}),Ar=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,keepMounted:a=!1,...o}=e,c=Dr(),l=c.checked||c.indeterminate,{mounted:u,transitionStatus:d,setMounted:f}=gt(l),p=Z.useRef(null),m={...c,transitionStatus:d};mt({open:l,ref:p,onComplete(){l||f(!1)}});let h={...Cr(c),...Ae},g=a||u,_=s(`span`,e,{ref:[t,p],state:m,stateAttributesMapping:h,props:o});return g?_:null});function jr({className:e,...t}){return(0,Q.jsx)(kr,{"data-slot":`checkbox`,className:L(`peer relative flex size-4 shrink-0 items-center justify-center rounded-[6px] border border-input transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary`,e),...t,children:(0,Q.jsx)(Ar,{"data-slot":`checkbox-indicator`,className:`grid place-content-center text-current transition-none [&>svg]:size-3.5`,children:(0,Q.jsx)(de,{})})})}function Mr({entry:e,onEntryChange:t}){let[n,r]=(0,Z.useState)(!1),[i,a]=(0,Z.useState)(!1);async function o(n,i){let a=e.release_types,o=i?[...a,n]:a.filter(e=>e!==n);t(e.id,{release_types:o}),r(!0);try{let n=await te(e.id,{releaseTypes:o});t(e.id,{release_types:n.release_types})}catch{t(e.id,{release_types:a}),J.error(`Couldn't update preferences — try again.`)}finally{r(!1)}}async function s(n,r){let i=e.muted_event_types,o=r?[...i,n]:i.filter(e=>e!==n);t(e.id,{muted_event_types:o}),a(!0);try{let n=await te(e.id,{mutedEventTypes:o});t(e.id,{muted_event_types:n.muted_event_types})}catch{t(e.id,{muted_event_types:i}),J.error(`Couldn't update preferences — try again.`)}finally{a(!1)}}return(0,Q.jsxs)(`div`,{className:`flex flex-col gap-3 sm:flex-row sm:gap-6`,children:[(0,Q.jsxs)(`fieldset`,{className:`flex flex-col gap-1.5`,disabled:n,children:[(0,Q.jsx)(`legend`,{className:`text-label text-muted-foreground`,children:`Release types`}),(0,Q.jsxs)(`div`,{className:`flex flex-wrap gap-3`,children:[(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.release_types.includes(`album`),onCheckedChange:e=>o(`album`,e),"aria-label":`Album release type`}),`Album`]}),(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.release_types.includes(`single`),onCheckedChange:e=>o(`single`,e),"aria-label":`Single release type`}),`Single`]}),(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.release_types.includes(`ep`),onCheckedChange:e=>o(`ep`,e),"aria-label":`EP release type`}),`EP`]}),(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.release_types.includes(`deluxe`),onCheckedChange:e=>o(`deluxe`,e),"aria-label":`Deluxe release type`}),`Deluxe`]})]})]}),(0,Q.jsxs)(`fieldset`,{className:`flex flex-col gap-1.5`,disabled:i,children:[(0,Q.jsx)(`legend`,{className:`text-label text-muted-foreground`,children:`Mute alerts`}),(0,Q.jsxs)(`div`,{className:`flex flex-wrap gap-3`,children:[(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.muted_event_types.includes(`new_release`),onCheckedChange:e=>s(`new_release`,e),"aria-label":`Mute new release alerts`}),`New release`]}),(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.muted_event_types.includes(`guest_feature`),onCheckedChange:e=>s(`guest_feature`,e),"aria-label":`Mute guest feature alerts`}),`Guest feature`]}),(0,Q.jsxs)(`label`,{className:`flex min-h-11 items-center gap-1.5 py-1 text-label text-foreground`,children:[(0,Q.jsx)(jr,{checked:e.muted_event_types.includes(`deluxe_change`),onCheckedChange:e=>s(`deluxe_change`,e),"aria-label":`Mute deluxe change alerts`}),`Deluxe change`]})]})]})]})}function Nr(e,t,n,r,i,a,o,s,c,l=2){let u=R(n.current,{event:e,orientation:r,loopFocus:i,rtl:a,cols:l,disabledIndices:o,minIndex:s,maxIndex:c,prevIndex:t>c?s:t,stopEvent:!0});return ft(n.current,u)?void 0:u}var Pr=Z.createContext(void 0),Fr=Z.createContext(void 0),Ir=Z.createContext(void 0),Lr=Z.createContext(!1),Rr=Z.createContext(``);function zr(){let e=Z.useContext(Pr);if(!e)throw Error(c(22));return e}function Br(){let e=Z.useContext(Fr);if(!e)throw Error(c(23));return e}function Vr(){let e=Z.useContext(Ir);if(!e)throw Error(c(24));return e}function Hr(){return Z.useContext(Rr)}function Ur(){return Z.useContext(Lr)}var $={id:e=>e.id,labelId:e=>e.labelId,items:e=>e.items,selectedValue:e=>e.selectedValue,hasSelectionChips:e=>{let t=e.selectedValue;return Array.isArray(t)&&t.length>0},hasSelectedValue:e=>{let{selectedValue:t,selectionMode:n}=e;return t==null?!1:n===`multiple`&&Array.isArray(t)?t.length>0:!0},hasNullItemLabel:(e,t)=>t?qe(e.items):!1,open:e=>e.open,mounted:e=>e.mounted,forceMounted:e=>e.forceMounted,inline:e=>e.inline,activeIndex:e=>e.activeIndex,selectedIndex:e=>e.selectedIndex,isActive:(e,t)=>e.activeIndex===t,isSelected:(e,t)=>{let n=e.isItemEqualToValue,r=e.selectedValue;return Array.isArray(r)?r.some(e=>lt(t,e,n)):lt(t,r,n)},transitionStatus:e=>e.transitionStatus,popupProps:e=>e.popupProps,listProps:e=>e.listProps,inputProps:e=>e.inputProps,triggerProps:e=>e.triggerProps,itemProps:e=>e.itemProps,positionerElement:e=>e.positionerElement,listElement:e=>e.listElement,popupId:e=>e.popupId,triggerElement:e=>e.triggerElement,inputElement:e=>e.inputElement,inputGroupElement:e=>e.inputGroupElement,popupSide:e=>e.popupSide,openMethod:e=>e.openMethod,inputInsidePopup:e=>e.inputInsidePopup,inputOwnsFormValue:e=>e.inputOwnsFormValue,selectionMode:e=>e.selectionMode,name:e=>e.name,form:e=>e.form,disabled:e=>e.disabled,readOnly:e=>e.readOnly,required:e=>e.required,grid:e=>e.grid,virtualized:e=>e.virtualized,itemToStringLabel:e=>e.itemToStringLabel,isItemEqualToValue:e=>e.isItemEqualToValue,modal:e=>e.modal,autoHighlight:e=>e.autoHighlight};function Wr(e){return e==null?void 0:`${e}-popup`}function Gr(e,t){return(n,r)=>n==null?!1:e.contains(n,r,t)}function Kr(e){return Array.isArray(e)?e.map(e=>Kr(e)).join(`,`):e==null?``:String(e)}var qr=new Map;function Jr(e={}){let t={usage:`search`,sensitivity:`base`,ignorePunctuation:!0,...e},n=`${Kr(e.locale)}|${JSON.stringify(t)}`,r=qr.get(n);if(r)return r;let i=new Intl.Collator(e.locale,t),a={contains(e,t,n){if(!t)return!0;let r=Re(e,n);for(let e=0;e<=r.length-t.length;e+=1)if(i.compare(r.slice(e,e+t.length),t)===0)return!0;return!1},startsWith(e,t,n){if(!t)return!0;let r=Re(e,n);return i.compare(r.slice(0,t.length),t)===0},endsWith(e,t,n){if(!t)return!0;let r=Re(e,n),a=t.length;return r.length>=a&&i.compare(r.slice(r.length-a),t)===0}};return qr.set(n,a),a}var Yr=Jr;function Xr(e,t=!1){let{overflowY:n}=v(e);return n!==`auto`&&n!==`scroll`?!1:t?e.clientHeight>0:e.scrollHeight>e.clientHeight}var Zr=Symbol(`none`),Qr={value:Zr,index:-1};function $r(e){let{id:t,onOpenChangeComplete:n,defaultSelectedValue:r=null,selectedValue:i,onSelectedValueChange:a,defaultInputValue:s,inputValue:c,open:d,defaultOpen:f=!1,selectionMode:m,onItemHighlighted:_,name:v,form:y,disabled:b=!1,readOnly:x=!1,required:S=!1,inputRef:C,grid:w=!1,items:D,filteredItems:O,filter:k,openOnInputClick:A=!0,autoHighlight:j=!1,keepHighlight:N=!1,highlightItemOnHover:ee=!0,loopFocus:P=!0,itemToStringLabel:F,itemToStringValue:I,isItemEqualToValue:L=ht,virtualized:te=!1,inline:re=!1,fillInputOnItemPress:ie=!0,modal:ae=!1,limit:R=-1,autoComplete:se=`list`,formAutoComplete:z,locale:ce,submitOnItemClick:B=!1}=e,{clearErrors:V}=Rt(),{setDirty:le,validityData:H,setFilled:U,name:de,disabled:fe,setTouched:pe,setFocused:me,validationMode:_e,validation:ve}=At(),ye=Ie(),be=Ct({id:t}),xe=Yr({locale:ce}),[Se,Ce]=Z.useState(!1),[De,Oe]=Z.useState(null),ke=Z.useRef([]),Ae=Z.useRef([]),je=Z.useRef(null),Ne=Z.useRef(null),Fe=Z.useRef(null),Le=Z.useRef(null),He=Z.useRef(null),Ue=Z.useRef(!0),We=Z.useRef(!1),Ge=Z.useRef(null),Ke=Z.useRef(null),qe=Z.useRef(null),Je=Z.useRef(Qr),Ye=Z.useRef(null),Xe=Z.useRef([]),Ze=Z.useRef(null),Qe=fe||b,et=de??v,tt=m===`multiple`,nt=m===`single`,rt=c!==void 0||s!==void 0,it=D!==void 0,at=O!==void 0,ot;ot=j===`always`?`always`:j?`input-change`:!1;let[K,st]=St({controlled:i,default:tt?r??o:r,name:`Combobox`,state:`selectedValue`}),ut=Z.useMemo(()=>k===null?()=>!0:k===void 0?Gr(xe,F):k,[k,xe,F]),ft=ne(()=>rt?s??``:nt?Re(K,F):``).current,[vt,yt]=St({controlled:c,default:ft,name:`Combobox`,state:`inputValue`}),[q,bt]=St({controlled:d,default:f,name:`Combobox`,state:`open`}),J=Be(D),xt=De??String(vt).trim(),Tt=nt?Re(K,F):``,Et=nt&&!Se&&xt!==``&&Tt.length===xt.length&&xe.contains(Tt,xt),Dt=Et?``:xt,Ot=it&&at&&Et,kt=Z.useMemo(()=>D?J?D.flatMap(e=>e.items):D:o,[D,J]),Nt=Z.useMemo(()=>{if(O&&!Ot)return O;if(!D)return o;if(J){let e=D,t=[],n=0;for(let r of e){if(R>-1&&n>=R)break;let e=R>-1?R-n:1/0,i=Dt===``?r.items.slice(0,e):[];if(Dt!==``)for(let t of r.items){if(i.length>=e)break;ut(t,Dt,F)&&i.push(t)}if(i.length>0){let e={...r,items:i};t.push(e),n+=i.length}}return t}if(Dt===``)return R>-1?kt.slice(0,R):kt;let e=[];for(let t of kt){if(R>-1&&e.length>=R)break;ut(t,Dt,F)&&e.push(t)}return e},[O,Ot,D,J,Dt,R,ut,F,kt]),Lt=Z.useMemo(()=>J?Nt.flatMap(e=>e.items):Nt,[Nt,J]),Y=ne(()=>{let e=null;return re&&q&&it&&m!==`none`&&(e=pt(Lt,K,L,tt)),new Me({id:be,labelId:void 0,selectedValue:K,open:q,items:D,selectionMode:m,listRef:ke,labelsRef:Ae,popupRef:je,emptyRef:He,inputRef:Ne,startDismissRef:Fe,endDismissRef:Le,keyboardActiveRef:Ue,chipsContainerRef:Ge,clearRef:Ke,valuesRef:Xe,pointerDownItemRef:Ze,selectionEventRef:qe,name:et,form:y,disabled:Qe,readOnly:x,required:S,grid:w,virtualized:te,openOnInputClick:A,itemToStringLabel:F,isItemEqualToValue:L,modal:ae,autoHighlight:ot,submitOnItemClick:B,hasInputValue:rt,mounted:!1,forceMounted:!1,transitionStatus:`idle`,inline:re,activeIndex:null,selectedIndex:e,popupProps:{},listProps:{},inputProps:{},triggerProps:{},itemProps:p,positionerElement:null,listElement:null,popupId:void 0,triggerElement:null,inputElement:null,inputGroupElement:null,popupSide:null,openMethod:null,inputInsidePopup:!0,inputOwnsFormValue:m===`none`,onOpenChangeComplete:E,setOpen:E,setInputValue:E,setSelectedValue:E,setIndices:E,handleSelection:E,forceMount:E,requestSubmit:E})}).current,zt=m===`none`?vt:K,Bt=Z.useMemo(()=>m===`none`?zt:Array.isArray(K)?K.map(e=>W(e,I)):W(K,I),[zt,I,m,K]),Ut=g(_),Gt=g(n),Kt=G(Y,$.activeIndex),qt=G(Y,$.selectedIndex),Yt=G(Y,$.positionerElement),Xt=G(Y,$.listElement),Zt=G(Y,$.triggerElement),Qt=G(Y,$.inputElement),$t=G(Y,$.inputGroupElement),en=G(Y,$.inline),tn=G(Y,$.inputInsidePopup),nn=G(Y,$.inputOwnsFormValue),rn=Ee(Zt),{mounted:an,setMounted:on,transitionStatus:sn}=gt(q),{openMethod:cn,triggerProps:ln}=he(q),un=g(()=>Bt);jt(tn?rn:Ne,be,zt,un,!Qe,v);let dn=g(()=>{D?Ae.current=Lt.map(e=>Re(e,F)):Y.set(`forceMounted`,!0)}),fn=g((e,t,n)=>{if(t===-1){if(Je.current===Qr)return;Je.current=Qr}else Je.current={value:e,index:t};Ut(e,Ht(n,void 0,{index:t}))}),pn=g(e=>{Y.update(e);let t=e.activeIndex;if(t===void 0)return;let n=e.type||`none`;t===null?fn(void 0,-1,n):fn(Xe.current[t],t,n)}),mn=g((t,n)=>{if(We.current=n.reason===Pt,e.onInputValueChange?.(t,n),!n.isCanceled){if(n.reason===`input-change`){q&&De!==null&&Oe(null);let e=n.event,r=e.inputType;if(e.type===`compositionend`||r!=null&&r!==``&&r!==`insertReplacementText`){let e=t.trim()!==``;e&&Ce(!0),Ye.current={hasQuery:e};let n=Y.state.listElement;if(!Y.state.virtualized&&n){let e=je.current;for(let t of M(n.firstElementChild??n)){if(!h(t)||(e?!Mt(e,t):t.getAttribute(`role`)===`dialog`))break;if(Xr(t)){t.scrollTop=0;break}}}e&&ot&&Y.state.activeIndex==null&&(q||en)&&Y.set(`activeIndex`,0)}}else n.reason===`input-clear`&&t===``&&Y.state.inputInsidePopup&&(Ye.current={hasQuery:!1,selection:!0});yt(t)}}),hn=g((t,n)=>{if(q!==t&&(n.reason===`escape-key`&&it&&Lt.length===0&&!He.current&&n.allowPropagation(),e.onOpenChange?.(t,n),!n.isCanceled&&(t&&tn&&!en&&De!==null&&(Ce(!1),Oe(null),vt!==``&&n.reason!==`input-change`&&mn(``,X(Pt,n.event))),!t&&Se&&(nt?(en||Oe(xt),xt===``&&Ce(!1)):tt&&(en||Oe(xt),tn&&pn({activeIndex:null}),(!tn||en)&&mn(``,X(Pt,n.event)))),bt(t),!t&&tn&&(n.reason===`focus-out`||n.reason===`outside-press`)&&(pe(!0),me(!1),_e===`onBlur`)))){let e=m===`none`?vt:K;ve.commit(e)}}),gn=g((e,t)=>{a?.(e,t),!t.isCanceled&&(st(e),(m===`none`&&je.current&&ie||nt&&!Y.state.inputInsidePopup)&&mn(Re(e,F),X(t.reason,t.event)))}),_n=g((e,t)=>{let n=wt(e),r=qe.current??e;qe.current=null;let i=X(Wt,r),a=n?.closest(`a`)?.getAttribute(`href`);if(a){a.startsWith(`#`)&&hn(!1,i);return}if(tt){let e=Array.isArray(K)?K:[];if(gn(oe(e,t,L)?ge(e,t,L):[...e,t],i),i.isCanceled||!(Ne.current&&Ne.current.value.trim()!==``))return;Y.state.inputInsidePopup?mn(``,X(Pt,i.event)):hn(!1,i)}else{if(gn(t,i),i.isCanceled)return;hn(!1,i)}}),vn=g(()=>{let e=ve.inputRef.current?.form??Y.state.inputElement?.form;e&&typeof e.requestSubmit==`function`&&e.requestSubmit()}),yn=g(()=>{if(on(!1),Gt?.(!1),Ce(!1),Oe(null),pn(m===`none`?{activeIndex:null,selectedIndex:null}:{activeIndex:null}),tt&&Ne.current&&Ne.current.value!==``&&!We.current&&mn(``,X(Pt)),nt)if(Y.state.inputInsidePopup)Ne.current&&Ne.current.value!==``&&mn(``,X(Pt));else{let e=Re(K,F);Ne.current&&Ne.current.value!==e&&mn(e,X(e===``?Pt:It))}}),bn=Z.useMemo(()=>en&&Yt?{current:Yt.closest(`[role="dialog"]`)}:je,[en,Yt]);mt({enabled:!e.actionsRef,open:q,ref:bn,onComplete(){q||yn()}}),Z.useImperativeHandle(e.actionsRef,()=>({unmount:yn}),[yn]),u(function(){q||(Ze.current=null,m!==`none`&&pn({selectedIndex:pt(it?kt:Xe.current,K,L,tt)}))},[q,K,m,tt,it,kt,L,pn]),u(()=>{D&&(Xe.current=Lt,ke.current.length=Lt.length)},[D,Lt]),u(()=>{let e=Ye.current;if(e){let t=q||en||Y.state.positionerElement?.hidden===!1;if(e.hasQuery)ot&&t&&Y.set(`activeIndex`,0),Ye.current=null;else if(String(vt).trim()===``&&(Ye.current=null,t)){let t=e.selection;ot===`always`&&!t&&Y.state.selectionMode===`none`&&Y.set(`activeIndex`,0),queueMicrotask(()=>{if(!Y.state.open&&!Y.state.inline||Ne.current&&Ne.current.value.trim()!==``)return;let e=Y.state.selectedValue,n=Y.state.selectionMode===`multiple`,r=n&&Array.isArray(e)?e[e.length-1]:e,i=Y.state.selectionMode!==`none`&&r!=null;if(i||t){let t=it||at?Lt:Xe.current;Y.set(`activeIndex`,i?pt(t,e,Y.state.isItemEqualToValue,n):null)}else ot===`always`&&Y.set(`activeIndex`,0)})}}if(!q&&!en)return;let t=it||at?Lt:Xe.current,n=Y.state.activeIndex;if(n==null){if(ot===`always`&&t.length>0){Y.set(`activeIndex`,0);return}fn(void 0,-1,It);return}if(n>=t.length){fn(void 0,-1,It),Y.set(`activeIndex`,null);return}let r=t[n],i=Je.current.value,a=i!==Zr&<(r,i,Y.state.isItemEqualToValue);(Je.current.index!==n||!a)&&fn(r,n,It)},[Kt,ot,fn,at,it,Lt,en,q,Y,vt]),u(()=>{if(m===`none`){U(String(vt)!==``);return}U(tt?Array.isArray(K)&&K.length>0:K!=null)},[U,m,vt,K,tt]),Z.useEffect(()=>{it&&ot&&Lt.length===0&&pn({activeIndex:null})},[it,ot,Lt.length,pn]);function xn(e){let t=H.initialValue;return Array.isArray(e)&&Array.isArray(t)?!$e(e,t,(e,t)=>lt(e,t,L)):e!==t}_t(xt,()=>{!q||xt===``||xt===String(ft)||Ce(!0)});function Sn(){let e=Re(K,F);vt!==e&&mn(e,X(It))}_t(K,()=>{m!==`none`&&(V(et),le(xn(K)),ve.change(K),nt&&!rt&&!tn&&Sn())}),_t(vt,()=>{m===`none`&&(V(et),le(vt!==H.initialValue),ve.change(vt))}),_t(D,()=>{!nt||rt||tn||Se||Sn()});let Cn=we({open:en?!0:q,onOpenChange:hn,elements:{reference:tn?Zt:Qt,floating:Yt}}),wn=w?`grid`:`listbox`,Tn=q||en,En=Tn?`true`:`false`,Dn=Z.useMemo(()=>{let e=Qt?.tagName===`INPUT`,t=Qt==null||e,n=t||Tn,r=t?{autoComplete:`off`,spellCheck:`false`,autoCorrect:`off`,autoCapitalize:`none`}:{};return n&&(r.role=`combobox`,r[`aria-expanded`]=En,r[`aria-haspopup`]=wn,r[`aria-controls`]=Tn?Xt?.id:void 0,r[`aria-autocomplete`]=se),{reference:r,floating:{role:`presentation`}}},[Qt,Tn,En,wn,Xt?.id,se]),On=Pe(Cn,{enabled:!x&&!Qe&&A,event:`mousedown-only`,toggle:!1,touchOpenDelay:tn?0:100,reason:Ft}),kn=ct(Cn,{enabled:!x&&!Qe&&!en,outsidePressEvent:{mouse:`sloppy`,touch:`intentional`},bubbles:en?!0:void 0,outsidePress(e){let t=wt(e);return!Mt(Zt,t)&&!Mt(Ke.current,t)&&!Mt(Ge.current,t)&&!Mt($t,t)}}),An=ue(Cn,{enabled:!x&&!Qe,id:be,listRef:ke,activeIndex:Kt,selectedIndex:qt,virtual:!0,loopFocus:P,allowEscape:P&&!ot,focusItemOnOpen:Se||m===`none`&&!ot?!1:`auto`,focusItemOnHover:ee,resetOnPointerLeave:!N,orientation:w?`horizontal`:void 0,rtl:ye===`rtl`,disabledIndices:o,grid:w?Nr:void 0,onNavigate(e,t){!t&&!q||sn===`ending`||pn(t?{activeIndex:e,type:Ue.current?Vt:Jt}:{activeIndex:e})}}),jn=Z.useMemo(()=>l(An.reference,{onKeyDown(e){w&&Y.state.activeIndex==null&&(e.key===`ArrowLeft`||e.key===`ArrowRight`)&&e.preventBaseUIHandler()}},kn.reference,On.reference,Dn.reference),[An.reference,kn.reference,On.reference,Dn.reference,w,Y]),Mn=Z.useMemo(()=>l(Te,kn.floating),[kn.floating]),Nn=Z.useMemo(()=>l(An.floating,Dn.floating),[An.floating,Dn.floating]),Pn=Z.useMemo(()=>{let e=An.item;return e?{...e,onFocus:void 0}:p},[An.item]);dt(()=>{Y.update({inline:re,popupProps:Mn,listProps:Nn,inputProps:jn,triggerProps:ln,itemProps:Pn,setOpen:hn,setInputValue:mn,setSelectedValue:gn,setIndices:pn,handleSelection:_n,forceMount:dn,requestSubmit:vn,onOpenChangeComplete:Gt})}),u(()=>{Y.update({id:be,selectedValue:K,open:q,mounted:an,transitionStatus:sn,items:D,inline:re,popupProps:Mn,listProps:Nn,inputProps:jn,triggerProps:ln,openMethod:cn,itemProps:Pn,selectionMode:m,name:et,form:y,disabled:Qe,readOnly:x,required:S,grid:w,virtualized:te,openOnInputClick:A,itemToStringLabel:F,modal:ae,autoHighlight:ot,isItemEqualToValue:L,submitOnItemClick:B,hasInputValue:rt,inputOwnsFormValue:m===`none`&&(re||!Y.state.inputInsidePopup)})},[Y,be,K,q,an,sn,D,Mn,Nn,jn,Pn,cn,ln,m,et,Qe,x,S,w,te,A,F,ae,L,B,rt,re,ot,y]);let Fn=T(C,ve.inputRef),In=Z.useMemo(()=>({query:xt,hasItems:it,filteredItems:Nt,flatFilteredItems:Lt}),[xt,it,Nt,Lt]),Ln=Z.useMemo(()=>Array.isArray(zt)?``:W(zt,I),[zt,I]),Rn=tt&&Array.isArray(K)&&K.length>0,zn=tt||m===`none`&&nn?void 0:et,Bn=Z.useMemo(()=>!tt||!Array.isArray(K)||!et?null:K.map(e=>{let t=W(e,I);return(0,Q.jsx)(`input`,{type:`hidden`,form:y,name:et,value:t,disabled:Qe},t)}),[tt,K,y,et,I,Qe]),Vn=(0,Q.jsxs)(Z.Fragment,{children:[e.children,(0,Q.jsx)(`input`,{...ve.getValidationProps(Qe,{onFocus(){if(tn){Zt?.focus();return}(Ne.current||Zt)?.focus()},onChange(e){if(e.nativeEvent.defaultPrevented||Qe||x)return;let t=e.currentTarget.value,n=t.toLowerCase(),r=X(It,e.nativeEvent),i=()=>Xe.current.findIndex(e=>W(e,I).toLowerCase()===n||Re(e,F).toLowerCase()===n);function a(){if(tt)return;if(m===`none`){mn(t,r);return}let e=i();e===-1&&(e=Xe.current.findIndex((e,t)=>{let r=Ae.current[t];return r!=null&&r.toLowerCase()===n}));let a=e===-1?void 0:Xe.current[e];a!=null&&gn?.(a,r)}nt&&(dn(),D&&i()===-1&&Y.set(`forceMounted`,!0)),queueMicrotask(a)}}),id:be&&zn==null?`${be}-hidden-input`:void 0,form:y,name:zn,autoComplete:z,disabled:Qe,required:S&&!Rn,readOnly:x,value:Ln,ref:Fn,style:zn?ze:Ve,tabIndex:-1,"aria-hidden":!0,suppressHydrationWarning:!0}),Bn]});return(0,Q.jsx)(Pr.Provider,{value:Y,children:(0,Q.jsx)(Fr.Provider,{value:Cn,children:(0,Q.jsx)(Lr.Provider,{value:it,children:(0,Q.jsx)(Ir.Provider,{value:In,children:(0,Q.jsx)(Rr.Provider,{value:vt,children:Vn})})})})})}var ei={...V,...Et,popupSide:e=>e?{"data-popup-side":e}:null,listEmpty:e=>e?{"data-list-empty":``}:null};function ti(e){let t=G(e,$.mounted),n=G(e,$.popupSide),r=G(e,$.positionerElement);return t&&r?n:null}function ni(){return Vr().filteredItems.length===0}function ri(e){return e===`rtl`?[`ArrowRight`,`ArrowLeft`]:[`ArrowLeft`,`ArrowRight`]}function ii(e,t){let n=e>=t-1?t-2:e;return n>=0?n:void 0}function ai(e,t,n){let r=e.state.listRef.current[t];r&&(e.state.selectionEventRef.current=n,r.click(),e.state.selectionEventRef.current=null)}var oi=Z.forwardRef(function(e,t){let{render:n,className:r,nativeButton:i=!0,disabled:a=!1,id:o,style:c,...l}=e,{state:u,disabled:f,setTouched:p,setFocused:h,validationMode:_,validation:v}=At(),{labelId:y}=kt(),b=zr(),x=G(b,$.selectionMode),S=G(b,$.disabled),C=G(b,$.readOnly),w=G(b,$.required),T=G(b,$.positionerElement),E=G(b,$.listElement),D=G(b,$.popupId),O=G(b,$.triggerProps),k=G(b,$.inputInsidePopup),A=G(b,$.id),j=G(b,$.labelId),M=G(b,$.open),N=G(b,$.selectedValue),ee=G(b,$.activeIndex),P=G(b,$.selectedIndex),F=G(b,$.hasSelectedValue),I=Br(),L=Hr(),te=_e(),ne=f||S||a,re=ni(),ie=ti(b);Ct({id:k?o:void 0});let ae=k?o??A:o,oe=st(y,j),R;M&&k?R=D??Wr(A):M&&(R=E?.id);let se=Z.useRef(``);function z(e){se.current=e.pointerType}let{reference:ce}=tt(I,{enabled:!M&&!C&&!S&&x===`single`,listRef:b.state.labelsRef,activeIndex:ee,selectedIndex:P,onMatch(e){let t=b.state.valuesRef.current[e];t!==void 0&&b.state.setSelectedValue(t,X(It))}}),{reference:B}=Pe(I,{enabled:!C&&!S,event:`mousedown`}),{buttonRef:V,getButtonProps:le}=d({native:i,disabled:ne}),H={...u,open:M,disabled:ne,popupSide:ie,listEmpty:re,placeholder:x===`none`?!1:!F};return s(`button`,e,{ref:[t,V,g(e=>{b.set(`triggerElement`,e)})],state:H,props:[O,B,ce,{id:ae,tabIndex:k?0:-1,role:k?`combobox`:void 0,"aria-expanded":M,"aria-haspopup":k?`dialog`:`listbox`,"aria-controls":R,"aria-required":k&&w||void 0,"aria-labelledby":oe,onPointerDown:z,onPointerEnter:z,onFocus(){h(!0),!(ne||C)&&te.start(0,b.state.forceMount)},onBlur(e){if(!Mt(T,e.relatedTarget)&&(p(!0),h(!1),_===`onBlur`)){let e=x===`none`?L:N;v.commit(e)}},onMouseDown(e){if(ne||C||(k||I.set(`domReferenceElement`,e.currentTarget),b.state.forceMount(),se.current!==`touch`&&(b.state.inputRef.current?.focus(),k||e.preventDefault()),M))return;let t=m(e.currentTarget);function n(e){let t=b.state.triggerElement;if(!t)return;let n=wt(e),r=b.state.positionerElement,i=b.state.listElement;Mt(t,n)||Mt(r,n)||Mt(i,n)||Ne(e,t)||b.state.setOpen(!1,X(Gt,e))}k&&t.addEventListener(`mouseup`,n,{once:!0})},onKeyDown(e){C||(e.key===`ArrowDown`||e.key===`ArrowUp`)&&(ye(e),b.state.setOpen(!0,X(Lt,e.nativeEvent)),b.state.inputRef.current?.focus())}},v.getValidationProps(ne,l),le],stateAttributesMapping:ei})}),si=Z.createContext(void 0);function ci(){return Z.useContext(si)}var li=Z.createContext(void 0);function ui(e){let t=Z.useContext(li);if(t===void 0&&!e)throw Error(c(21));return t}var di=Z.forwardRef(function(e,t){let n=zr(),{buttonRef:r,getButtonProps:i}=d({native:!1}),a=T(t,r);function o(e){n.state.setOpen(!1,X(Ut,e.nativeEvent,e.currentTarget))}return(0,Q.jsx)(`span`,{ref:a,...i({onClick:o}),"aria-label":`Dismiss`,tabIndex:void 0,style:ze})}),fi=Z.forwardRef(function(e,t){let{render:n,className:r,disabled:i=!1,id:a,style:o,...c}=e,{state:l,disabled:u,setTouched:d,setFocused:f,validationMode:p,validation:m}=At(),{labelId:h}=kt(),_=ci(),v=!!ui(!0),y=zr(),b=Hr(),x=Ie(),S=G(y,$.required),C=G(y,$.disabled),w=G(y,$.readOnly),T=G(y,$.name),E=G(y,$.form),D=G(y,$.selectionMode),O=G(y,$.autoHighlight),k=G(y,$.inputProps),A=G(y,$.triggerProps),j=G(y,$.open),M=G(y,$.mounted),N=G(y,$.selectedValue),ee=G(y,$.id),P=G(y,$.inline),F=G(y,$.modal),I=!!O,L=ti(y),te=u||C||i,ne=ni(),re=v||P,ie=!re||F,ae=zt(a??(re?void 0:ee)),oe=v?Tt:l,[R,z]=Z.useState(null),ce=Z.useRef(!1),B=Z.useRef(null),V=Z.useRef(!1),le=D===`none`&&!v,H=g(e=>{let t=v||y.state.inline;t&&!y.state.hasInputValue&&y.state.setInputValue(``,X(It)),y.update({inputElement:e,inputInsidePopup:t,inputOwnsFormValue:le})}),ue=v?c:m.getValidationProps(te,c);function U(){y.state.setIndices({activeIndex:null,selectedIndex:null,type:y.state.keyboardActiveRef.current?Vt:Jt})}function de(){y.state.keyboardActiveRef.current=!1}let fe={...oe,open:j,disabled:te,readOnly:w,popupSide:L,listEmpty:ne};function me(e){if(!_)return;let t,{highlightedChipIndex:n}=_,r=_.chipsRef.current.length,[i,a]=ri(x);return n===void 0?(e.key===i&&(e.currentTarget.selectionStart??0)===0&&N.length>0&&(e.preventDefault(),t=r>0?r-1:void 0),t):(e.key===i?(e.preventDefault(),t=n>0?n-1:void 0):e.key===a?(e.preventDefault(),t=n0){let t=_.chipsRef.current.length,n=t>0?t-1:N.length-1,r=N.filter((e,t)=>t!==n);U(),y.state.setSelectedValue(r,X(It,e.nativeEvent));return}let i=_?.highlightedChipIndex!==void 0,a=me(e);if(_?.setHighlightedChipIndex(a),a===void 0?i&&y.state.inputRef.current?.focus():_?.chipsRef.current[a]?.focus(),e.which!==229&&e.key===`Enter`&&j){let t=y.state.activeIndex,n=e.nativeEvent;if(t===null){if(P)return;y.state.setOpen(!1,X(It,n));return}ye(e),ai(y,t,n)}},onPointerMove:de,onPointerDown:de},ue],stateAttributesMapping:ei}),ge=v?(0,Q.jsx)(Ot.Provider,{value:Dt,children:he}):he;return(0,Q.jsxs)(Z.Fragment,{children:[j&&ie&&(0,Q.jsx)(di,{ref:y.state.startDismissRef}),ge]})});function pi(e,t,n,r,i){if(e.baseUIHandlerPrevented||r)return;let a=wt(e.nativeEvent),o=b(a)?a:null;o!==e.currentTarget&&(i?.(o)||ce(o))||(e.preventDefault(),!n&&(t.state.inputRef.current?.focus(),t.state.openOnInputClick&&t.state.setOpen(!0,X(Ft,e.nativeEvent))))}var mi={...Ae,...U},hi=Z.forwardRef(function(e,t){let{render:n,className:r,disabled:i=!1,nativeButton:a=!0,keepMounted:o=!1,style:c,...l}=e,{disabled:u}=At(),f=zr(),p=G(f,$.selectionMode),m=G(f,$.disabled),h=G(f,$.readOnly),g=G(f,$.open),_=G(f,$.selectedValue),v=G(f,$.hasSelectionChips),y=Hr(),b=!1;b=p===`none`?y!==``:p===`single`?_!=null:v;let x=u||m||i,{buttonRef:S,getButtonProps:C}=d({native:a,disabled:x}),{mounted:w,transitionStatus:T,setMounted:E}=gt(b),D={disabled:x,visible:b,open:g,transitionStatus:T};mt({open:b,ref:f.state.clearRef,onComplete(){b||E(!1)}});let O=s(`button`,e,{state:D,ref:[t,S,f.state.clearRef],props:[{tabIndex:-1,children:`x`,onMouseDown(e){e.preventDefault()},onClick(e){if(x||h)return;let t=f.state.keyboardActiveRef.current?Vt:Jt;f.state.setInputValue(``,X(Nt,e.nativeEvent)),p===`none`?f.state.setIndices({activeIndex:null,type:t}):(f.state.setSelectedValue(Array.isArray(_)?[]:null,X(Nt,e.nativeEvent)),f.state.setIndices({activeIndex:null,selectedIndex:null,type:t})),f.state.inputRef.current?.focus()}},l,C],stateAttributesMapping:mi});return o||w?O:null}),gi=Z.createContext(null);function _i(){return Z.useContext(gi)}function vi(e){let{children:t}=e,{filteredItems:n}=Vr(),r=_i(),i=r?r.items:n;return(0,Q.jsx)(Z.Fragment,{children:i.map(t)})}var yi=Z.forwardRef(function(e,t){var n;let{render:r,className:i,style:a,children:o,...c}=e,l=zr(),u=Br(),d=!!ui(!0),{filteredItems:f,hasItems:p}=Vr(),m=G(l,$.selectionMode),h=G(l,$.grid),_=G(l,$.listProps),v=G(l,$.virtualized),y=G(l,$.forceMounted),b=m===`multiple`,x=f.length===0,S=g(e=>{l.set(`positionerElement`,e)}),C=g(e=>{l.set(`listElement`,e)}),w=Z.useMemo(()=>typeof o==`function`?n||=(0,Q.jsx)(vi,{children:o}):o,[o]),T={empty:x},E=u.useState(`floatingId`),D=s(`div`,e,{state:T,ref:[t,C,d?null:S],props:[_,{children:w,tabIndex:-1,id:E,role:h?`grid`:`listbox`,"aria-multiselectable":b?`true`:void 0,onKeyDown(e){if(!(l.state.disabled||l.state.readOnly)&&e.key===`Enter`){let t=l.state.activeIndex;if(t==null)return;ye(e),ai(l,t,e.nativeEvent)}},onKeyDownCapture(){l.state.keyboardActiveRef.current=!0},onPointerMoveCapture(){l.state.keyboardActiveRef.current=!1}},c]});if(v)return D;let O=p&&!y?void 0:l.state.labelsRef;return(0,Q.jsx)(z,{elementsRef:l.state.listRef,labelsRef:O,children:D})}),bi=`⁠`;function xi(e){let t=e.ownerDocument.createTreeWalker(e,NodeFilter.SHOW_TEXT),n=null;for(;t.nextNode();){let e=t.currentNode;e.nodeValue!==``&&(n=e)}return n}function Si(){let e=_e(),t=Z.useRef(null);return Z.useEffect(()=>{if(Ge)return;let n=t.current;if(n==null)return;let r=xi(n);if(r==null)return;let i=r.data,a=`${i}${bi}`;return r.nodeValue=a,e.start(200,()=>{r.nodeValue===a&&(r.nodeValue=i)}),()=>{e.clear(),r.nodeValue===a&&(r.nodeValue=i)}},[t,e]),t}var Ci=Z.createContext(void 0);function wi(){let e=Z.useContext(Ci);if(e===void 0)throw Error(c(20));return e}var Ti=Z.forwardRef(function(e,t){let{keepMounted:n=!1,...r}=e,i=zr(),a=G(i,$.mounted),o=G(i,$.forceMounted);return a||n||o?(0,Q.jsx)(Ci.Provider,{value:n,children:(0,Q.jsx)(et,{ref:t,...r})}):null}),Ei=Z.forwardRef(function(e,t){let{render:n,className:r,anchor:i,positionMethod:a,side:o,align:s,sideOffset:c,alignOffset:l,collisionBoundary:d=`clipping-ancestors`,collisionPadding:f,arrowPadding:p,sticky:m,disableAnchorTracking:h=!1,collisionAvoidance:_=Je,style:v,...y}=e,b=zr(),x=Br(),S=wi(),C=G(b,$.modal),w=G(b,$.open),T=G(b,$.mounted),E=G(b,$.openMethod),D=G(b,$.positionerElement),O=G(b,$.triggerElement),k=G(b,$.inputElement),A=G(b,$.inputGroupElement),j=G(b,$.inputInsidePopup),M=G(b,$.transitionStatus),N=ni(),ee=Ke({anchor:i??(j?O:A??k),floatingRootContext:x,positionMethod:a,mounted:T,side:o,sideOffset:c,align:s,alignOffset:l,arrowPadding:p,collisionBoundary:d,collisionPadding:f,sticky:m,disableAnchorTracking:h,keepMounted:S,collisionAvoidance:_,lazyFlip:!0});Ze(w&&C,E===`touch`,D,O);let P={open:w,side:ee.side,align:ee.align,anchorHidden:ee.anchorHidden,empty:N};u(()=>{b.set(`popupSide`,ee.side)},[b,ee.side]);let F=g(e=>{b.set(`positionerElement`,e)}),I=it(e,P,{styles:ee.positionerStyles,transitionStatus:M,props:y,refs:[t,F],hidden:!T,inert:!w});return(0,Q.jsxs)(li.Provider,{value:ee,children:[T&&C&&(0,Q.jsx)(ae,{inert:xe(!w),cutout:A??k??O}),I]})}),Di={...be,...Ae},Oi=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,initialFocus:a,finalFocus:o,...c}=e,l=zr(),d=ui(),f=Br(),p=G(l,$.mounted),m=G(l,$.open),h=G(l,$.openMethod),g=G(l,$.popupProps),_=G(l,$.transitionStatus),v=G(l,$.inputInsidePopup),y=G(l,$.inputElement),b=G(l,$.modal),x=G(l,$.id),S=ni(),C=c.id??(v?Wr(x):void 0);u(()=>(l.set(`popupId`,l.state.popupRef.current?.id||C),()=>{l.set(`popupId`,void 0)}),[l,C]),mt({open:m,ref:l.state.popupRef,onComplete(){m&&l.state.onOpenChangeComplete(!0)}});let w=s(`div`,e,{state:{open:m,side:d.side,align:d.align,anchorHidden:d.anchorHidden,transitionStatus:_,empty:S},ref:[t,l.state.popupRef],props:[g,{id:C,role:v?`dialog`:`presentation`,onFocus(e){let t=wt(e.nativeEvent);h!==`touch`&&(Mt(l.state.listElement,t)||t===e.currentTarget)&&l.state.inputRef.current?.focus()}},je(_),c],stateAttributesMapping:Di}),T=a===void 0?v?e=>e===`touch`?l.state.popupRef.current:y:!1:a,E;E=o??(v?void 0:!1);let D=!v||b;return(0,Q.jsx)(Le,{context:f,disabled:!p,modal:D,openInteractionType:h,initialFocus:T,returnFocus:E,getInsideElements:()=>[l.state.startDismissRef.current,l.state.endDismissRef.current],children:(0,Q.jsxs)(Z.Fragment,{children:[w,D&&(0,Q.jsx)(di,{ref:l.state.endDismissRef})]})})}),ki=Z.createContext(void 0);function Ai(){let e=Z.useContext(ki);if(!e)throw Error(c(19));return e}var ji=Z.createContext(!1);function Mi(){return Z.useContext(ji)}var Ni=e(i(),1);function Pi(e){let{componentProps:t,forwardedRef:n,virtualized:r,indexFromFilter:i}=e,{render:a,className:o,style:c,value:l=null,index:f,disabled:p=!1,nativeButton:m=!1,...h}=t,g=Z.useRef(null),_=ve({guess:!0,index:f,textRef:g}),v=zr(),y=Mi(),b=Ur(),x=G(v,$.selectionMode),S=G(v,$.disabled),C=G(v,$.readOnly),w=G(v,$.isItemEqualToValue),T=S||p,E=x!==`none`,D=f??i??_.index,O=D!==-1,k=G(v,$.id),A=G(v,$.isActive,D),j=G(v,$.isSelected,l),M=G(v,$.itemProps),N=Z.useRef(null),ee=k!=null&&O?`${k}-${D}`:void 0,P=j&&E;u(()=>{if(!(O&&(r||f!=null)))return;let e=v.state.listRef.current;return e[D]=N.current,()=>{delete e[D]}},[O,r,D,f,v]),u(()=>{if(!O||b)return;let e=v.state.valuesRef.current;return e[D]=l,()=>{delete e[D]}},[O,b,D,l,v]),u(()=>{if(!O||b)return;let e=v.state.selectedValue;lt(l,Array.isArray(e)?e[e.length-1]:e,w)&&v.set(`selectedIndex`,D)},[O,b,v,D,l,w]);let{getButtonProps:F,buttonRef:I}=d({disabled:T,focusableWhenDisabled:!0,native:m,composite:!0}),L={disabled:T,selected:P,highlighted:A};function te(e){function t(){v.state.handleSelection(e,l)}v.state.submitOnItemClick?(Ni.flushSync(t),v.state.requestSubmit()):t()}let ne={id:ee,role:y?`gridcell`:`option`,"aria-selected":E?P:void 0,tabIndex:void 0,onPointerDownCapture(e){e.isPrimary&&(v.state.pointerDownItemRef.current=e.currentTarget),e.preventDefault()},onMouseDown(e){e.preventDefault()},onClick(e){T||C||te(e.nativeEvent)},onMouseUp(e){let t=v.state.pointerDownItemRef.current===e.currentTarget;v.state.pointerDownItemRef.current=null,!(T||C||e.button!==0||t||!A)&&te(e.nativeEvent)}},re=s(`div`,t,{ref:[I,n,_.ref,N],state:L,props:[M,ne,h,F]}),ie=Z.useMemo(()=>({selected:P,textRef:g}),[P,g]);return(0,Q.jsx)(ki.Provider,{value:ie,children:re})}function Fi(e){let{componentProps:t,forwardedRef:n}=e,r=G(zr(),$.isItemEqualToValue),{flatFilteredItems:i}=Vr();return(0,Q.jsx)(Pi,{componentProps:t,forwardedRef:n,virtualized:!0,indexFromFilter:ke(i,t.value??null,r)})}var Ii=Z.memo(Z.forwardRef(function(e,t){let n=G(zr(),$.virtualized);return n&&e.index==null?(0,Q.jsx)(Fi,{componentProps:e,forwardedRef:t}):(0,Q.jsx)(Pi,{componentProps:e,forwardedRef:t,virtualized:n,indexFromFilter:void 0})})),Li=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,children:a,...o}=e,{filteredItems:c}=Vr(),l=zr(),u=Si(),d=c.length===0?a:null;return s(`div`,e,{ref:[t,l.state.emptyRef,u],props:[{children:d,role:`status`,"aria-live":`polite`,"aria-atomic":!0},o]})});function Ri(e){let{multiple:t=!1,defaultValue:n,value:r,onValueChange:i,autoComplete:a,...o}=e;return(0,Q.jsx)($r,{...o,selectionMode:t?`multiple`:`single`,selectedValue:r,defaultSelectedValue:n,onSelectedValueChange:i,formAutoComplete:a})}var zi=Z.forwardRef(function(e,t){let{selected:n}=Ai();return e.keepMounted||n?(0,Q.jsx)(Bi,{...e,ref:t}):null}),Bi=Z.memo(Z.forwardRef((e,t)=>{let{render:n,className:r,style:i,keepMounted:a,...o}=e,{selected:c}=Ai(),l=Z.useRef(null),{transitionStatus:u,setMounted:d}=gt(c),f=s(`span`,e,{ref:[t,l],state:{selected:c,transitionStatus:u},props:[{"aria-hidden":!0,children:`✔️`},o],stateAttributesMapping:Ae});return mt({open:c,ref:l,onComplete(){c||d(!1)}}),f})),Vi=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,...a}=e,o=zr(),c=G(o,$.open),l=G(o,$.hasSelectionChips),[u,d]=Z.useState(void 0);c&&u!==void 0&&d(void 0);let f=Z.useRef([]),m=s(`div`,e,{ref:[t,o.state.chipsContainerRef],props:[l?{role:`toolbar`}:p,{onMouseDown(e){pi(e,o,o.state.disabled,o.state.readOnly)}},a]}),h=Z.useMemo(()=>({highlightedChipIndex:u,setHighlightedChipIndex:d,chipsRef:f}),[u,d,f]);return(0,Q.jsx)(si.Provider,{value:h,children:(0,Q.jsx)(z,{elementsRef:f,children:m})})}),Hi=Z.createContext(void 0);function Ui(){let e=Z.useContext(Hi);if(!e)throw Error(c(17));return e}var Wi=Z.forwardRef(function(e,t){let{render:n,className:r,style:i,...a}=e,o=zr(),{setHighlightedChipIndex:c,chipsRef:l}=ci(),u=Ie(),d=G(o,$.disabled),f=G(o,$.readOnly),p=G(o,$.selectedValue),{ref:m,index:h}=ve();function g(e){let t=h,[n,r]=ri(u);return e.key===n?(e.preventDefault(),t=h>0?h-1:void 0):e.key===r?(e.preventDefault(),t=ht!==h),X(It,e.nativeEvent))):e.key===`Enter`||e.key===` `?(ye(e),t=void 0):e.key===`ArrowDown`||e.key===`ArrowUp`?(ye(e),o.state.setOpen(!0,X(Lt,e.nativeEvent)),t=void 0):e.key.length===1&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&(t=void 0),t}let _=s(`div`,e,{ref:[t,m],state:{disabled:d},props:[{tabIndex:-1,"aria-disabled":d||void 0,"aria-readonly":f||void 0,onKeyDown(e){if(d||f)return;let t=g(e);Ni.flushSync(()=>{c(t)}),t===void 0?o.state.inputRef.current?.focus():l.current[t]?.focus()}},a]}),v=Z.useMemo(()=>({index:h}),[h]);return(0,Q.jsx)(Hi.Provider,{value:v,children:_})}),Gi=Z.forwardRef(function(e,t){let{render:n,className:r,disabled:i=!1,nativeButton:a=!0,style:o,...c}=e,l=zr(),{index:u}=Ui(),f=G(l,$.disabled),p=G(l,$.readOnly),m=G(l,$.selectedValue),h=G(l,$.isItemEqualToValue),g=f||i,{buttonRef:_,getButtonProps:v}=d({native:a,disabled:g||p,focusableWhenDisabled:!0}),y={disabled:g};function b(e){let t=l.state.activeIndex;if(t==null)return;let n=ke(l.state.valuesRef.current,e,h);n!==-1&&t===n&&l.state.setIndices({activeIndex:null,type:l.state.keyboardActiveRef.current?Vt:Jt})}function x(e){let t=X(Y,e.nativeEvent),n=m[u];return b(n),l.state.setSelectedValue(m.filter((e,t)=>t!==u),t),l.state.inputRef.current?.focus(),t}return s(`button`,e,{ref:[t,_],state:y,props:[{tabIndex:-1,onMouseDown(e){e.preventDefault()},onClick(e){x(e).isPropagationAllowed||e.stopPropagation()},onKeyDown(e){(e.key===`Enter`||e.key===` `)&&(x(e).isPropagationAllowed||ye(e))}},c,v]})});function Ki({className:e,...t}){return(0,Q.jsx)(`div`,{"data-slot":`input-group`,role:`group`,className:L(`group/input-group relative flex h-9 w-full min-w-0 items-center rounded-4xl border border-input bg-input/30 transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-2xl has-data-[align=block-start]:rounded-2xl has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-[3px] has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[textarea]:rounded-xl has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5`,e),...t})}var qi=_(`flex h-auto cursor-text items-center justify-center gap-2 py-2 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 **:data-[slot=kbd]:rounded-4xl **:data-[slot=kbd]:bg-muted-foreground/10 **:data-[slot=kbd]:px-1.5 [&>svg:not([class*='size-'])]:size-4`,{variants:{align:{"inline-start":`order-first pl-3 has-[>button]:-ml-1 has-[>kbd]:ml-[-0.15rem]`,"inline-end":`order-last pr-3 has-[>button]:-mr-1 has-[>kbd]:mr-[-0.15rem]`,"block-start":`order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-3 [.border-b]:pb-3`,"block-end":`order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-3 [.border-t]:pt-3`}},defaultVariants:{align:`inline-start`}});function Ji({className:e,align:t=`inline-start`,...n}){return(0,Q.jsx)(`div`,{role:`group`,"data-slot":`input-group-addon`,"data-align":t,className:L(qi({align:t}),e),onClick:e=>{e.target.closest(`button`)||e.currentTarget.parentElement?.querySelector(`input`)?.focus()},...n})}var Yi=_(`flex items-center gap-2 rounded-4xl text-sm shadow-none`,{variants:{size:{xs:`h-6 gap-1 px-1.5 [&>svg:not([class*='size-'])]:size-3.5`,sm:``,"icon-xs":`size-6 p-0 has-[>svg]:p-0`,"icon-sm":`size-8 p-0 has-[>svg]:p-0`}},defaultVariants:{size:`xs`}});function Xi({className:e,type:t=`button`,variant:n=`ghost`,size:r=`xs`,...i}){return(0,Q.jsx)(a,{type:t,"data-size":r,variant:n,className:L(Yi({size:r}),e),...i})}function Zi({className:e,...t}){return(0,Q.jsx)(xt,{"data-slot":`input-group-control`,className:L(`flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent`,e),...t})}var Qi=Ri;function $i({className:e,children:t,...n}){return(0,Q.jsxs)(oi,{"data-slot":`combobox-trigger`,className:L(`[&_svg:not([class*='size-'])]:size-4`,e),...n,children:[t,(0,Q.jsx)(We,{className:`pointer-events-none size-4 text-muted-foreground`})]})}function ea({className:e,...t}){return(0,Q.jsx)(hi,{"data-slot":`combobox-clear`,render:(0,Q.jsx)(Xi,{variant:`ghost`,size:`icon-xs`}),className:L(e),...t,children:(0,Q.jsx)(an,{className:`pointer-events-none`})})}function ta({className:e,children:t,disabled:n=!1,showTrigger:r=!0,showClear:i=!1,...a}){return(0,Q.jsxs)(Ki,{className:L(`w-auto`,e),children:[(0,Q.jsx)(fi,{render:(0,Q.jsx)(Zi,{disabled:n}),...a}),(0,Q.jsxs)(Ji,{align:`inline-end`,children:[r&&(0,Q.jsx)(Xi,{size:`icon-xs`,variant:`ghost`,render:(0,Q.jsx)($i,{}),"data-slot":`input-group-button`,className:`group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent`,disabled:n}),i&&(0,Q.jsx)(ea,{disabled:n})]}),t]})}function na({className:e,side:t=`bottom`,sideOffset:n=6,align:r=`start`,alignOffset:i=0,anchor:a,...o}){return(0,Q.jsx)(Ti,{children:(0,Q.jsx)(Ei,{side:t,sideOffset:n,align:r,alignOffset:i,anchor:a,className:`isolate z-50`,children:(0,Q.jsx)(Oi,{"data-slot":`combobox-content`,"data-chips":!!a,className:L(`group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-2xl bg-popover text-popover-foreground shadow-2xl ring-1 ring-foreground/5 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-9 *:data-[slot=input-group]:border-none *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...o})})})}function ra({className:e,...t}){return(0,Q.jsx)(yi,{"data-slot":`combobox-list`,className:L(`no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0`,e),...t})}function ia({className:e,children:t,...n}){return(0,Q.jsxs)(Ii,{"data-slot":`combobox-item`,className:L(`relative flex w-full cursor-default items-center gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,e),...n,children:[t,(0,Q.jsx)(zi,{render:(0,Q.jsx)(`span`,{className:`pointer-events-none absolute right-2 flex size-4 items-center justify-center`}),children:(0,Q.jsx)(de,{className:`pointer-events-none`})})]})}function aa({className:e,...t}){return(0,Q.jsx)(Li,{"data-slot":`combobox-empty`,className:L(`hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex`,e),...t})}function oa({className:e,...t}){return(0,Q.jsx)(Vi,{"data-slot":`combobox-chips`,className:L(`flex min-h-9 flex-wrap items-center gap-1.5 rounded-4xl border border-input bg-input/30 bg-clip-padding px-2.5 py-1.5 text-sm transition-colors focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-[3px] has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1.5 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40`,e),...t})}function sa({className:e,children:t,showRemove:n=!0,removeLabel:r,...i}){return(0,Q.jsxs)(Wi,{"data-slot":`combobox-chip`,className:L(`flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-4xl bg-muted-foreground/10 px-2 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0`,e),...i,children:[t,n&&(0,Q.jsx)(Gi,{render:(0,Q.jsx)(a,{variant:`ghost`,size:`icon-xs`}),className:`-ml-1 text-muted-foreground hover:text-foreground`,"aria-label":r,"data-slot":`combobox-chip-remove`,children:(0,Q.jsx)(an,{className:`pointer-events-none`})})]})}function ca({className:e,...t}){return(0,Q.jsx)(fi,{"data-slot":`combobox-chip-input`,className:L(`min-w-16 flex-1 outline-none`,e),...t})}function la(){return Z.useRef(null)}function ua(e){return e.kind===`create`?e.name:e.tag.name}var da=new Set([`escape-key`,`outside-press`,`focus-out`,`input-blur`]);function fa({entry:e,vocabulary:t,onArtist:n,pendingNames:r,onCommit:i,onClose:a}){let[o,s]=(0,Z.useState)(``),[c,l]=(0,Z.useState)(!0),[u,d]=(0,Z.useState)(``),f=(0,Z.useId)(),p=(0,Z.useRef)(null),m=yt({query:o,vocabulary:t,onArtist:n,pendingNames:r}),h=tr(o),g=h>32,_=m.state===`items`?m.items.filter(e=>!(g&&e.kind===`create`)):[];function v(e){let t=ua(e);i(t),p.current=t,s(``)}function y(e){if(p.current!==null){let t=p.current;if(p.current=null,e===t)return}let t=tr(o),n=tr(e);t<32&&n>=32?d(`Tag name limit reached — 32 characters.`):t<25&&n>=25&&d(`7 characters left.`),s(e)}return(0,Q.jsxs)(Qi,{items:_,value:null,onValueChange:e=>{e&&v(e)},inputValue:o,onInputValueChange:y,itemToStringLabel:ua,autoHighlight:!0,open:c,onOpenChange:(e,t)=>{if(e){l(!0);return}da.has(t.reason)&&(l(!1),a(t.reason===`escape-key`?`escape`:`blur`))},children:[(0,Q.jsxs)(`div`,{className:`flex items-center gap-1`,children:[(0,Q.jsx)(ta,{placeholder:`Tag name`,"aria-label":`Add tag to ${e.name}`,"aria-describedby":f,className:`h-8 w-full max-w-60 text-label`,showTrigger:!1,autoFocus:!0}),h>=25&&(0,Q.jsxs)(`span`,{id:f,className:`text-label tabular-nums ${g?`text-destructive`:h>=32?`text-foreground`:`text-muted-foreground`}`,children:[h,`/`,32]})]}),(0,Q.jsx)(`span`,{"aria-live":`polite`,className:`sr-only`,children:u}),(0,Q.jsxs)(na,{children:[(0,Q.jsx)(ra,{children:_.map(e=>(0,Q.jsx)(ia,{value:e,children:e.kind===`create`?(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(nn,{"aria-hidden":`true`,className:`size-3`}),`Create “`,e.name,`”`]}):e.tag.name},e.kind===`create`?`__create__`:e.tag.id))}),(0,Q.jsx)(aa,{children:m.state===`already-on`?`“${m.name}” is already on this artist`:g?`Tags can be at most 32 characters.`:`Type a name to create a tag`})]})]})}var pa=0;function ma(){return pa+=1,`pending-${pa}`}function ha({entry:e,actions:t,announce:n}){let{vocabulary:r,ensureLoaded:i,remember:o}=cr(),s=(0,Z.useRef)(null),c=(0,Z.useRef)(null),l=(0,Z.useRef)(null),u=(0,Z.useRef)(e.tags.length),[d,f]=(0,Z.useState)([]),[p,m]=(0,Z.useState)(!1),h=e.tags.length+d.length,g=h>=10;(0,Z.useLayoutEffect)(()=>{let t=e.tags.length!==u.current;u.current=e.tags.length;let n=l.current;if(!t||!n||n.kind===`add-button`)return;l.current=null;let r=n.kind===`grow`?e.tags.length-1:n.index;(s.current?.querySelectorAll(`[data-chip-remove]`))?.[r]?.focus()},[e.tags]),(0,Z.useLayoutEffect)(()=>{l.current?.kind!==`add-button`||!c.current||(l.current=null,c.current.focus())},[d,p,e.tags]);function _(r,i){let a=e.tags.length===10,o=e.tags.length-1;l.current=o===0?{kind:`add-button`}:{kind:`chip`,index:Math.min(i,o-1)},t.removeTag(e.id,r.id),n(a?`Removed “${r.name}” from ${e.name}. You can add tags again.`:`Removed “${r.name}” from ${e.name}.`),ee(e.id,r.id).catch(()=>{l.current=null,t.addTag(e.id,r,i),J.error(`Couldn't remove “${r.name}” from ${e.name} — try again.`)})}function v(){i(),m(!0)}function y(t,n){if(t instanceof F){if(t.code===`tag_cap_reached`)return`${e.name} already has 10 tags — remove one first.`;if(t.code===`tag_name_too_long`)return`Tags can be at most 32 characters.`}return`Couldn't add “${n}” to ${e.name} — try again.`}function b(r){let i=h+1>=10,a=ma();f(e=>[...e,{tempId:a,name:r}]),i&&(m(!1),l.current={kind:`grow`}),N(e.id,r).then(r=>{f(e=>e.filter(e=>e.tempId!==a)),t.addTag(e.id,r),o(r),n(i?`Added “${r.name}” to ${e.name}. Max 10 tags reached.`:`Added “${r.name}” to ${e.name}.`)}).catch(e=>{f(e=>e.filter(e=>e.tempId!==a)),i&&(l.current={kind:`add-button`}),J.error(y(e,r))})}return(0,Q.jsxs)(`div`,{ref:s,className:`mt-1 flex flex-wrap items-center gap-2`,children:[e.tags.map((n,r)=>{let i=t.filterTagIds.has(n.id);return(0,Q.jsxs)(Zt,{variant:`secondary`,className:L(`group/chip h-6 max-w-full gap-1 pr-0 pl-2 text-label font-normal focus-within:h-auto focus-within:min-h-6 focus-within:py-0.5`,i&&`ring-1 ring-foreground/30 ring-inset`),children:[(0,Q.jsxs)(`button`,{type:`button`,"aria-pressed":i,"aria-label":`Filter by ${n.name}`,className:`-ml-2 flex h-6 min-w-0 cursor-pointer items-center gap-1 rounded-4xl pr-1 pl-2 hover:bg-foreground/10 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none`,onClick:()=>t.toggleFilterTag(n),children:[i&&(0,Q.jsx)(de,{"aria-hidden":`true`,className:`size-3 shrink-0`}),(0,Q.jsx)(`span`,{className:`min-w-0 truncate group-focus-within/chip:break-all group-focus-within/chip:whitespace-normal`,title:n.name,children:n.name})]}),(0,Q.jsx)(a,{variant:`ghost`,size:`icon-xs`,className:`-ml-1 text-muted-foreground hover:text-foreground`,"data-chip-remove":!0,"aria-label":`Remove tag ${n.name} from ${e.name}`,onClick:()=>_(n,r),children:(0,Q.jsx)(an,{"aria-hidden":`true`,className:`size-3`})})]},n.id)}),d.map(t=>(0,Q.jsxs)(Zt,{variant:`secondary`,className:`group/chip h-6 max-w-full gap-1 pr-0 pl-2 text-label font-normal focus-within:h-auto focus-within:min-h-6 focus-within:py-0.5`,children:[(0,Q.jsx)(`span`,{className:`min-w-0 truncate group-focus-within/chip:break-all group-focus-within/chip:whitespace-normal`,title:t.name,children:t.name}),(0,Q.jsx)(a,{variant:`ghost`,size:`icon-xs`,className:`-ml-1 text-muted-foreground hover:text-foreground`,"data-chip-remove":!0,"aria-label":`Remove tag ${t.name} from ${e.name}`,"aria-disabled":`true`,children:(0,Q.jsx)(an,{"aria-hidden":`true`,className:`size-3`})})]},t.tempId)),p?(0,Q.jsx)(fa,{entry:e,vocabulary:r,onArtist:e.tags,pendingNames:d.map(e=>e.name),onCommit:b,onClose:e=>{m(!1),e===`escape`&&(l.current={kind:`add-button`})}}):g?(0,Q.jsxs)(`span`,{className:`text-label text-muted-foreground`,children:[`max `,10,` tags`]}):(0,Q.jsxs)(a,{ref:c,variant:`ghost`,size:`xs`,className:`h-6 text-label text-muted-foreground hover:text-foreground`,"aria-label":`Add tag to ${e.name}`,onClick:v,children:[(0,Q.jsx)(nn,{"aria-hidden":`true`,className:`size-3`}),`tag`]})]})}function ga({entry:e,onEntryChange:t,onRemove:n,tagActions:r,announce:i}){return(0,Q.jsxs)(`li`,{className:`flex flex-col gap-4 rounded-md bg-card p-4 sm:flex-row sm:items-center`,children:[(0,Q.jsx)(vt,{src:e.image_url,alt:e.name,size:64}),(0,Q.jsxs)(`div`,{className:`flex min-w-0 flex-1 flex-col gap-1`,children:[(0,Q.jsx)(`span`,{className:`truncate text-heading font-semibold text-foreground`,title:e.name,children:e.name}),e.disambiguation!==null&&(0,Q.jsx)(`span`,{className:`truncate text-label text-muted-foreground`,children:e.disambiguation}),(0,Q.jsxs)(`p`,{className:`flex flex-wrap gap-x-2 text-label text-muted-foreground`,children:[(0,Q.jsx)(`span`,{children:e.latest_release_date===null?`No releases yet`:`Latest release: ${fe(e.latest_release_date)}`}),e.next_release_date!==null&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(`span`,{"aria-hidden":`true`,children:`·`}),(0,Q.jsxs)(`span`,{children:[`Upcoming: `,fe(e.next_release_date)]})]})]}),(0,Q.jsx)(ha,{entry:e,actions:r,announce:i}),(0,Q.jsx)(xr,{entry:e,onEntryChange:t,announce:i})]}),(0,Q.jsx)(Mr,{entry:e,onEntryChange:t}),(0,Q.jsx)(a,{variant:`ghost`,size:`icon`,className:`h-11 w-11 shrink-0 text-muted-foreground hover:text-destructive`,"aria-label":`Remove ${e.name} from watchlist`,onClick:()=>n(e),children:(0,Q.jsx)(an,{"aria-hidden":`true`})})]})}function _a({options:e,selected:t,onChange:n}){let r=la(),[i,a]=(0,Z.useState)(``);return(0,Q.jsxs)(Qi,{multiple:!0,items:e,value:t,onValueChange:(e,t)=>{if(t.reason===`escape-key`){t.cancel();return}n(e.map(e=>e.id).sort((e,t)=>e-t))},onOpenChange:(e,t)=>{!e&&t.reason===`item-press`&&t.cancel()},onInputValueChange:a,isItemEqualToValue:(e,t)=>e.id===t.id,itemToStringLabel:e=>e.name,children:[(0,Q.jsxs)(oa,{ref:r,children:[t.map(e=>(0,Q.jsx)(sa,{className:`h-6 max-w-full bg-secondary text-label font-normal text-secondary-foreground`,removeLabel:`Remove ${e.name} from tag filter`,children:(0,Q.jsx)(`span`,{className:`min-w-0 truncate`,title:e.name,children:e.name})},e.id)),(0,Q.jsx)(ca,{"aria-labelledby":`watchlist-tags-label`,"aria-label":`Filter by tags, any of`,placeholder:t.length===0?`All tags`:``})]}),(0,Q.jsxs)(na,{anchor:r,children:[(0,Q.jsx)(aa,{children:e.length===0?`No tags yet — add one with “+ tag” on any artist's card.`:`No tags match “${i}”`}),(0,Q.jsx)(ra,{children:e=>(0,Q.jsxs)(ia,{value:e,children:[e.name,(0,Q.jsx)(`span`,{className:`ml-auto text-muted-foreground tabular-nums`,children:e.count})]},e.id)})]})]})}var va={$:`s`,ø:`o`,æ:`ae`,ß:`ss`};function ya(e){return e.normalize(`NFD`).replace(/\p{M}/gu,``).toLowerCase().replace(/[$øæß]/g,e=>va[e])}function ba(e,t){let n=t.trim();return n===``?!0:ya(e).includes(ya(n))}var xa=`name-asc`,Sa=[{value:`name-asc`,label:`Name (A–Z)`},{value:`name-desc`,label:`Name (Z–A)`},{value:`added-desc`,label:`Date added (newest)`},{value:`added-asc`,label:`Date added (oldest)`},{value:`latest-desc`,label:`Latest release (newest)`},{value:`latest-asc`,label:`Latest release (oldest)`}];function Ca(e){return Sa.some(t=>t.value===e)}var wa=[`album`,`single`,`ep`,`deluxe`],Ta=200;function Ea(e){if(!e)return[];let t=new Set;for(let n of e.split(`,`).slice(0,Ta)){if(!/^[1-9]\d*$/.test(n))continue;let e=Number(n);Number.isSafeInteger(e)&&t.add(e)}return[...t].sort((e,t)=>e-t)}function Da(e){let t=e.get(`sort`);return{q:e.get(`q`)??``,sort:Ca(t)?t:xa,tagIds:Ea(e.get(`tags`)),muted:e.get(`muted`)===`1`,custom:e.get(`custom`)===`1`}}function Oa(e){let t=new URLSearchParams(e),{sort:n,tagIds:r,muted:i,custom:a}=Da(e);return n===`name-asc`?t.delete(`sort`):t.set(`sort`,n),r.length===0?t.delete(`tags`):t.set(`tags`,r.join(`,`)),i?t.set(`muted`,`1`):t.delete(`muted`),a?t.set(`custom`,`1`):t.delete(`custom`),t}var ka=new Intl.Collator(void 0,{sensitivity:`base`}),Aa=(e,t)=>e.artist_id-t.artist_id;function ja(e){return(t,n)=>{let r=t.latest_release_date,i=n.latest_release_date;return r===null&&i===null?Aa(t,n):r===null?1:i===null?-1:r===i?Aa(t,n):(rka.compare(e.name,t.name)||Aa(e,t);case`name-desc`:return(e,t)=>-ka.compare(e.name,t.name)||Aa(e,t);case`added-asc`:return(e,t)=>Date.parse(e.created_at)-Date.parse(t.created_at)||Aa(e,t);case`added-desc`:return(e,t)=>Date.parse(t.created_at)-Date.parse(e.created_at)||Aa(e,t);case`latest-asc`:return ja(1);case`latest-desc`:return ja(-1)}}function Na(e,t){return t.length===0||e.tags.some(e=>t.includes(e.id))}function Pa(e){let t=new Map;for(let n of e)for(let e of n.tags){let n=t.get(e.id);n?n.count+=1:t.set(e.id,{id:e.id,name:e.name,count:1})}return q([...t.values()])}function Fa(e){return e.muted_event_types.length>0}function Ia(e){let t=new Set(e.release_types);return!(t.size===wa.length&&wa.every(e=>t.has(e)))}function La(e,t,n){return e.filter(e=>n.has(e.id)||ba(e.name,t.q)&&Na(e,t.tagIds)&&(!t.muted||Fa(e))&&(!t.custom||Ia(e))).sort(Ma(t.sort))}function Ra(e){return e.q.trim()!==``||e.tagIds.length>0||e.muted||e.custom}var za=[`q`,`tags`,`muted`,`custom`];function Ba(e,t){return`${e} of ${t} artist${t===1?``:`s`}`}function Va({on:e,label:t,onToggle:n}){return(0,Q.jsxs)(a,{variant:e?`secondary`:`outline`,"aria-pressed":e,onClick:()=>n(!e),children:[e&&(0,Q.jsx)(de,{"aria-hidden":`true`,className:`size-4`}),t]})}function Ha({query:e,onQueryChange:t,filterInputRef:n,showClear:r,onClearFilters:i,sort:o,onSortChange:s,tagOptions:c,selectedTags:l,onSelectedTagsChange:u,muted:d,onMutedChange:f,custom:p,onCustomChange:m}){return(0,Q.jsxs)(`div`,{role:`group`,"aria-label":`Filter and sort artists`,className:`flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-end`,children:[(0,Q.jsxs)(`label`,{className:`flex flex-col gap-1 text-label text-muted-foreground sm:w-64`,children:[`Filter watchlist`,(0,Q.jsxs)(Ki,{children:[(0,Q.jsx)(Ji,{children:(0,Q.jsx)($t,{"aria-hidden":`true`})}),(0,Q.jsx)(Zi,{ref:n,type:`text`,placeholder:`Filter by name…`,enterKeyHint:`search`,autoComplete:`off`,spellCheck:!1,value:e,onChange:e=>t(e.target.value),onKeyDown:n=>{n.key===`Escape`&&e!==``&&(n.preventDefault(),t(``))}}),e!==``&&(0,Q.jsx)(Ji,{align:`inline-end`,children:(0,Q.jsx)(Xi,{size:`icon-xs`,"aria-label":`Clear name filter`,onClick:()=>{t(``),n.current?.focus()},children:(0,Q.jsx)(an,{"aria-hidden":`true`})})})]})]}),(0,Q.jsxs)(`div`,{className:`flex flex-col gap-1 sm:w-56`,children:[(0,Q.jsx)(`span`,{id:`watchlist-sort-label`,className:`text-label text-muted-foreground`,children:`Sort by`}),(0,Q.jsxs)(ot,{value:o,onValueChange:e=>s(e),children:[(0,Q.jsx)(He,{"aria-labelledby":`watchlist-sort-label`,className:`w-full`,children:(0,Q.jsx)(De,{children:e=>Sa.find(t=>t.value===e)?.label})}),(0,Q.jsx)(Ye,{children:Sa.map(e=>(0,Q.jsx)(nt,{value:e.value,children:e.label},e.value))})]})]}),(0,Q.jsxs)(`div`,{className:`flex flex-col gap-1 sm:max-w-md sm:min-w-60 sm:flex-1`,children:[(0,Q.jsx)(`span`,{id:`watchlist-tags-label`,className:`text-label text-muted-foreground`,children:`Tags: any of`}),(0,Q.jsx)(_a,{options:c,selected:l,onChange:u})]}),(0,Q.jsxs)(`fieldset`,{className:`flex flex-col gap-1`,children:[(0,Q.jsx)(`legend`,{className:`text-label text-muted-foreground`,children:`Show only`}),(0,Q.jsxs)(`div`,{className:`flex flex-wrap gap-2`,children:[(0,Q.jsx)(Va,{on:d,label:`Has muted events`,onToggle:f}),(0,Q.jsx)(Va,{on:p,label:`Custom release types`,onToggle:m})]})]}),r&&(0,Q.jsxs)(a,{variant:`ghost`,className:`h-9 text-muted-foreground hover:text-foreground`,onClick:i,children:[(0,Q.jsx)(Qt,{"aria-hidden":`true`}),`Clear filters`]})]})}var Ua=300,Wa=500,Ga=n(function(){let[e,t]=(0,Z.useState)(null),[n,r]=(0,Z.useState)(!1),[i,o]=(0,Z.useState)(null),[s,c]=(0,Z.useState)(``),[l,u]=(0,Z.useState)(!1),{params:d,write:f}=bt(),p=Da(d),[m,h]=(0,Z.useState)(p.q),g=(0,Z.useRef)(p.q),_=(0,Z.useRef)(null),v=(0,Z.useRef)(null),y=(0,Z.useRef)(null),b=(0,Z.useRef)({n:0,m:0}),[x,C]=(0,Z.useState)(()=>new Set),w=(0,Z.useRef)(new Map),T=(0,Z.useRef)(new Map),E=(0,Z.useRef)(!1);function k(e){C(t=>t.has(e)?t:new Set(t).add(e))}function A(){C(e=>e.size===0?e:new Set)}function j(e){let t=new Set;for(let n of e){let e=T.current,r=e.has(n)?e.get(n):n;r!=null&&w.current.has(r)&&t.add(r)}return[...t].sort((e,t)=>e-t)}function M(e){f(t=>{e.length===0?t.delete(`tags`):t.set(`tags`,e.join(`,`))},{replace:!0})}function N(){y.current&&clearTimeout(y.current),y.current=setTimeout(()=>{let{n:e,m:t}=b.current;oe(e>0?`Showing ${Ba(e,t)}.`:`No artists match these filters.`)},Wa)}function ee(e){h(e),A(),N(),_.current&&clearTimeout(_.current),_.current=setTimeout(()=>{g.current=e,f(t=>{e===``?t.delete(`q`):t.set(`q`,e)},{replace:!0})},Ua)}(0,Z.useEffect)(()=>{p.q!==g.current&&(_.current&&clearTimeout(_.current),g.current=p.q,h(p.q),A())},[p.q]);let P=`${p.sort}|${p.tagIds.join(`,`)}|${p.muted}|${p.custom}`,I=(0,Z.useRef)(P);(0,Z.useEffect)(()=>{I.current!==P&&(I.current=P,A())},[P]),(0,Z.useEffect)(()=>{let e=Oa(d);if(E.current){let t=j(Da(e).tagIds);t.length===0?e.delete(`tags`):e.set(`tags`,t.join(`,`))}e.toString()!==d.toString()&&f(t=>{for(let n of[`sort`,`tags`,`muted`,`custom`]){let r=e.get(n);r===null?t.delete(n):t.set(n,r)}},{replace:!0})},[d,f]),(0,Z.useEffect)(()=>()=>{_.current&&clearTimeout(_.current),y.current&&clearTimeout(y.current)},[]);let L=(0,Z.useCallback)(()=>(r(!1),S().then(e=>(t(e),e)).catch(()=>(r(!0),null))),[]);(0,Z.useEffect)(()=>{L()},[L]),(0,Z.useEffect)(()=>{if(!e||E.current)return;E.current=!0;let t=new Set(e.flatMap(e=>e.tags.map(e=>e.id))),n=p.tagIds.filter(e=>t.has(e));n.length!==p.tagIds.length&&M(n)},[e]);function te(e,n){k(e),t(t=>t&&t.map(t=>t.id===e?{...t,...n}:t))}function ne(e,n,r){k(e),t(t=>t&&t.map(t=>{if(t.id!==e||t.tags.some(e=>e.id===n.id))return t;let i=[...t.tags],a=r===void 0?i.length:Math.max(0,Math.min(r,i.length));return i.splice(a,0,n),{...t,tags:i}}))}function re(e,n){k(e),t(t=>t&&t.map(t=>t.id===e?{...t,tags:t.tags.filter(e=>e.id!==n)}:t))}function ie(e,t){f(n=>{let r=Da(n).tagIds;if(!r.includes(e))return;let i=new Set(r.filter(t=>t!==e));t!==null&&i.add(t),i.size===0?n.delete(`tags`):n.set(`tags`,[...i].sort((e,t)=>e-t).join(`,`))},{replace:!0})}function ae(e){e.kind===`renamed`?w.current.set(e.tag.id,e.tag.name):e.kind===`deleted`?(T.current.set(e.tagId,null),ie(e.tagId,null)):(T.current.set(e.sourceId,e.target.id),w.current.set(e.target.id,e.target.name),ie(e.sourceId,e.target.id)),t(t=>t&&t.map(t=>{let n=ar(t.tags,e);return n===t.tags?t:{...t,tags:n}}))}function oe(e){c(e)}async function R(e,t){if(!H(e)){J.error(`Can't add this artist yet -- search MusicBrainz instead.`);return}try{k((await O({mbid:t.id,name:t.name,imageUrl:t.image_url??void 0,disambiguation:t.disambiguation??void 0,deezerId:void 0})).id),L()}catch(e){if(e instanceof F&&e.status===409){let e=(await L())?.find(e=>e.mbid===t.id);e&&k(e.id);return}J.error(`Couldn't add this artist. Try again in a moment.`)}}async function se(e){t(t=>t&&t.filter(t=>t.id!==e.id));try{await D(e.id)}catch{J.error(`Couldn't remove ${e.name} — it may already be gone.`),L();return}J.success(`Removed ${e.name} from your watchlist.`,{description:`Undo re-adds this artist with default preferences -- its previous release-type and mute settings are not restored.`,duration:5e3,action:{label:`Undo`,onClick:()=>{O({mbid:e.mbid,name:e.name,deezerId:e.deezer_id??void 0,disambiguation:e.disambiguation??void 0,imageUrl:e.image_url??void 0,note:e.note??void 0}).then(e=>(k(e.id),L())).catch(()=>{J.error(`Couldn't restore ${e.name}. Try adding it again.`)})}}})}if(e)for(let t of e)for(let e of t.tags)w.current.set(e.id,e.name);let z=e?Pa(e):[],ce=q(p.tagIds.flatMap(e=>{let t=w.current.get(e);return t===void 0?[]:[{id:e,name:t,count:z.find(t=>t.id===e)?.count??0}]})),B=new Set(p.tagIds),V=e?La(e,{...p,q:m},x):[];b.current={n:V.length,m:e?.length??0};function le(e){f(t=>{e.length===0?t.delete(`tags`):t.set(`tags`,e.join(`,`))}),A(),N()}function ue(e,t){f(n=>{t?n.set(e,`1`):n.delete(e)}),A(),N()}function U(e){w.current.set(e.id,e.name);let t=new Set(p.tagIds);t.delete(e.id)||t.add(e.id),le([...t].sort((e,t)=>e-t))}function de(e){f(t=>{e===`name-asc`?t.delete(`sort`):t.set(`sort`,e)}),A(),y.current&&clearTimeout(y.current);let t=Sa.find(t=>t.value===e).label;oe(`Sorted by ${t}.`)}function fe(){_.current&&clearTimeout(_.current),y.current&&clearTimeout(y.current),h(``),g.current=``,f(e=>{for(let t of za)e.delete(t)}),A(),v.current?.focus();let t=e?.length??0;oe(`Filters cleared. Showing ${Ba(t,t)}.`)}return(0,Q.jsx)(sr,{onChange:ae,children:(0,Q.jsxs)(`div`,{className:`flex flex-col gap-6 p-8`,children:[(0,Q.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-4`,children:[(0,Q.jsx)(`h1`,{className:`text-display font-semibold text-foreground`,children:`Watchlist`}),(0,Q.jsxs)(a,{variant:`secondary`,onClick:()=>u(!0),children:[(0,Q.jsx)(rn,{"aria-hidden":`true`}),`Manage tags`]})]}),(0,Q.jsx)(`div`,{role:`status`,"aria-atomic":`true`,className:`sr-only`,children:s}),(0,Q.jsx)(dr,{open:l,onOpenChange:u}),(0,Q.jsxs)(`div`,{className:`flex flex-col gap-6`,children:[(0,Q.jsx)(pr,{onResults:o}),i&&(0,Q.jsx)(gr,{response:i,watchlistEntries:e,onAdd:R})]}),n&&(0,Q.jsx)(Xt,{heading:`Couldn't load your watchlist.`,body:`Please try again.`,action:(0,Q.jsx)(a,{onClick:L,children:`Retry`})}),!n&&e===null&&(0,Q.jsx)(`div`,{className:`flex flex-col gap-4`,children:[0,1,2].map(e=>(0,Q.jsx)(Yt,{className:`h-24 w-full rounded-md`},e))}),!n&&e!==null&&e.length===0&&(0,Q.jsx)(Xt,{heading:`No artists yet`,body:`Search above to add one.`}),!n&&e!==null&&e.length>0&&(0,Q.jsxs)(`section`,{"aria-labelledby":`watchlist-artists-heading`,className:`flex flex-col gap-4`,children:[(0,Q.jsxs)(`div`,{className:`flex flex-wrap items-baseline justify-between gap-2`,children:[(0,Q.jsx)(`h2`,{id:`watchlist-artists-heading`,className:`text-heading font-semibold text-foreground outline-none`,children:`Your artists`}),(0,Q.jsx)(`p`,{className:`text-label text-muted-foreground tabular-nums`,children:Ba(V.length,e.length)})]}),(0,Q.jsx)(Ha,{query:m,onQueryChange:ee,filterInputRef:v,showClear:Ra({...p,q:m})&&V.length>0,onClearFilters:fe,sort:p.sort,onSortChange:de,tagOptions:z,selectedTags:ce,onSelectedTagsChange:le,muted:p.muted,onMutedChange:e=>ue(`muted`,e),custom:p.custom,onCustomChange:e=>ue(`custom`,e)}),V.length===0?(0,Q.jsx)(Xt,{heading:`No artists match these filters`,body:`Try a different name or fewer filters, or clear them to see all ${e.length} artist${e.length===1?``:`s`}.`,action:(0,Q.jsx)(a,{onClick:fe,children:`Clear filters`})}):(0,Q.jsx)(`ul`,{className:`flex flex-col gap-4`,children:V.map(e=>(0,Q.jsx)(ga,{entry:e,onEntryChange:te,onRemove:se,tagActions:{addTag:ne,removeTag:re,filterTagIds:B,toggleFilterTag:U},announce:oe},e.id))})]})]})})});export{Ga as default}; \ No newline at end of file diff --git a/internal/webassets/build/client/index.html b/internal/webassets/build/client/index.html index 13c614d..77e6168 100644 --- a/internal/webassets/build/client/index.html +++ b/internal/webassets/build/client/index.html @@ -1,11 +1,11 @@ -
\ No newline at end of file +import("/assets/entry.client-sq613zEF.js"); \ No newline at end of file From 48d663be1beaafff4837eb10c4128bd8629f4c7f Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 15:12:42 -0500 Subject: [PATCH 113/142] docs(25-18): record the WR-01..WR-04 decisions in the UI-SPEC and CONTEXT G4 line - UI-SPEC: Deezer wrapper (WR-04), fallback anchor (WR-02), keyed sticky set (WR-01, WR-03) - CONTEXT: amend the locked G4 line for the WR-03 override --- .../25-find-filter-watchlist-and-history/25-CONTEXT.md | 2 +- .../25-find-filter-watchlist-and-history/25-UI-SPEC.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md b/.planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md index 2b0ecb1..41d2c5c 100644 --- a/.planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md @@ -50,7 +50,7 @@ These come from a grill session after discuss and the UI phase. Where they confl - **G1 (supersedes D-14's retention clause and the roadmap's PITFALLS #8 note):** `latest_release_date` **ignores retention**. It is the newest date among all of the artist's stored `new_release` events, at any age. ListWatchlist and GetWatchlistEntry take **no cutoff**, and `watchlist.Service` does not get the retention setting. With the 90-day default, a retention-scoped value would leave most artists at "No releases yet" and collapse the sort. The accepted cost is that the Watchlist can show a date History no longer lists, and the routes stay unlinked (D-13). - **G2 (supersedes D-15's "future dates as-is"):** a date is **upcoming** when it is strictly after UTC `CURRENT_DATE` at its own precision. `2027` and `2026-11` are upcoming on 2026-10-04, but `2026` and `2026-10` are not. Postgres decides this inside the same LATERAL join, so the client never compares dates. - **G3 (extends D-03/D-14):** the payload carries two fields. `latest_release_date` is the newest non-upcoming date, and it alone drives the latest-release sort. `next_release_date` is the **nearest** upcoming date, shown as a muted `Upcoming: {date}` line, and it never affects the sort. Both come from the single `GET /watchlist` query and the shared POST/PATCH projection (Phase 24 D-26). -- **G4 (supersedes UI-SPEC's card-removal focus flow and announcement):** **sticky cards.** An in-card edit never removes its own card from a filtered view, and the card gets no cue. Stickiness clears on a change to search text, sort, tag selection, a toggle, Clear filters, or a chip-click filter, and on reload. "N of M" counts **visible** cards. +- **G4 (supersedes UI-SPEC's card-removal focus flow and announcement):** **sticky cards.** An in-card edit never removes its own card from a filtered view, and the card gets no cue. Stickiness clears on a change to search text, sort, tag selection, a toggle, Clear filters, or a chip-click filter, and on reload. "N of M" counts **visible** cards. Amended 2026-10-11 (WR-03, user decision 2026-10-11): "tag selection" means the user's own tag-filter change. The route's own tag rewrites (a Manage tags merge or delete of a selected tag, the first-load tag-id check, and URL canonicalization) keep sticky cards and carry them to the filter they land on. Back/Forward still clears them. - **G5 (extends D-04):** the name filter adds a fold table after removing accents: `$→s`, `ø→o`, `æ→ae`, `ß→ss`, applied to both the query and the name. `asap` matches A$AP Rocky. - **G6 (D-12 sequencing):** History's move to URL state stays in scope as **its own plan**, separate from the HIST-02 tag filter, so the tag filter can land and be verified on its own. - **G7 (confirms D-11):** both comboboxes stay, with the UI-SPEC's hand-rolled `Combobox` hardening. A backlog todo covers unifying them on base-ui. diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md b/.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md index a2307eb..7039a83 100644 --- a/.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md @@ -185,7 +185,7 @@ This keeps Phase 24 [R1] intact: a tag chip never takes a hue, a tinted fill, or
(unchanged) header:

Watchlist

……… [Manage tags] (unchanged) route-level
(unchanged, Phase 24 [R6]) - SearchBox "Search artists" + SearchResultsColumns (unchanged, D-01; one G-25-9 exception: the Deezer row's disabled button is capped at 40% of the row below lg and truncates its label) + SearchBox "Search artists" + SearchResultsColumns (unchanged, D-01; one G-25-9 exception: the Deezer row's disabled button truncates its label inside a wrapper capped at 40% of the row below lg, and the wrapper carries the explanatory title so it shows on hover (WR-04, user decision 2026-10-11)) error | skeletons | "No artists yet" (unchanged)
NEW, only when M > 0 heading row @@ -396,8 +396,8 @@ announcement per change, and all six modes are visible at once. ### Router & sticky-state rules -- **Scroll reset and anchoring (D-12, G-25-4):** `web/app/root.tsx:37` mounts ``, and react-router 7.18.2 calls `window.scrollTo(0, 0)` on every navigation, push or replace, unless it passes `preventScrollReset: true`. Every filter, sort, and typing change is a navigation, so Watchlist and History write params only through one shared helper that always passes `{ preventScrollReset: true }`, plus `replace: true` where this spec says replace. That stops the router from resetting scroll, but a filter that shrinks the list can leave the page shorter than the old `scrollY`, and the browser then clamps it. So a chip-label click also anchors the clicked chip: `useScrollAnchor` records its `getBoundingClientRect().top` before the tags param is written, and a layout effect scrolls by the drift after the re-render. The clicked card keeps its on-screen position, and on a short page the browser clamps it. -- **Sticky set (G4):** keep an explicit `stickyIds` set. A card is visible when it matches the filters or `stickyIds.has(id)`, and an in-card edit adds its card's id. Only the G4 user actions clear the set (search text, sort, tag selection, a toggle, Clear filters, a chip-click filter, reload). An `entries` change never clears or recomputes it, because `watchlist.tsx` calls `refresh()` (a full `GET /watchlist`) after add-from-search, after an Undo restore, and after a failed remove. +- **Scroll reset and anchoring (D-12, G-25-4):** `web/app/root.tsx:37` mounts ``, and react-router 7.18.2 calls `window.scrollTo(0, 0)` on every navigation, push or replace, unless it passes `preventScrollReset: true`. Every filter, sort, and typing change is a navigation, so Watchlist and History write params only through one shared helper that always passes `{ preventScrollReset: true }`, plus `replace: true` where this spec says replace. That stops the router from resetting scroll, but a filter that shrinks the list can leave the page shorter than the old `scrollY`, and the browser then clamps it. So a chip-label click also anchors the clicked chip: `useScrollAnchor` records its `getBoundingClientRect().top` before the tags param is written, and a layout effect scrolls by the drift after the re-render. The clicked card keeps its on-screen position, and on a short page the browser clamps it. If the clicked card leaves the filtered list, the anchor falls back to the clicked card's next sibling card, then its previous one, whichever is still in the list; with neither left, no correction runs (WR-02, user decision 2026-10-11). +- **Sticky set (G4):** keep an explicit `stickyIds` set. A card is visible when it matches the filters or `stickyIds.has(id)`, and an in-card edit adds its card's id. Only the G4 user actions clear the set (search text, sort, tag selection, a toggle, Clear filters, a chip-click filter, reload). An `entries` change never clears or recomputes it, because `watchlist.tsx` calls `refresh()` (a full `GET /watchlist`) after add-from-search, after an Undo restore, and after a failed remove. The set is keyed to the filter it was made under, so a filter change from a G4 action or from Back/Forward drops it in the same render as the URL change. The route's own tag rewrites (a Manage tags merge or delete of a selected tag, the first-load tag-id check, and URL canonicalization) carry it to the filter they land on (WR-01, WR-03, user decision 2026-10-11). - **Adds are sticky (G4):** an artist added from search (`handleAddSearchResult`) or restored by Undo joins `stickyIds` (id from the POST response), so an add under an active filter never succeeds invisibly. - **Tag-id validation runs once (D-13):** URL tag ids are checked against the payload-derived set only on the first successful payload load. After that the tag filter changes only by user action or the Manage tags `onDeleted`/`onMerged` callbacks; `refresh()` never mutates it. From c55905ad6d43ba5101498fce049a96eaa7e09699 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 15:16:16 -0500 Subject: [PATCH 114/142] docs(25-18): complete ship-the-review-fixes plan --- .planning/REQUIREMENTS.md | 12 +- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 16 ++- .../25-18-SUMMARY.md | 133 ++++++++++++++++++ 4 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 .planning/phases/25-find-filter-watchlist-and-history/25-18-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 17f37d0..1900939 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -22,12 +22,12 @@ ### Watchlist View -- [ ] **WLVW-01**: User can search the watchlist by artist name +- [x] **WLVW-01**: User can search the watchlist by artist name - [ ] **WLVW-02**: User can sort by name (A–Z/Z–A) or date added (newest/oldest), with a stable tie-break - [ ] **WLVW-03**: User can sort by latest release (the artist's newest own `new_release` event date that is not upcoming); artists with none sort last in both directions -- [ ] **WLVW-04**: User can filter the watchlist by one or more tags +- [x] **WLVW-04**: User can filter the watchlist by one or more tags - [ ] **WLVW-05**: User can filter to artists with muted event types or non-default release-type filters -- [ ] **WLVW-06**: Search, sort, and filters combine; the view shows "N of M artists" and an empty state with a clear-filters action +- [x] **WLVW-06**: Search, sort, and filters combine; the view shows "N of M artists" and an empty state with a clear-filters action ### History @@ -89,12 +89,12 @@ Which phases cover which requirements. Updated during roadmap creation. | TAG-06 | Phase 24 | Complete | | TAG-07 | Phase 24 | Complete | | NOTE-01 | Phase 24 | Complete | -| WLVW-01 | Phase 25 | Gaps Found | +| WLVW-01 | Phase 25 | Complete | | WLVW-02 | Phase 25 | Gaps Found | | WLVW-03 | Phase 25 | Gaps Found | -| WLVW-04 | Phase 25 | Gaps Found | +| WLVW-04 | Phase 25 | Complete | | WLVW-05 | Phase 25 | Gaps Found | -| WLVW-06 | Phase 25 | Gaps Found | +| WLVW-06 | Phase 25 | Complete | | HIST-02 | Phase 25 | Gaps Found | | BULK-01 | Phase 26 | Pending | | BULK-02 | Phase 26 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index a50f89b..16d998d 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -193,7 +193,7 @@ Plans: 4. The user can filter History by a tag alongside the existing filters. It lists events for artists carrying that tag, including artists since removed from the watchlist, and it never shows an event older than the retention window, either on the first page or via "load older". 5. Opening the Watchlist makes one request regardless of watchlist size. Searching, sorting, and filtering then happen instantly with no further requests. -**Plans:** 17/18 plans executed +**Plans:** 18/18 plans executed Plans: **Wave 1** @@ -245,7 +245,7 @@ Plans: **Verification gap closure — Wave 3** *(blocked on 25-15, 25-16, 25-17)* -- [ ] 25-18-PLAN.md — Embedded bundle rebuild with marker counts, the WR-01..WR-04 decisions recorded in three UI-SPEC lines, the Definition of Done, and a blast-radius gate against `8ccde67` +- [x] 25-18-PLAN.md — Embedded bundle rebuild with marker counts, the WR-01..WR-04 decisions recorded in three UI-SPEC lines, the Definition of Done, and a blast-radius gate against `8ccde67` **UI hint**: yes @@ -350,7 +350,7 @@ Phases execute in numeric order: 24 → 25 → 26 → 27 → 28 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 24. Artist Tags & Notes | 14/14 | Complete | 2026-10-06 | -| 25. Find & Filter — Watchlist and History | 17/18 | In Progress| | +| 25. Find & Filter — Watchlist and History | 18/18 | In Progress| | | 26. Bulk Edit & Remove | 0/? | Not started | - | | 27. Paste-a-List Bulk Add | 0/? | Not started | - | | 28. Tags on Discord Notifications | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 11e7bb1..7151fc0 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,16 +5,16 @@ milestone_name: Watchlist Organization current_phase: 25 current_phase_name: Find & Filter — Watchlist and History status: executing -stopped_at: Completed 25-17-PLAN.md -last_updated: "2026-10-12T20:11:09.570Z" +stopped_at: Completed 25-18-PLAN.md +last_updated: "2026-10-12T20:16:10.854Z" last_activity: 2026-10-12 last_activity_desc: Phase 25 execution started -state_head: fe62df65d7e80e110e90330f4dc050a986d8c927 +state_head: 48d663be1beaafff4837eb10c4128bd8629f4c7f progress: total_phases: 5 completed_phases: 1 total_plans: 32 - completed_plans: 31 + completed_plans: 32 percent: 20 --- @@ -30,7 +30,7 @@ See: .planning/PROJECT.md (updated 2026-10-06) ## Current Position Phase: 25 (Find & Filter — Watchlist and History) — EXECUTING -Plan: 4 of 18 +Plan: 5 of 18 Status: Ready to execute Last activity: 2026-10-12 — Phase 25 execution started @@ -179,6 +179,7 @@ Last activity: 2026-10-12 — Phase 25 execution started | Phase 25 P15 | 8 min | 2 tasks | 4 files | | Phase 25 P16 | 4 min | 1 tasks | 2 files | | Phase 25 P17 | 5 min | 2 tasks | 3 files | +| Phase 25 P18 | 5 min | 3 tasks | 3 files | ## Accumulated Context @@ -420,6 +421,7 @@ Recent decisions affecting current work: - [Phase 25]: 25-15: sticky set keyed to filterKey and reset during render (not a derived key compare, which revives cards when a filter is turned off); route-own tag rewrites carry via a single-landing, replace-only carry - [Phase 25]: 25-16: Deezer hint title and 40% cap live on a wrapper span (a disabled button gets no pointer events); Button is w-full inside it (WR-04) - [Phase 25]: 25-17: useScrollAnchor falls back to the clicked card's next, then previous, sibling when the chip leaves the DOM (WR-02, user decision 2026-10-11); list heading excluded +- [Phase 25]: 25-18: WR-01..WR-04 decisions recorded in UI-SPEC; CONTEXT G4 amended for WR-03 (user decision 2026-10-11) ### Pending Todos @@ -515,8 +517,8 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-12T20:11:09.475Z -Stopped at: Completed 25-17-PLAN.md +Last session: 2026-10-12T20:16:10.768Z +Stopped at: Completed 25-18-PLAN.md Resume file: None ## Operator Next Steps diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-18-SUMMARY.md b/.planning/phases/25-find-filter-watchlist-and-history/25-18-SUMMARY.md new file mode 100644 index 0000000..e1c6bca --- /dev/null +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-18-SUMMARY.md @@ -0,0 +1,133 @@ +--- +phase: 25-find-filter-watchlist-and-history +plan: 18 +subsystem: ui +tags: [embedded-spa, gap-closure, ui-spec, definition-of-done] +status: complete +requires: + - phase: 25-find-filter-watchlist-and-history + provides: "WR-01/WR-03 sticky carry (25-15), WR-04 Deezer wrapper (25-16), WR-02 fallback anchor (25-17)" +provides: + - "internal/webassets/build/client rebuilt from the gap-closure web/, so the Go binary serves all four review fixes" + - "25-UI-SPEC and the CONTEXT G4 line record the 2026-10-11 gap-planning decisions" + - "Green Definition of Done and an empty blast-radius diff against 8ccde67" +affects: [gsd-verify-work re-test of G-25-4 and WR-02/03/04] +actuals: + tokens: 40000 + tasks: 3 + commits: 2 +plan_head_before: 20a874064fc0353d6add8601d045d9120ca28c2b +tech-stack: + added: [] + patterns: + - "Bundle freshness proven by marker counts against the planning-head embedded tree" +key-files: + created: [] + modified: + - internal/webassets/build/client + - .planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md + - .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md +key-decisions: + - "User decision 2026-10-11 (gap planning): WR-01..WR-04 recorded on the UI-SPEC lines they govern; CONTEXT G4 amended in place for WR-03" +requirements-completed: [WLVW-01, WLVW-04, WLVW-06] +coverage: + - id: D1 + description: "Embedded SPA carries the sticky carry, fallback anchor, and Deezer wrapper fixes" + requirement: WLVW-04 + verification: + - kind: command + ref: "marker counts under internal/webassets/build/client/assets vs 8ccde67, then go build ./..." + status: pass + human_judgment: false + - id: D2 + description: "UI-SPEC (three lines) and CONTEXT G4 (one line) record the WR decisions" + requirement: WLVW-04 + verification: + - kind: command + ref: "git diff -U0 8ccde67 line counts: UI-SPEC 3/3, CONTEXT 1/1" + status: pass + human_judgment: false + - id: D3 + description: "Full Definition of Done and blast radius" + requirement: WLVW-01 + verification: + - kind: command + ref: "go vet, golangci-lint, go test with coverage (90.29), sqlc-check, typecheck, prettier, web suite (604 tests)" + status: pass + human_judgment: false + - id: D4 + description: "Real-browser focus, scroll anchoring, Back/Forward, and Deezer pill hover" + requirement: WLVW-04 + human_judgment: true + rationale: "Layout, focus, and tooltips need a real browser; human_verify_mode is end-of-phase and the items go to /gsd-verify-work" +duration: 5 min +completed: 2026-10-12 +--- + +# Phase 25 Plan 18: Ship the Review Fixes Summary + +**The embedded SPA is rebuilt so a Go-only build serves the WR-01..WR-04 fixes, the UI-SPEC and locked G4 line record the user's 2026-10-11 decisions, and every Definition of Done gate is green.** + +## Accomplishments + +- Rebuilt `internal/webassets/build/client` with the frozen-lockfile recipe (install, build, replace). `package.json` and `pnpm-lock.yaml` did not drift, and `go build ./...` compiles the embed. +- Marker counts under `internal/webassets/build/client/assets` (before at `8ccde67` -> after): + - `carryTo`: 0 -> 5 + - `previousElementSibling`: 0 -> 1 + - `w-full`: 39 -> 40 +- 25-UI-SPEC: exactly three lines changed (page-order Deezer note WR-04, D-12 scroll bullet WR-02, sticky-set bullet WR-01/WR-03). +- 25-CONTEXT: the single G4 bullet keeps its text and gains the `Amended 2026-10-11 (WR-03, user decision 2026-10-11)` sentence; the diff against `8ccde67` is one line removed, one added. +- 25-UAT.md and 25-VERIFICATION.md untouched. + +## Task Commits + +1. Task 1 (tracer): `24d8aaa` build(25-18): rebuild the embedded SPA with the review fixes +2. Task 2: `48d663b` docs(25-18): record the WR-01..WR-04 decisions in the UI-SPEC and CONTEXT G4 line +3. Task 3: no commit (all gates passed first time, nothing to fix) + +## Definition of Done + +| Gate | Result | +| ---- | ------ | +| `go vet ./...` | clean | +| `golangci-lint run` | 0 issues | +| Integration suite with coverage profile (no `-race`) | no `FAIL` lines; only the known `go: no such tool "covdata"` from `internal/testutil` | +| Backend coverage gate (`coverage-report --mode=total`) | 90.29 (floor 80) | +| `sqlc-check` (version guard first) | `v1.31.1` matches `SQLC_VERSION`; `sqlc generate` leaves `internal/db/sqlc/` clean | +| `web typecheck` | clean | +| `prettier --check "**/*.{ts,tsx}"` | clean | +| `web test` | 31 files, 604 tests passed; coverage statements 95.28, branches 90.62, functions 96.1, lines 97.12 (floor 70) | +| Blast radius vs `8ccde67` | empty (no `*.go`, queries, db, events, go.mod/sum, package files, ui/, useUrlParams, root.tsx, TagChips.tsx, UAT, VERIFICATION) | + +`make` and bare `pnpm` are not on this box's PATH, so each Makefile recipe was run directly (`golangci-lint` and `sqlc` from `$HOME/go/bin`). The dev Postgres was down at task start, so I ran the `db-up` recipe (`docker compose up -d --wait postgres`) before the suite. + +## Accepted exceptions + +Accepted exception: CI-only race (carried from user decision 2026-10-11, 25-14) + +Reason: cgo is disabled locally so `go test -race` cannot run here. No Go source changed in 25-15 to 25-18 (the blast-radius check pins every `*.go` file), and CI's Linux test job runs `-race`. + +## Deviations from Plan + +None - plan executed exactly as written. + +## Human Checks (queued for /gsd-verify-work) + +Real browser, long Watchlist scrolled down: (1) remove a tag from a card and click its second chip label, (2) un-press a chip on a single-tag card with two tags selected, (3) Manage tags merge/delete of a selected tag after a card went sticky, (4) Back after a sticky card under a toggled filter; at 375px hover the truncated Deezer pill. Expected results are in the plan's Task 3 human-check. + +## Known Stubs + +None. + +## Threat Flags + +None. T-25-SC (frozen lockfile, package files unchanged), T-25-48 (marker counts above baseline), T-25-49 (empty blast radius), and T-25-50 (exact doc diffs) are mitigated as planned. + +## Next + +Phase 25 gap-closure plans are complete; ready for /gsd-verify-work re-verification. + +## Self-Check: PASSED + +- FOUND: internal/webassets/build/client, 25-UI-SPEC.md, 25-CONTEXT.md +- FOUND commits: 24d8aaa, 48d663b From fc4b2d4cefd631d589cc2c4406f37f04e34bfdde Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 15:26:35 -0500 Subject: [PATCH 115/142] docs(25): add code review report --- .../25-REVIEW.md | 217 +++++++----------- 1 file changed, 84 insertions(+), 133 deletions(-) diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-REVIEW.md b/.planning/phases/25-find-filter-watchlist-and-history/25-REVIEW.md index 5d17742..bb0ff43 100644 --- a/.planning/phases/25-find-filter-watchlist-and-history/25-REVIEW.md +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-REVIEW.md @@ -1,188 +1,139 @@ --- phase: 25-find-filter-watchlist-and-history -reviewed: 2026-10-11T20:41:29Z +reviewed: 2026-10-12T20:25:38Z depth: standard -files_reviewed: 13 +files_reviewed: 8 files_reviewed_list: - - internal/httpserver/events_test.go - - web/app/components/history/EventCard.tsx - - web/app/components/history/EventCard.test.tsx - - web/app/components/watchlist/SearchResultsColumns.tsx - web/app/components/watchlist/SearchResultsColumns.test.tsx - - web/app/components/watchlist/TagChips.tsx - - web/app/components/watchlist/TagChips.test.tsx - - web/app/lib/format.ts - - web/app/lib/format.test.ts - - web/app/lib/useScrollAnchor.ts + - web/app/components/watchlist/SearchResultsColumns.tsx - web/app/lib/useScrollAnchor.test.tsx - - web/app/routes/watchlist.tsx + - web/app/lib/useScrollAnchor.ts + - web/app/lib/watchlistView.test.ts + - web/app/lib/watchlistView.ts - web/app/routes/watchlist.test.tsx + - web/app/routes/watchlist.tsx findings: critical: 0 - warning: 4 - info: 5 - total: 9 + warning: 3 + info: 3 + total: 6 status: issues_found --- -# Phase 25: Code Review Report (gap closure 25-11 to 25-14) +# Phase 25: Code Review Report (gap closure 25-15 to 25-17) -**Reviewed:** 2026-10-11T20:41:29Z +**Reviewed:** 2026-10-12T20:25:38Z **Depth:** standard -**Files Reviewed:** 13 +**Files Reviewed:** 8 **Status:** issues_found -_Prior review (commit 2cd98c2, 40 files) was `issues_found` with 0 critical, 4 warning, and 7 info. This report replaces it and covers only `2cd98c2..HEAD`._ +_The prior report (2026-10-11, 0 critical, 4 warning, 5 info) is replaced. This report covers `efcb6ec..HEAD` for the eight files above._ ## Summary -This is an incremental review of the three gap-closure plans: -- **25-11:** `useScrollAnchor` and chip-click anchoring, plus clearing sticky cards on Back/Forward. -- **25-12:** the History card's `Detected` date (`formatDetectedDate`) and a retention integration test. -- **25-13:** the Deezer "Search MusicBrainz to add" width cap and label truncation. - -The backend test (`TestRetention_TagFilterKeepsOldReleasesDetectedInWindow`) is sound: it is order-checked, it terminates, cleanup cascades via `ON DELETE CASCADE`, and the time margins are wide. `formatDetectedDate` and the EventCard line are correct; tests pin TZ to UTC. +This re-review covers gap-closure plans 25-15 (sticky set keyed to the filter, carried across the route's own rewrites), 25-16 (Deezer hint title moved to a wrapper), and 25-17 (fallback scroll-anchor candidates). The four scoped test files pass locally (`vitest run`, 158 tests). -All the substantive defects are in the 25-11 scroll-anchor and sticky-card interaction: -- The hook assumes the URL change and the sticky clear land in one commit. React Router 7.18.2 commits navigations inside `startTransition` (`node_modules/react-router/dist/development/chunk-62JRHF6Z.mjs:6752`). That puts the synchronous `clearSticky()` in an earlier commit. A sticky card the user clicks on can unmount and remount, which loses both the scroll anchor and focus. -- The anchor is skipped by design when the clicked card leaves the list. That is a reachable path where the G-25-4 collapse comes back. -- The new `filterKey` effect clears sticky cards on the route's own programmatic URL writes, not only on Back/Forward. +Status of the prior warnings: -The 25-13 truncation leans on a `title` that the Button's `disabled:pointer-events-none` makes unreachable. +| Prior | Verdict | Notes | +|-------|---------|-------| +| WR-01 (sticky card unmounts before the filter commits) | **Resolved** | `reconcileSticky` runs during render (`watchlist.tsx:95-101`), so the reset lands in the same transition render as the location. The sync `clearSticky()` calls were removed from `handleTagsChange`, `handleFlagChange` and `handleSortChange`. A sticky card's chip click can only add a tag the card carries, so the card matches and stays mounted. The route test at `watchlist.test.tsx:2392` pins `isConnected`, node identity, focus and the `scrollBy` correction, and its RED evidence shows it failed against the old code. | +| WR-02 (anchor dropped when the clicked card leaves) | **Partially resolved** | Only the two immediate neighbours are tried. If they also leave, the collapse comes back. See WR-01 below. Focus loss on the same path is still unhandled. See WR-03 below. | +| WR-03 (`filterKey` effect clears on the route's own writes, after paint) | **Resolved** | The post-paint effect is gone. `writeKeepingSticky` and `carrySticky` keep stickies for a replace landing on the target key only. User pushes and Back/Forward pops still clear. Merge and delete route tests cover it (`watchlist.test.tsx:1709, 1740`), and the commit-probe Back test (`2068`) proves the same-commit drop. A Back change that only touches `q` still clears after paint through the `view.q` effect. That is acceptable, because `query` itself also resyncs in that effect. | +| WR-04 (title unreachable on a disabled button) | **Partially resolved** | Mouse hover now reaches the wrapper's `title`. Moving it off the button removed the hint's accessible description, and touch and keyboard users still have no way to read it. See WR-02 below. | -No security issues found. +I traced the carry state machine (`reconcileSticky` / `carrySticky`) through these paths: merge, delete, no-op `replaceSelectedTag`, canonicalization after a pop, first-load validation, a same-tick write layered on an unlanded push, and the q debounce. It converges, and it does not keep stickies across a user filter change. I found no security issues. ## Warnings -### WR-01: A chip click on a sticky card unmounts the card before the filter commits, dropping the scroll anchor and focus - -**File:** `web/app/routes/watchlist.tsx:441-448, 459-465` (with `web/app/lib/useScrollAnchor.ts:12-18`) -**Issue:** `toggleFilterTag` captures the chip and then calls `handleTagsChange`. That runs `write()`, which is a router navigation, and then `clearSticky()`. The two updates commit separately: -- `clearSticky()` is a discrete-event (sync-lane) state update. It commits at the end of the click. -- The navigation is async inside the router. React Router 7.18.2 then applies the new location inside `React.startTransition`, so it commits later. - -The first commit therefore renders the **old** filter with stickies cleared. Take a card that was visible only because it was sticky: say the user just removed tag A while filtering by A, and the card still carries B. That commit removes the card. - -The second commit applies the new filter. In this example that is `tags=A,B` with OR semantics (`matchesAnyTag`), so the card matches again and is re-mounted as a **new** DOM node. When `useScrollAnchor`'s layout effect runs on the key change, the captured chip has `isConnected === false`, so no correction is made. Two further effects follow: -- The page can visibly collapse and re-expand between the two commits. -- The focused chip button is destroyed, so focus falls to ``. The TagChips contract asserts `label` keeps focus. - -No test combines a sticky card with a chip click. The G-25-4 tests only use fresh, non-sticky rows. - -**Fix:** Make the sticky invalidation part of the same render as the URL change instead of a separate state write. For example, key the sticky set to the filter it was created under and derive it during render: -```tsx -const [sticky, setSticky] = useState<{ key: string; ids: ReadonlySet }>( - () => ({ key: filterKey, ids: new Set() }) -) -const stickyIds = sticky.key === filterKey ? sticky.ids : EMPTY_SET - -function markSticky(id: number) { - setSticky((prev) => { - const ids = prev.key === filterKey ? prev.ids : EMPTY_SET - return ids.has(id) ? prev : { key: filterKey, ids: new Set(ids).add(id) } - }) -} -// drop clearSticky() from handleTagsChange/handleFlagChange/handleSortChange -// and the filterKey effect; keep the q path (query is local state). -``` -This also removes the post-paint flash in WR-03. Add a route test: make a card sticky, click one of its chips, and assert `scrollBy` was called and the chip still has focus. - -### WR-02: The anchor is silently dropped whenever the clicked card leaves the list, so the G-25-4 collapse returns - -**File:** `web/app/lib/useScrollAnchor.ts:15`, `web/app/routes/watchlist.test.tsx:2182-2210` -**Issue:** The reworded contract (25-14, UAT test 4) says that after a chip-label click "the clicked card stays where it was on screen." Tag filtering is OR (`matchesAnyTag`). So un-pressing a chip while two or more tags are selected removes every card that carries only that tag, including the clicked one. That is the exact case in `?tags=3,7`, clicking "trap". - -When that happens `a.el.isConnected` is false, the hook returns, and the page collapses under the viewport with no compensation. This is the original "it drops after clicking a tag" symptom. The test at line 2182 enshrines this as the intended outcome instead of providing a fallback. -**Fix:** Capture fallback anchors at click time and use the first one still connected. Candidates are the clicked card's `
  • `, its next and previous sibling cards, and the list heading: -```ts -const capture = useCallback((els: HTMLElement[]) => { - anchor.current = els.map((el) => ({ el, top: el.getBoundingClientRect().top })) -}, []) -// layout effect: const a = anchor.current?.find((x) => x.el.isConnected) -``` -In `TagChips`/`toggleFilterTag`, pass `[chip, chip.closest("li")?.nextElementSibling, chip.closest("li")?.previousElementSibling]` filtered to elements. Then update the line-2182 test to expect a correction based on the surviving neighbor. - -### WR-03: The `filterKey` effect clears sticky cards on the route's own URL writes, not just Back/Forward, and only after paint +### WR-01: The fallback anchor only checks the two immediate neighbours, so the G-25-4 collapse returns when a run of cards leaves -**File:** `web/app/routes/watchlist.tsx:153-161` (interacts with `274-286`, `296-298`, `165-184`) -**Issue:** The comment says the effect exists because "Back/Forward changes filters without going through a handler." But it fires on **any** `filterKey` change, and the route itself rewrites `tags` programmatically in two places: -- `replaceSelectedTag` runs when Manage tags merges or deletes a selected tag. -- The canonicalization effect runs `settleTagIds`. +**File:** `web/app/lib/useScrollAnchor.ts:3-10, 12-14, 28-29`; `web/app/lib/useScrollAnchor.test.tsx:217-226` +**Issue:** `anchorCandidates` returns `[chip, card.nextElementSibling, card.previousElementSibling]`. Tag filtering is any-of (`matchesAnyTag`). When a selected chip is un-pressed, every card that carries only that tag leaves, not just the clicked one. -Concrete regression introduced by this diff: -1. Filter by `rap`. -2. Remove `rap` from card X. X stays because it is sticky. -3. Open Manage tags and merge `rap` into `hip-hop`. `replaceSelectedTag(3, 9)` rewrites `tags=9`, `filterKey` changes, and `clearSticky()` runs. -4. X vanishes, even though the user never changed the filter. Before 2cd98c2 it stayed. +With the default name sort, cards that carry only the un-pressed tag are interleaved with the others, so runs of adjacent leavers are common. Example: `?tags=3,7` over ten cards where five carry only tag 3. If the clicked card's next and previous siblings also carry only tag 3, every candidate is disconnected. The layout effect then returns without correcting, which is the original "it drops after clicking a tag" symptom. -Separately, the effect runs after paint. On Back the first painted frame still shows the stale sticky cards, and they disappear one frame later. -**Fix:** Preferred: the render-derived sticky set from WR-01 fixes the flash. For the merge case, have `applyTagChange` carry the sticky set across the key it is about to write. Alternatively, set a `selfWriteRef` before `replaceSelectedTag` or the canonical `write`, and skip the clear when the key change came from that write: +The hook test "calls no scroll API when no candidate survives" (line 217) treats this as the intended outcome. The header comment also overstates the behaviour: "or its nearest surviving neighbour" is really "one of its two adjacent cards". +**Fix:** Capture every sibling card's top at click time. After the commit, choose the connected one closest in DOM order to the clicked card: ```ts -const selfWriteRef = useRef(false) -// in replaceSelectedTag / canonical effect, before write(): selfWriteRef.current = true -useEffect(() => { - if (prevFilterKey.current === filterKey) return - prevFilterKey.current = filterKey - if (selfWriteRef.current) { selfWriteRef.current = false; return } - clearSticky() -}, [filterKey]) +export function anchorCandidates(el: HTMLElement): HTMLElement[] { + const card = el.closest("li") + if (!card?.parentElement) return [el] + const cards = [...card.parentElement.children].filter( + (c): c is HTMLElement => c instanceof HTMLElement + ) + const i = cards.indexOf(card) + const out: HTMLElement[] = [el] + for (let d = 1; d < cards.length; d++) { + if (cards[i + d]) out.push(cards[i + d]) + if (cards[i - d]) out.push(cards[i - d]) + } + return out +} ``` +If measuring every card at click time is a concern, cap `d` at a small window, such as 10. Then change the line-217 test so that a surviving card two positions away is used. Also fix the header comment, or the UI-SPEC D-12 line if the two-neighbour limit is intentional. -### WR-04: The truncated "Search MusicBrainz to add" label relies on a `title` tooltip that can never appear +### WR-02: Moving `title` to the wrapper strips the Deezer hint's accessible description, and touch and keyboard users still cannot read it -**File:** `web/app/components/watchlist/SearchResultsColumns.tsx:192-201`, `web/app/components/watchlist/SearchResultsColumns.test.tsx:167-193` -**Issue:** Below `lg`, the Deezer button is now capped at `max-w-[40%]` and its label truncates. In a 375px row the cap is about 115px, so the label reads roughly "Search Mus…". At `sm`, each of the two grid columns is about 285px, so the label is similarly cut. +**File:** `web/app/components/watchlist/SearchResultsColumns.tsx:195-203`; `web/app/components/watchlist/SearchResultsColumns.test.tsx:183-194` +**Issue:** Before 25-16, `title` sat on the native ` + {DEEZER_ADD_HINT} ``` -If you keep the tooltip, change the test to assert the wrapper's `title`. - -## Info +In the test, replace `not.toHaveAttribute("title")` with `expect(button).toHaveAccessibleDescription(DEEZER_ADD_HINT_TEXT)`. For touch, consider a visible one-line note under the Deezer column heading, like the existing "unavailable" `

    `, instead of relying on per-row hover. -### IN-01: The `format.ts` header comment is stale +### WR-03: When the clicked card leaves the list, the focused chip is destroyed and focus falls to `` -**File:** `web/app/lib/format.ts:7-19` -**Issue:** The header claims "Six formatters, six call sites" and lists them. The module now also exports `formatReleaseDate` and `formatDetectedDate`, so the inventory is wrong. It is also the long header style that CLAUDE.md's comment discipline asks to avoid. -**Fix:** Replace the inventory with a 1-3 line intent summary. Per-function comments already document each formatter. +**File:** `web/app/routes/watchlist.tsx:491-497`; `web/app/lib/useScrollAnchor.ts:25-32` +**Issue:** 25-17 now handles scroll position on the "clicked card leaves" path, but not focus. When a selected chip is un-pressed and its card no longer matches, the focused label button unmounts with the card, and `document.activeElement` becomes ``. A keyboard user (Enter/Space on the chip) is sent back to the top of the tab order. -### IN-02: One History card mixes two date formats +The UI-SPEC focus table only defines "Card chip label | Toggled, card still visible | stays on that label button", so there is no rule for this case. The previous-card and next-card route tests (`watchlist.test.tsx:2330, 2367`) never assert where focus goes. +**Fix:** After the scroll correction in the layout effect, move focus to a sensible target if the captured element had focus and is now disconnected. The surviving fallback card's first chip label or the card itself works, or the list heading (`#watchlist-artists-heading`) with `tabIndex={-1}`: +```ts +const hadFocus = anchor.current?.[0]?.el === document.activeElement // capture time +// in the layout effect, after scrollBy: +if (hadFocus && !candidates[0].el.isConnected) { + ;(a.el.querySelector("button") ?? a.el).focus({ preventScroll: true }) +} +``` +Add a "Toggled, card leaves" row to the UI-SPEC focus table, and assert focus in the two WR-02 route tests. -**File:** `web/app/components/history/EventCard.tsx:86-96` (vs `122`, `176`, `212`) -**Issue:** The new line renders `Detected 11 Oct 2026`. The release date in all three bodies is still the raw ISO string, `event.release_date ?? "Release date unknown"`, which renders as `2015-04-08`. The `formatDetectedDate` comment says it "match[es] formatReleaseDate", but EventCard never calls `formatReleaseDate`. One card now shows `2015-04-08` directly above `Detected 11 Oct 2026`. -**Fix:** Render release dates through `formatReleaseDate(event.release_date)` in `NewReleaseBody`, `GuestFeatureBody` and `DeluxeChangeBody`. Then update the `getByText(/2015-04-08/)` assertion. +## Info -### IN-03: The local-calendar behavior of `formatDetectedDate` is untested +### IN-01: Dead stub and a misleading comment in the previous-card route test -**File:** `web/app/lib/format.test.ts:142-153` -**Issue:** Vitest pins `TZ=UTC` (`web/vitest.config.ts`), so `getDate()` and `getUTCDate()` give identical results. Swapping to UTC accessors, or adopting a different local-day rule, would pass every case. The function's stated intent is a local date. -**Fix:** Add a case that derives the expectation from local accessors on an instant near midnight. Alternatively, document that local-vs-UTC is covered only by `formatAbsoluteTime`'s existing TZ self-check. +**File:** `web/app/routes/watchlist.test.tsx:2336-2351` +**Issue:** The comment says "The label detaches, so key the rect on read order: 400 first, then 120". The label is read once at capture time. After the commit it is disconnected and skipped by `find(isConnected)`, so the `120` branch never runs. The test passes because of the `Daddy Yankee` `stubCardTop` alone. +**Fix:** Drop the read-order mock, or replace it with `expect(label.isConnected).toBe(false)` so the test states why the fallback is used. -### IN-04: The sort-announcement tests can pass without cancellation on a slow runner +### IN-02: `clearFilters` re-derives "will the filter key change" by hand, and neither branch is tested -**File:** `web/app/routes/watchlist.test.tsx:1404-1451` -**Issue:** `shouldAdvanceTime: true` advances the fake clock with real time. If typing "a" and picking the sort option take more than 500ms of real time, the count announcement fires **before** the sort. The sort message then overwrites it, and "cancels a pending count announcement" passes even if `handleSortChange` stopped clearing `countTimer`. -**Fix:** Before the sort pick, assert that the timer is still pending, for example `expect(status()).not.toHaveTextContent("Showing")` right after typing. That way a vacuous run fails instead of passing. +**File:** `web/app/routes/watchlist.tsx:517` +**Issue:** `if (view.tagIds.length === 0 && !view.muted && !view.custom) clearSticky()` duplicates the knowledge that `CLEAR_FILTER_KEYS` minus `q` makes up the URL-owned part of `filterKeyOf`. Adding a key to either list silently breaks the predicate. No route test makes a card sticky and then runs Clear filters (`grep` finds no sticky assertion near any "Clear filters" click). +**Fix:** Derive the predicate from the key itself, for example `if (filterKeyOf({ ...view, tagIds: [], muted: false, custom: false }) === filterKey) clearSticky()`. Alternatively, call `clearSticky()` unconditionally, since it is idempotent and the landing reset makes it harmless. Add one sticky-then-Clear-filters test for the q-only branch. -### IN-05: G-25-4 has jsdom-only coverage, while the gap's debug plan required a browser-level check +### IN-03: The browser-level checks behind WR-01, WR-02 and WR-04 are still outstanding -**File:** `web/app/lib/useScrollAnchor.test.tsx`, `web/app/routes/watchlist.test.tsx:2099-2211` -**Issue:** Every anchor test mocks `getBoundingClientRect` and `scrollBy`. Three things the real failure depends on are never exercised: -- the two-commit transition ordering (WR-01), -- native CSS scroll anchoring (`overflow-anchor`), -- browser clamping on short pages. +**File:** `web/app/lib/useScrollAnchor.test.tsx`; `web/app/routes/watchlist.test.tsx:2222-2428`; `web/app/components/watchlist/SearchResultsColumns.test.tsx:167-199` +**Issue:** All anchor tests stub `getBoundingClientRect` and `scrollBy`. 25-15 D4, 25-16 D2, 25-17 D3 and 25-18 D4 are `human_judgment: true` and deferred to `/gsd-verify-work`. These checks are still unverified: +- how the hook's `scrollBy` interacts with native `overflow-anchor` (Chromium/Firefox) and with page-end clamping; +- the native tooltip actually appearing through `disabled:pointer-events-none`. -The G-25-4 diagnosis listed "Browser-level test (not jsdom) that the clicked chip stays roughly in place" as a required artifact. -**Fix:** Add a Playwright-style check, or record a manual UAT step that covers a sticky-card chip click and an un-press with two or more tags selected (WR-01, WR-02). +This carries over prior IN-05. Prior IN-01 to IN-04 (format.ts header, EventCard date mix, TZ-pinned `formatDetectedDate`, sort-announcement timing) were outside 25-15 to 25-17 and are unchanged. +**Fix:** Run the UAT steps in a real browser before closing the phase: a sticky-card chip click, un-pressing with two or more tags selected (last card, middle card, and a run of adjacent leavers), and Deezer pill hover at 375/640/1024px. --- -_Reviewed: 2026-10-11T20:41:29Z_ +_Reviewed: 2026-10-12T20:25:38Z_ _Reviewer: Claude (gsd-code-reviewer)_ _Depth: standard_ From 16c1e4d3e1a4bcf94783851d19cae78a2f11ec78 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 15:34:13 -0500 Subject: [PATCH 116/142] docs(25): re-verify phase after gap closure 25-15..25-18 --- .../25-VERIFICATION.md | 267 +++++++++--------- 1 file changed, 134 insertions(+), 133 deletions(-) diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md b/.planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md index fa190f7..d6ef4a8 100644 --- a/.planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md @@ -1,8 +1,8 @@ --- phase: 25-find-filter-watchlist-and-history -verified: 2026-10-11T20:55:00Z -status: gaps_found -score: 9/11 must-haves verified +verified: 2026-10-12T20:32:03Z +status: human_needed +score: 18/21 must-haves verified covered_files: - .planning/REQUIREMENTS.md - .planning/phases/25-find-filter-watchlist-and-history/25-01-PLAN.md @@ -33,8 +33,18 @@ covered_files: - .planning/phases/25-find-filter-watchlist-and-history/25-13-SUMMARY.md - .planning/phases/25-find-filter-watchlist-and-history/25-14-PLAN.md - .planning/phases/25-find-filter-watchlist-and-history/25-14-SUMMARY.md + - .planning/phases/25-find-filter-watchlist-and-history/25-15-PLAN.md + - .planning/phases/25-find-filter-watchlist-and-history/25-15-SUMMARY.md + - .planning/phases/25-find-filter-watchlist-and-history/25-16-PLAN.md + - .planning/phases/25-find-filter-watchlist-and-history/25-16-SUMMARY.md + - .planning/phases/25-find-filter-watchlist-and-history/25-17-PLAN.md + - .planning/phases/25-find-filter-watchlist-and-history/25-17-SUMMARY.md + - .planning/phases/25-find-filter-watchlist-and-history/25-18-PLAN.md + - .planning/phases/25-find-filter-watchlist-and-history/25-18-SUMMARY.md + - .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md - .planning/phases/25-find-filter-watchlist-and-history/25-REVIEW.md - .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md + - .planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md - internal/events/service.go - internal/httpserver/events.go - internal/httpserver/events_test.go @@ -66,62 +76,49 @@ covered_files: - web/app/routes/history.tsx - web/app/routes/watchlist.test.tsx - web/app/routes/watchlist.tsx -covered_digest: "v1:sha256:4138975a44bd5bc51f09251a687f30275bd7c88eae97a8da29c4ae0e159b9faa" +covered_digest: "v1:sha256:936afd60e0b433d234625edec48053e8537d5d60a8d7a9d5cd84924ec969eb8e" behavior_unverified: 0 overrides_applied: 0 re_verification: - previous_status: human_needed - previous_score: 5/5 + previous_status: gaps_found + previous_score: 9/11 gaps_closed: - - "G-25-7: History cards show `Detected {date}` from created_at; retention stays on created_at; detected-recently/released-long-ago regression test passes" - - "G-25-9 (code level): Deezer button capped at max-w-[40%] below lg with a truncating label span; type line truncates (real-browser layout check still pending)" - - "G-25-4 main path: a chip-label click on a non-sticky card anchors the clicked chip in both directions (jsdom route tests)" - - "Prior review WR-01..WR-04 (2cd98c2 review) fixed in 344d69b, 000b090, 4bdc558, 9cc7050" - gaps_remaining: - - "G-25-4: chip-label click on a sticky card (one just edited or added under the filter) loses the scroll anchor and focus" + - "G-25-4 sticky path (prior WR-01): a chip-label click on a sticky card keeps the same label element connected and focused, and scrollBy runs once with the drift. The prior verifier's red probe is now the committed route test watchlist.test.tsx:2392, which passes." + - "Prior WR-03 regression: per the 2026-10-11 user decision, a Manage tags merge or delete of a selected tag now keeps sticky cards (route tests :1709 and :1740 pass), and the post-paint filterKey effect is gone" + - "Prior decision items 10-12 (WR-02, WR-03, WR-04) were decided by the user on 2026-10-11 and implemented in 25-15, 25-16, and 25-17, as recorded in UI-SPEC and CONTEXT G4" + gaps_remaining: [] regressions: - - "Review WR-03: since 000b090, a Manage tags merge or delete of a selected tag clears sticky cards (probe test red). Whether that breaks G4 depends on how the contract is read; the user needs to decide (see human_verification)" -gaps: - - truth: "G-25-4: the router never resets scroll, and after a chip-label click far down a 50+ artist list the clicked card stays where it was on screen (clamped by the browser on short pages)" - status: partial - reason: "Fails when the clicked card is sticky (G4: edited in-card or added under the active filter) and does not match the old filter. handleTagsChange calls clearSticky() (sync lane), and React Router 7.18.2 applies the navigation inside React.startTransition (node_modules/react-router/dist/development/chunk-62JRHF6Z.mjs ~6752). So the first commit renders the old filter without the sticky card and unmounts it. The second commit applies the new filter and mounts a new DOM node. useScrollAnchor's captured chip is then disconnected, so no correction runs and focus falls to . A verifier probe test went red: start at /?tags=3, remove reggaeton from Bad Bunny (now sticky), click Bad Bunny's `Filter by trap`. Result: URL ?tags=3,7, card shown again, label.isConnected=false, activeElement=BODY, window.scrollBy never called (expected (0,-280)). This also breaks the 25-11 truth that the chip label keeps focus on itself (D-08) on this path. This is review WR-01." - artifacts: - - path: "web/app/routes/watchlist.tsx" - issue: "handleTagsChange (~441-448) clears stickyIds in a separate sync-lane state write that commits before the router's transition-lane location update; toggleFilterTag (~459) relies on the anchor surviving both commits" - - path: "web/app/lib/useScrollAnchor.ts" - issue: "Correct for a stable element, but the anchored element is destroyed by the intermediate commit, so the isConnected guard skips the correction" - - path: "web/app/routes/watchlist.test.tsx" - issue: "G-25-4 describe (~2100-2211) only uses fresh, non-sticky rows; no test combines a sticky card with a chip click" - missing: - - "Make sticky invalidation part of the same render as the URL change (e.g. key the sticky set to the filterKey it was created under and derive it during render, as 25-REVIEW.md WR-01 proposes), so the clicked sticky card is never unmounted between commits" - - "Route test: make a card sticky (detach a selected tag), click one of its other chip labels, assert window.scrollBy was called with the drift and the label still has focus and is the same element" + - "Deezer hint pill: moving `title` from the disabled `. +- `web/app/components/watchlist/SearchResultsColumns.test.tsx`: + - The test at lines 167-200 asserts the wrapper is a SPAN with `max-w-[40%] shrink-0 lg:max-w-none`, no `pointer-events-none`, and a title containing "MusicBrainz identity". It also asserts the button is disabled with `w-full`, no `max-w-[40%]`, and `not.toHaveAttribute("title")`, and that `button.children` has length 1 (the truncating label span). + - The test at lines 289-309 asserts the Deezer row's `textContent` equals `` `${deezerArtist.name}${deezerArtist.type}Search MusicBrainz to add` ``. + - The `deezerArtist` fixture is at lines 33-41. + - @testing-library/jest-dom 7.0.1 provides `toHaveAccessibleDescription`. +- `25-UI-SPEC.md` line 188 (inside the page-order code block) starts ` SearchBox "Search artists" + SearchResultsColumns` and ends with `... so it shows on hover (WR-04, user decision 2026-10-11))`. After 25-19 and 25-20, the UI-SPEC diff against `9d3413f` is one removed and two added lines. +- Embedded bundle marker baselines under `internal/webassets/build/client/assets` at `9d3413f` (via `git grep -o -F`): `parentElement` 9, `data-chip-label` 0, `aria-describedby` 3. Minification keeps DOM property names (`parentElement`) and JSX attribute keys (`data-chip-label`, `aria-describedby`). Plans 25-19 and 25-20 don't touch the bundle, so the tree is unchanged between `9d3413f` and the start of this plan. +- `Makefile`: + - The `web` target (line 139) runs a frozen install, a build, and then replaces `internal/webassets/build/client`. + - `SQLC_VERSION := v1.31.1`. + - `sqlc-check` depends on `sqlc-version-check`. + + + + + + Task 1: The disabled Deezer button announces its reason through aria-describedby + web/app/components/watchlist/SearchResultsColumns.tsx, web/app/components/watchlist/SearchResultsColumns.test.tsx + + - `web/app/components/watchlist/SearchResultsColumns.tsx` lines 1-12 and 127-221 (imports, DEEZER_ADD_HINT, SearchResultRow). + - `web/app/components/watchlist/SearchResultsColumns.test.tsx` lines 1-80 (fixtures), 146-248 (Deezer and uncapped-button tests), and 289-309 (row text test). + + + - The disabled `Search MusicBrainz to add` button has the accessible description DEEZER_ADD_HINT, and its accessible name is still `Search MusicBrainz to add`. + - The existing assertions on the button and wrapper still hold: the wrapper is a SPAN with the 40% cap classes and the hint title, and the button has no title, `w-full`, and exactly one child, the truncating label span. + - The description element has class `sr-only`, contains exactly DEEZER_ADD_HINT, sits inside the row, and is not inside the button. + - With two Deezer results, the two buttons' `aria-describedby` values differ, and each resolves via `document.getElementById` to an element whose text is DEEZER_ADD_HINT. + - The `Add to Watchlist` and `Already watching` buttons have no `aria-describedby`. + - The Deezer row's `textContent` is the name, then the type, then `Search MusicBrainz to add`, then DEEZER_ADD_HINT. The sr-only text is part of the row's text content. + + +Per user decision 2026-10-12 (G-25-13, review WR-02: add aria-describedby pointing at an sr-only hint). + +RED first. Update and add the tests from the behavior block, importing `DEEZER_ADD_HINT` from `./SearchResultsColumns`. Run the +first verify command and confirm that the description assertions fail. Then implement in `SearchResultsColumns.tsx`: +- Export `DEEZER_ADD_HINT`, so the test asserts the exact string. +- Import `useId` alongside `useState`, and call `const hintId = useId()` once at the top of `SearchResultRow`. Hooks run + unconditionally, so call it before any branch. +- On the disabled Deezer `Button`, add `aria-describedby={hintId}`. +- Inside the wrapper span, directly after that `Button`, render a span with `id={hintId}` and `className="sr-only"` whose only child + is `{DEEZER_ADD_HINT}` as plain JSX text. +- Keep the wrapper's `title` and classes exactly as they are, and leave the label span inside the button untouched. +- Add nothing visible, and use no other attribute changes. +- Amend the `// WR-04:` comment to say, in at most two lines, that `aria-describedby` gives the button the same hint for screen + readers (G-25-13). + +In the test at lines 167-200, keep every existing assertion and add `expect(button).toHaveAccessibleDescription(DEEZER_ADD_HINT)`. +Add a test with two Deezer artists that checks distinct, resolvable ids. In the uncapped-buttons test, add assertions that `Add to +Watchlist` and `Already watching` have no `aria-describedby`. Update the expected `textContent` in the lines 289-309 test to append +`DEEZER_ADD_HINT`. + +Before staging, run `corepack pnpm --dir web exec prettier --write "**/*.{ts,tsx}"`, then run the remaining verify commands +(CLAUDE.md DoD item 6). Commit once after GREEN, with hooks enabled and no AI attribution, for example `fix(25-21): describe the +disabled Deezer button with its MusicBrainz hint`. + + + corepack pnpm --dir web exec vitest run app/components/watchlist/SearchResultsColumns.test.tsx --coverage.enabled=false + corepack pnpm --dir web run typecheck && grep -qF 'export const DEEZER_ADD_HINT' web/app/components/watchlist/SearchResultsColumns.tsx && grep -qF 'useId()' web/app/components/watchlist/SearchResultsColumns.tsx && grep -qF 'aria-describedby={hintId}' web/app/components/watchlist/SearchResultsColumns.tsx && grep -qF 'toHaveAccessibleDescription(DEEZER_ADD_HINT)' web/app/components/watchlist/SearchResultsColumns.test.tsx && grep -qF 'title={DEEZER_ADD_HINT}' web/app/components/watchlist/SearchResultsColumns.tsx + corepack pnpm --dir web exec prettier --check "**/*.{ts,tsx}" && corepack pnpm --dir web test + non-zero exit: a failing SearchResultsColumns test (before the change the description assertion fails); an "error TS" line; the export, `useId()`, `aria-describedby={hintId}`, the accessible-description assertion, or the wrapper title missing; prettier listing files under "Code style issues"; any "FAIL" line; or a line beginning "ERROR: Coverage for" + + + - `expect(button).toHaveAccessibleDescription(DEEZER_ADD_HINT)` passes, and `getByRole("button", { name: "Search MusicBrainz to add" })` still finds the button. + - Two Deezer rows get distinct `aria-describedby` ids that each resolve to the hint text. + - The wrapper keeps `title={DEEZER_ADD_HINT}` and its cap classes, and the button keeps no title and exactly one child. + - Before the commit, `corepack pnpm --dir web exec prettier --check "**/*.{ts,tsx}"` and the full `corepack pnpm --dir web test` both exit 0 (CLAUDE.md DoD item 6). + + Screen readers announce why the Deezer button is unavailable, with no visual change. Committed. + + + + Task 2: Record the accessible description on the UI-SPEC page-order SearchResultsColumns line (one line) + .planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md + + - `.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md` lines 184-200 (page-order code block; line 188 is the SearchResultsColumns note). + + +Make one scoped `Edit` on line 188, the line starting ` SearchBox "Search artists" + SearchResultsColumns`, per user decision +2026-10-12 (G-25-13). Never use a whole-file `Write`. Replace the line's closing text `(WR-04, user decision 2026-10-11))` with exactly: + +`(WR-04, user decision 2026-10-11); the disabled button also carries that hint as its accessible description, through aria-describedby on an sr-only span with a per-row useId id, with no visual change (G-25-13, user decision 2026-10-12))` + +Keep the line's leading whitespace and the rest of the text unchanged, with no wrap. Change nothing else in the UI-SPEC, and leave +25-CONTEXT.md, 25-UAT.md, and 25-VERIFICATION.md alone. Commit the doc alone with hooks enabled and no AI attribution, for example +`docs(25-21): record the Deezer hint's accessible description in the UI-SPEC`. + + + f=.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md; [ "$(grep -c 'SearchBox "Search artists" + SearchResultsColumns' "$f")" -eq 1 ] && grep -F 'SearchBox "Search artists" + SearchResultsColumns' "$f" | grep -qF 'aria-describedby on an sr-only span with a per-row useId id, with no visual change (G-25-13, user decision 2026-10-12))' && grep -qF 'widened to the nearest surviving card by G-25-11, user decision 2026-10-12' "$f" && grep -q '^| Card chip label | Toggled, card leaves |.*G-25-12, user decision 2026-10-12' "$f" && d=$(git diff -U0 9d3413f -- "$f") && removed=$(printf '%s\n' "$d" | grep -E '^-' | grep -vE '^--- (a/|/dev/null)' | wc -l) && added=$(printf '%s\n' "$d" | grep -E '^\+' | grep -vE '^\+\+\+ (b/|/dev/null)' | wc -l) && echo "removed=$removed added=$added" && test "$removed" -eq 2 && test "$added" -eq 3 && test -z "$(git diff --name-only 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md .planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md)" + non-zero exit: the page-order line is missing, duplicated, or lacks the G-25-13 text; 25-19's D-12 line or 25-20's focus row is gone; the cumulative UI-SPEC diff against `9d3413f` is not exactly two removed and three added lines; or CONTEXT, UAT, or VERIFICATION changed + + + - The single page-order SearchResultsColumns line ends with the G-25-13 accessible-description clause. + - `git diff -U0 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md` shows exactly two removed and three added content lines, from 25-19, 25-20, and this task. + - 25-CONTEXT.md, 25-UAT.md, and 25-VERIFICATION.md are unchanged since `9d3413f`. + + The UI design contract states the Deezer hint's accessible description. Committed. + + + + Task 3: Rebuild the embedded SPA, run the Definition of Done, and confirm the blast radius + Tasks 1-2 and plans 25-19 and 25-20 are committed (`git status --porcelain web/` prints nothing), the dev Postgres answers on localhost:5432 (`docker compose ps postgres` healthy), and `corepack pnpm --dir web install --frozen-lockfile` can resolve from the local store or the registry. + internal/webassets/build/client + + - `.claude/CLAUDE.md` "Definition of Done" (gate order; never `--no-verify`). + - `Makefile` `web` target (line 139), `test-integration` (`COVER_PKGS`), `coverage-gate`, and `sqlc-check` recipes. + - `.planning/phases/25-find-filter-watchlist-and-history/25-18-PLAN.md` Tasks 1 and 3 (the same rebuild, marker method, and gate chain) and `25-18-SUMMARY.md` "Accepted exceptions". + + +Rebuild by running the `make web` recipe directly: +- `corepack pnpm --dir web install --frozen-lockfile` +- `corepack pnpm --dir web run build` +- `rm -rf internal/webassets/build/client` +- `mkdir -p internal/webassets/build` +- `cp -r web/build/client internal/webassets/build/client` + +If the install modifies `web/package.json` or `web/pnpm-lock.yaml`, restore both with `git checkout --` and stop. + +Confirm that the bundle carries each fix by comparing marker counts against the `9d3413f` tree (second verify command). Each count +must be strictly greater than its baseline: +- `parentElement` comes from 25-19's `anchorCandidates`. +- `data-chip-label` comes from 25-20's chip label attribute and focus selector. +- `aria-describedby` comes from this plan's Deezer button. + +The vendor chunks can't account for an increase, because the lockfile is frozen and the blast-radius check pins the package files. + +Then run the Definition of Done in order, before committing: +1. `go vet ./...` +2. `golangci-lint run` (from `$HOME/go/bin`) +3. The integration suite with a coverage profile, using the Makefile's `COVER_PKGS` equivalent, without `-race`. This is the Go chain + in the third verify command, which is 25-18's chain with only the log name changed. +4. The coverage gate: `go run ./cmd/coverage-report --mode=total --profile=coverage.out` must print 80 or more. +5. The full `sqlc-check` recipe, including its `sqlc-version-check` guard first. `sqlc version` must equal the Makefile's + `SQLC_VERSION` before `sqlc generate` runs. Then `git diff --exit-code -- internal/db/sqlc/` must be clean. +6. `corepack pnpm --dir web run typecheck`, then `corepack pnpm --dir web exec prettier --check "**/*.{ts,tsx}"`, then + `corepack pnpm --dir web test` (70% on all four axes). + +If a gate goes red, fix it in the file of the plan that owns it (25-19, 25-20, or this plan's Task 1), commit that fix separately, +rebuild the bundle again, and re-run. Then confirm the blast radius against `9d3413f` (fifth verify command). + +Commit the refreshed tree with hooks enabled and no AI attribution, for example `build(25-21): rebuild the embedded SPA with the +G-25-11..13 fixes`. The deleted files are superseded hashed assets, which is intended. + +In the SUMMARY, record: +- The `before -> after` marker counts. +- An `Accepted exceptions` heading carrying the line `Accepted exception: CI-only race (carried from user decision 2026-10-11, + 25-14)`. The reason: cgo is disabled locally, no Go source changed in 25-19 to 25-21, and CI's Linux test job runs `-race`. + + + corepack pnpm --dir web install --frozen-lockfile && corepack pnpm --dir web run build && rm -rf internal/webassets/build/client && mkdir -p internal/webassets/build && cp -r web/build/client internal/webassets/build/client && out=$(git status --porcelain internal/webassets/build/client) && test -n "$out" + non-zero exit from the install, build, or copy; or `test -n` failing because the bundle did not change + ok=1; git cat-file -e 9d3413f:internal/webassets/build/client/assets || ok=0; for m in 'parentElement' 'data-chip-label' 'aria-describedby'; do bo=$(git grep -o -F -e "$m" 9d3413f -- internal/webassets/build/client/assets; test $? -le 1) || ok=0; b=$(printf '%s' "$bo" | grep -c .); n=$(grep -r -o -F -e "$m" internal/webassets/build/client/assets | wc -l); echo "$m: $b -> $n"; [ "$n" -gt "$b" ] || ok=0; done; [ "$ok" = 1 ] && go build ./... + non-zero exit: the `9d3413f` baseline tree can't be read (a git error, never counted as zero); a marker's count in the rebuilt assets is not strictly greater than at `9d3413f` (the printed `before -> after` line names it; on the pre-change tree the check fails with `data-chip-label: 0 -> 0`); or the embed doesn't compile + go vet ./... && PATH="$HOME/go/bin:$PATH" golangci-lint run && { TEST_DATABASE_URL='postgres://drop_tracker:drop_tracker@localhost:5432/drop_tracker?sslmode=disable' go test ./... -count=1 -coverprofile=coverage.out -coverpkg="$(go list ./... | grep -vE '(^|/)(internal/db/sqlc|cmd/coverage-report|cmd/migrate|cmd/migration-check)$' | paste -sd, -)" > "${TMPDIR:-/tmp}/gotest-25-21.log" 2>&1; true; } && ! grep -E '^(FAIL|--- FAIL)' "${TMPDIR:-/tmp}/gotest-25-21.log" | grep -v 'internal/testutil' && cov=$(go run ./cmd/coverage-report --mode=total --profile=coverage.out) && echo "Backend coverage: $cov" && awk -v c="$cov" 'BEGIN { exit !(c + 0 >= 80) }' && want=$(sed -n 's/^SQLC_VERSION := //p' Makefile) && got=$(PATH="$HOME/go/bin:$PATH" sqlc version) && test -n "$want" && { [ "$got" = "$want" ] || { echo "sqlc version mismatch: want $want, got $got" >&2; false; }; } && PATH="$HOME/go/bin:$PATH" sqlc generate && git diff --exit-code -- internal/db/sqlc/ + any vet or lint diagnostic; any `FAIL` or `--- FAIL` line in the test log other than internal/testutil's known `go: no such tool "covdata"` failure; coverage below 80; `SQLC_VERSION` missing from the Makefile or a `sqlc version mismatch` (the guard runs before generate); or a diff under internal/db/sqlc/ + corepack pnpm --dir web run typecheck && corepack pnpm --dir web exec prettier --check "**/*.{ts,tsx}" && corepack pnpm --dir web test + non-zero exit, any "error TS" line, prettier listing files under "Code style issues", any "FAIL" line, or a line beginning "ERROR: Coverage for" + out=$(git diff --name-only 9d3413f -- '*.go' queries internal/db internal/events go.mod go.sum web/package.json web/pnpm-lock.yaml web/app/components/ui web/app/lib/useUrlParams.ts web/app/root.tsx .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md .planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md) && test -z "$out" && changed=$(git diff --name-only 9d3413f -- web/app) && extra=$(printf '%s\n' "$changed" | grep -vxE 'web/app/(lib/useScrollAnchor\.ts|lib/useScrollAnchor\.test\.tsx|routes/watchlist\.tsx|routes/watchlist\.test\.tsx|components/watchlist/TagChips\.tsx|components/watchlist/SearchResultsColumns\.tsx|components/watchlist/SearchResultsColumns\.test\.tsx)'; true) && echo "unexpected web/app changes: ${extra:-none}" && test -z "$extra" + non-zero exit: a Go source file, the SQL, generated code, the events service, a migration, a dependency file, the vendored ui/ components, useUrlParams, root.tsx, 25-CONTEXT.md, 25-UAT.md, or 25-VERIFICATION.md changed since `9d3413f`; or a web/app file outside the seven gap-plan files changed (the printed paths name it) + + Run the built binary (or `docker compose up` with the rebuilt image) against the dev DB with 50+ artists. On the Watchlist, scrolled far down: (1) at `?tags=A,B`, un-press a chip on a card whose next and previous cards also carry only that tag (G-25-11); (2) repeat (1) with the keyboard (Tab to the chip label, press Enter), then repeat it on a filter where no card survives (G-25-12); (3) search `Bad Bunny` and move a screen reader (NVDA or VoiceOver) onto a Deezer row's disabled button, then hover the truncated pill (G-25-13). + (1) The nearest surviving card stays where it was on screen, clamped only when the filtered page is shorter than the window. (2) The focus ring lands on that surviving card's pressed chip label without moving the page. When nothing survives, focus lands on the `Your artists` heading, never on the top of the page. (3) The screen reader announces `Search MusicBrainz to add, button, unavailable` followed by the hint, with no visible change to the row, and hovering still shows the native tooltip. + Real-browser layout, focus rings, and assistive-tech output (human_verify_mode: end-of-phase). jsdom stubs layout and has no screen reader. These are the /gsd-verify-work re-test items for G-25-11, G-25-12, and G-25-13. + + + + - `git status --porcelain internal/webassets/build/client` is non-empty before staging, and `go build ./...` exits 0. + - Each of `parentElement`, `data-chip-label`, and `aria-describedby` occurs more often under `internal/webassets/build/client/assets` than in the `9d3413f` tree. + - Backend coverage prints 80 or more, and the web suite reports 70% or more on statements, branches, functions, and lines. + - The sqlc version guard passes before `sqlc generate` runs, and the sqlc diff is clean. + - Both blast-radius checks against `9d3413f` are empty. + - The SUMMARY records `Accepted exception: CI-only race (carried from user decision 2026-10-11, 25-14)` with its reason, plus the marker counts. + + The Go binary serves G-25-11, G-25-12, and G-25-13, every Definition of Done gate is green, the gap closure touched only its intended surface, and the re-test items are ready for /gsd-verify-work. Committed. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Third-party catalogue strings (Deezer) → rendered row | Artist name, type, and country render as plain JSX text (unchanged). The new hint is a static constant | +| Package registry → `pnpm install --frozen-lockfile` | Build-time dependency resolution for the bundle | +| Built bundle → go:embed → served SPA | The committed tree is what the binary serves | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-25-56 | Tampering (XSS) | Deezer hint span | low | mitigate | The span's only child is the static `DEEZER_ADD_HINT` constant, rendered as JSX text. No catalogue string feeds the description, and the id comes from React's `useId`, not from a Deezer value | +| T-25-SC | Tampering | npm installs during the rebuild | high | mitigate | No package is added. The rebuild uses `--frozen-lockfile`, and the blast-radius diff asserts that `web/package.json` and `web/pnpm-lock.yaml` are unchanged since `9d3413f` | +| T-25-57 | Tampering | Stale embedded bundle | medium | mitigate | The rebuild is verified on its own. The bundle must change, and each fix's marker (`parentElement`, `data-chip-label`, `aria-describedby`) must occur more often than in the `9d3413f` tree | +| T-25-58 | Elevation / scope creep | Go source, SQL, vendored components, decision and verification docs | low | mitigate | The blast-radius diff over `*.go`, queries/, internal/db, internal/events, ui/, useUrlParams.ts, root.tsx, 25-CONTEXT.md, 25-UAT.md, and 25-VERIFICATION.md must be empty, and web/app changes must stay within the seven gap-plan files | +| T-25-59 | Repudiation | Spec drift | low | mitigate | Task 2's verify fails unless the cumulative UI-SPEC diff against `9d3413f` is exactly two removed and three added lines carrying the G-25-11, G-25-12, and G-25-13 tags | + + + +- `corepack pnpm --dir web exec vitest run app/components/watchlist/SearchResultsColumns.test.tsx --coverage.enabled=false` passes, including `toHaveAccessibleDescription(DEEZER_ADD_HINT)`. +- The UI-SPEC carries the three G-25-11/12/13 lines, with a cumulative diff of two removed and three added lines against `9d3413f`. +- The bundle carries all three markers above their `9d3413f` counts, and `go build ./...` compiles the embed. +- The Go chain, the web chain, and both blast-radius checks exit 0, apart from the documented covdata line. +- After the SUMMARY is written: `grep -qF 'Accepted exception: CI-only race (carried from user decision 2026-10-11, 25-14)' .planning/phases/25-find-filter-watchlist-and-history/25-21-SUMMARY.md` exits 0. + + + +Screen readers announce the Deezer button's reason with no visual change (G-25-13). The Go binary serves the widened fallback anchor +(G-25-11) and the focus move (G-25-12), the UI design contract states all three decisions, every CLAUDE.md gate is green, and the +three gaps are ready for re-verification. + + + +Create `.planning/phases/25-find-filter-watchlist-and-history/25-21-SUMMARY.md` when done. Its `Accepted exceptions` section must carry +the line `Accepted exception: CI-only race (carried from user decision 2026-10-11, 25-14)` with the reason. Also list the +`before -> after` marker counts. + From 1cb2a7b37952f2270427a0d9a504e433926fd71e Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 16:54:03 -0500 Subject: [PATCH 119/142] docs(25): record CI-only race waiver for gap closure 25-19..25-21 --- .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-UAT.md b/.planning/phases/25-find-filter-watchlist-and-history/25-UAT.md index ca23578..c435014 100644 --- a/.planning/phases/25-find-filter-watchlist-and-history/25-UAT.md +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-UAT.md @@ -206,3 +206,4 @@ blocked: 0 ## Execution Waivers - waiver: "CI-only race (user decision 2026-10-11). The user accepted running the gap-closure integration suite without -race locally, because cgo is disabled on this Windows box. CI's race job remains the gate. Applies to gap-closure plans 25-11..25-14 only." +- waiver: "CI-only race extended (user decision 2026-10-12). The user accepted running the gap-closure integration suite without -race locally for gap-closure plans 25-19..25-21. CI's race job remains the gate." From 5969c99c9414223a4f965d8f67367548873e78b5 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 17:00:03 -0500 Subject: [PATCH 120/142] docs(25): revise gap closure plans 25-19..25-21 per checker feedback 25-20: focus relocation is keyboard-only (click detail === 0); mouse-click no-move tests; route tests cited by name. 25-21: race exception sourced from the 2026-10-12 UAT waiver. 25-19: note why CONTEXT stays unchanged. UAT checks now baseline on 1cb2a7b. --- .../25-19-PLAN.md | 10 +- .../25-20-PLAN.md | 207 ++++++++++++------ .../25-21-PLAN.md | 44 ++-- 3 files changed, 166 insertions(+), 95 deletions(-) diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-19-PLAN.md b/.planning/phases/25-find-filter-watchlist-and-history/25-19-PLAN.md index 2673c9d..4b0d198 100644 --- a/.planning/phases/25-find-filter-watchlist-and-history/25-19-PLAN.md +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-19-PLAN.md @@ -71,6 +71,10 @@ card. Claude's discretion (recorded): at each distance the next card is tried be next-then-previous order (25-17, WR-02 2026-10-11) as the distance-1 special case, so the A1..E5 probe anchors on E5. There is no distance cap, because a cap would bring back the collapse for a longer run. +25-CONTEXT.md stays unchanged even though the gap's `missing` list says "UI-SPEC/CONTEXT": no CONTEXT decision states the anchor's +fallback order (its D-12 covers URL params, D-08 the chip toggle, G4 sticky cards), so the UI-SPEC D-12 scroll bullet is the only +decision text that records the rule, and Task 2 rewrites it. + Purpose: the clicked card's neighbourhood stays put on screen for any run length. G-25-12 (plan 25-20) builds its focus target on this same fallback card. Output: the widened `anchorCandidates`, hook and route tests for the run case, and the D-12 bullet in 25-UI-SPEC rewritten to match. @@ -208,13 +212,13 @@ Keep the rest of the bullet as it is, and keep the bullet on one line with no wr fallback anchor in the UI-SPEC D-12 bullet`. - f=.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md; [ "$(grep -c '^- \*\*Scroll reset and anchoring (D-12, G-25-4):\*\*' "$f")" -eq 1 ] && grep '^- \*\*Scroll reset and anchoring (D-12, G-25-4):\*\*' "$f" | grep -qF 'widened to the nearest surviving card by G-25-11, user decision 2026-10-12' && ! grep -qF 'with neither left' "$f" && d=$(git diff -U0 9d3413f -- "$f") && removed=$(printf '%s\n' "$d" | grep -E '^-' | grep -vE '^--- (a/|/dev/null)' | wc -l) && added=$(printf '%s\n' "$d" | grep -E '^\+' | grep -vE '^\+\+\+ (b/|/dev/null)' | wc -l) && echo "removed=$removed added=$added" && test "$removed" -eq 1 && test "$added" -eq 1 && test -z "$(git diff --name-only 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md .planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md)" - non-zero exit: the D-12 bullet is missing or duplicated; it lacks the G-25-11 tag; the old two-neighbour sentence survives; the UI-SPEC diff against `9d3413f` is not exactly one removed and one added line (an extra edit or a wrapped line); or CONTEXT, UAT, or VERIFICATION changed + f=.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md; [ "$(grep -c '^- \*\*Scroll reset and anchoring (D-12, G-25-4):\*\*' "$f")" -eq 1 ] && grep '^- \*\*Scroll reset and anchoring (D-12, G-25-4):\*\*' "$f" | grep -qF 'widened to the nearest surviving card by G-25-11, user decision 2026-10-12' && ! grep -qF 'with neither left' "$f" && d=$(git diff -U0 9d3413f -- "$f") && removed=$(printf '%s\n' "$d" | grep -E '^-' | grep -vE '^--- (a/|/dev/null)' | wc -l) && added=$(printf '%s\n' "$d" | grep -E '^\+' | grep -vE '^\+\+\+ (b/|/dev/null)' | wc -l) && echo "removed=$removed added=$added" && test "$removed" -eq 1 && test "$added" -eq 1 && test -z "$(git diff --name-only 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md .planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md)" && test -z "$(git diff --name-only 1cb2a7b -- .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md)" + non-zero exit: the D-12 bullet is missing or duplicated; it lacks the G-25-11 tag; the old two-neighbour sentence survives; the UI-SPEC diff against `9d3413f` is not exactly one removed and one added line (an extra edit or a wrapped line); CONTEXT or VERIFICATION changed since `9d3413f`; or UAT changed since `1cb2a7b` (the commit that recorded the 2026-10-12 race waiver) - The single D-12 scroll bullet ends with the widened-rule sentence tagged `widened to the nearest surviving card by G-25-11, user decision 2026-10-12`. - `git diff -U0 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md` shows exactly one removed and one added content line. - - 25-CONTEXT.md, 25-UAT.md, and 25-VERIFICATION.md are unchanged since `9d3413f`. + - 25-CONTEXT.md and 25-VERIFICATION.md are unchanged since `9d3413f`, and 25-UAT.md is unchanged since `1cb2a7b`. The UI design contract states the widened fallback rule the user chose for G-25-11. Committed. diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-20-PLAN.md b/.planning/phases/25-find-filter-watchlist-and-history/25-20-PLAN.md index 31d2333..9e67dfc 100644 --- a/.planning/phases/25-find-filter-watchlist-and-history/25-20-PLAN.md +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-20-PLAN.md @@ -6,6 +6,7 @@ wave: 2 depends_on: [25-19] files_modified: - web/app/components/watchlist/TagChips.tsx + - web/app/components/watchlist/TagChips.test.tsx - web/app/lib/useScrollAnchor.ts - web/app/lib/useScrollAnchor.test.tsx - web/app/routes/watchlist.tsx @@ -18,42 +19,49 @@ review_ids: [WR-03] requirements: [WLVW-04] estimate: - tokens: 70000 - raw_tokens: 70000 + tokens: 75000 + raw_tokens: 75000 tasks: 3 confidence: low must_haves: truths: - "User decision 2026-10-12 (G-25-12, review WR-03: anchor): when a keyboard user toggles a chip label and its card leaves the list, focus moves to a chip label on the fallback anchor card, the same surviving card the scroll anchor uses (G-25-11, plan 25-19). It prefers a pressed label, so focus never falls to ." - - "Example: at `/?tags=3,7` (Bad Bunny and Daddy Yankee [reggaeton], Travis Scott [trap]), pressing Enter on Travis Scott's `Filter by trap` lands focus on Daddy Yankee's pressed `Filter by reggaeton`. Pressing Enter on Daddy Yankee's `Filter by reggaeton` lands focus on Travis Scott's pressed `Filter by trap`." - - "When no card survives the toggle (the filtered-to-zero empty state replaces the list), focus moves to the `Your artists` heading, which now has tabIndex -1 and stays mounted in both states." + - "Keyboard only (user decision 2026-10-12, G-25-12 scope): focus moves only for a keyboard-initiated toggle, Enter or Space on the focused label, which the browser reports as a click with `detail === 0`. A mouse or touch click (`detail` 1 or more) that removes the card leaves focus alone, even though Chromium and Firefox focus a button on click." + - "Example: at `/?tags=3,7` (Bad Bunny and Daddy Yankee [reggaeton], Travis Scott [trap]), pressing Enter on Travis Scott's `Filter by trap` lands focus on Daddy Yankee's pressed `Filter by reggaeton`. Pressing Enter on Daddy Yankee's `Filter by reggaeton` lands focus on Travis Scott's pressed `Filter by trap`. A mouse click on Travis Scott's `Filter by trap` filters the same way but focuses nothing." + - "When no card survives a keyboard toggle (the filtered-to-zero empty state replaces the list), focus moves to the `Your artists` heading, which now has tabIndex -1 and stays mounted in both states." - "The focus move uses `{ preventScroll: true }` and runs after the scroll correction, so it never undoes the anchor." - - "Focus moves only when the toggled label held focus at click time and focus actually fell to the document. A click that never focused the label, or a focus the user moved elsewhere before the commit, is left alone." - - "Unchanged: when the toggled card stays visible, focus stays on that same label button (UI-SPEC focus table row 'Toggled, card still visible', and route tests at lines 2173 and 2392)." - - "25-UI-SPEC's focus table has a 'Card chip label | Toggled, card leaves' row, tagged `G-25-12, user decision 2026-10-12`, directly after the 'Toggled, card still visible' row." + - "Besides the keyboard check, focus moves only when the toggled label held focus at click time and focus actually fell to the document. A programmatic click on an unfocused label (which also reports `detail === 0`), or a focus the user moved elsewhere before the commit, is left alone." + - "Unchanged: when the toggled card stays visible, focus stays on that same label button (UI-SPEC focus table row 'Toggled, card still visible', and the route tests `filters to the tag's carriers, mirrors it in the toolbar, and keeps focus on the label` and `keeps a sticky card's chip anchored and focused when the chip filters (WR-01)`)." + - "25-UI-SPEC's focus table has a 'Card chip label | Toggled, card leaves' row, tagged `G-25-12, user decision 2026-10-12`, directly after the 'Toggled, card still visible' row. The row states the keyboard-only scope." artifacts: - path: web/app/lib/useScrollAnchor.ts - provides: "`useScrollAnchor(key, onFocusLost?)`: reports the surviving anchor, or null, through useEffectEvent when the focused captured element left the DOM" + provides: "`useScrollAnchor(key, onFocusLost?)` returning `capture(el, viaKeyboard?)`: reports the surviving anchor, or null, through useEffectEvent when a keyboard capture's focused element left the DOM" exports: ["useScrollAnchor", "anchorCandidates"] contains: "useEffectEvent" - path: web/app/components/watchlist/TagChips.tsx - provides: "`data-chip-label` on each chip's filter-toggle button, mirroring `data-chip-remove`" + provides: "`data-chip-label` on each chip's filter-toggle button, mirroring `data-chip-remove`, and `e.detail === 0` passed as toggleFilterTag's viaKeyboard argument" contains: "data-chip-label" + - path: web/app/components/watchlist/TagChips.test.tsx + provides: "toggleFilterTag receives viaKeyboard false for a mouse click and true for Enter on the focused label" - path: web/app/routes/watchlist.tsx - provides: "The focus target: the fallback card's pressed chip label, then any chip label, then the tabIndex -1 list heading, focused with preventScroll" + provides: "`toggleFilterTag(tag, anchor?, viaKeyboard?)` forwarding viaKeyboard to the capture, and the focus target: the fallback card's pressed chip label, then any chip label, then the tabIndex -1 list heading, focused with preventScroll" contains: "preventScroll: true" - path: web/app/lib/useScrollAnchor.test.tsx - provides: "onFocusLost cases: survivor, null, chip stays, never focused, and focus moved elsewhere" + provides: "onFocusLost cases: keyboard survivor, null, chip stays, mouse click (detail 1), never focused, and focus moved elsewhere" - path: web/app/routes/watchlist.test.tsx - provides: "Keyboard Enter route cases: last card, middle card, run of leavers, and no survivor (heading)" + provides: "Keyboard Enter route cases (last card, middle card, run of leavers, no survivor to the heading) and a mouse-click case that moves no focus" - path: .planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md provides: "Focus table row 'Toggled, card leaves'" contains: "G-25-12, user decision 2026-10-12" key_links: + - from: "web/app/components/watchlist/TagChips.tsx chip label onClick" + to: "web/app/routes/watchlist.tsx toggleFilterTag -> captureScrollAnchor" + via: "actions.toggleFilterTag(tag, e.currentTarget, e.detail === 0) -> captureScrollAnchor(anchor, viaKeyboard)" + pattern: "detail === 0" - from: "web/app/routes/watchlist.tsx useScrollAnchor(filterKey, focusAfterCardLeaves)" to: "web/app/lib/useScrollAnchor.ts layout effect" - via: "capture records hadFocus -> layout effect after scrollBy -> captured chip disconnected and focus on -> useEffectEvent -> onFocusLost(surviving card or null)" + via: "capture records viaKeyboard && hadFocus -> layout effect after scrollBy -> captured chip disconnected and focus on -> useEffectEvent -> onFocusLost(surviving card or null)" pattern: "useEffectEvent" - from: "web/app/routes/watchlist.tsx focus target query" to: "web/app/components/watchlist/TagChips.tsx chip label button" @@ -64,7 +72,7 @@ must_haves: requirement_id: WLVW-04 category: safety verification: test - - statement: "MUST NOT move focus when the toggled card stays visible, when the label did not hold focus at click time, or when focus has already moved to another element." + - statement: "MUST NOT move focus for a mouse or touch toggle (click `detail` 1 or more), when the toggled card stays visible, when the label did not hold focus at click time, or when focus has already moved to another element." requirement_id: WLVW-04 category: safety verification: test @@ -85,7 +93,16 @@ then sent back to the top of the tab order. The 25-08 truth ("keeps focus on the Locked decision (binding). Cite it as "user decision 2026-10-12 (G-25-12)": focus moves to the fallback anchor card's chip label, and the fallback card is the one plan 25-19 resolves (depends on G-25-11). +Scope (binding, user decision 2026-10-12 on the plan checker's follow-up): keyboard only. Focus relocation fires only for a +keyboard-initiated toggle, Enter or Space on the focused label. A mouse click that removes a card leaves focus alone. A held-focus +check alone can't tell them apart, because Chromium and Firefox focus a button on mouse click. + Claude's discretion (recorded): +- Detect keyboard activation with the click event's `detail === 0`, read in TagChips' label `onClick` and passed through + `toggleFilterTag` to the capture. `detail` is the click count, so a mouse or touch click reports 1 or more, and Enter or Space + activation of a button reports 0. It is stateless and per event. Tracking the last pointerdown versus keydown instead would need + document listeners and goes stale for a click that fires neither. A programmatic `click()` also reports 0, so the held-focus and + focus-fell-to-the-document checks stay as guards. - Prefer a pressed chip label, as the gap asks. After a chip-click filter every surviving card carries a still-selected tag, because the click clears stickiness (G4), so a pressed label exists in practice. The any-label step is a guard. - When no card survives, focus the `Your artists` heading with tabIndex -1. The heading stays mounted in both the list and the @@ -95,8 +112,8 @@ Claude's discretion (recorded): `key` alone, and the route keeps the knowledge of chip labels and the heading. Purpose: keyboard users keep their place in the list after a toggle that removes the card they were on. -Output: the hook's focus-lost callback, the chip-label marker attribute, the route's focus target with the heading fallback, tests, -and the new UI-SPEC focus table row. +Output: the keyboard flag from the chip label to the capture, the hook's focus-lost callback, the chip-label marker attribute, the +route's focus target with the heading fallback, tests, and the new UI-SPEC focus table row. @@ -121,17 +138,29 @@ Facts verified at the planning head `9d3413f`. Plan 25-19 changes only `anchorCa - React is 19.2.6 and @types/react is 19.2.15. `useEffectEvent(callback: T): T` is exported by both. - `web/app/components/watchlist/TagChips.tsx` lines 187-204: the chip label button (`type="button"`, `aria-pressed={active}`, `` aria-label={`Filter by ${tag.name}`} ``, `onClick={(e) => actions.toggleFilterTag(tag, e.currentTarget)}`). The × beside it has a - bare `data-chip-remove` attribute (line 209). Pending chips render no label button. + bare `data-chip-remove` attribute (line 209). Pending chips render no label button. The `TagActions` interface (line 27) declares + `toggleFilterTag(tag: TagRef, anchor?: HTMLElement): void`. +- `web/app/components/watchlist/TagChips.test.tsx`: the test "clicking the label toggles the filter and never attaches or detaches" + runs `await userEvent.click(label)` and asserts `expect(toggleFilterTag).toHaveBeenCalledWith({ id: 3, name: "reggaeton" }, label)`. + `toHaveBeenCalledWith` matches the exact argument list, so a third argument breaks it unless the expectation gains it. +- Click `detail` in the test stack (verified in `web/node_modules`): + - user-event 14.6.4 dispatches the keyboard-activation `click` (Enter on keydown, Space on keyup) with `detail` 0. + - `userEvent.click` dispatches `detail` 1 and first focuses the button on mousedown, as Chromium and Firefox do. + - `fireEvent.click` from @testing-library/dom defaults `detail` to 0, so a mouse-like `fireEvent` click needs `{ detail: 1 }`. + - React's synthetic MouseEvent exposes `detail`. +- `web/app/lib/useScrollAnchor.test.tsx` imports `render, screen` from @testing-library/react and the default `userEvent`. Add + `fireEvent` to that import if a test uses it. - `web/app/routes/watchlist.tsx`: - Line 1 imports `useCallback, useEffect, useRef, useState`. - Line 195 is `const captureScrollAnchor = useScrollAnchor(filterKey)`. - - Lines 491-497 are `toggleFilterTag(tag, anchor?)`, which captures, then `handleTagsChange` (a push write). + - Lines 491-497 are `toggleFilterTag(tag: TagRef, anchor?: HTMLElement)`, which calls `captureScrollAnchor(anchor)` when an anchor + is given, then `handleTagsChange` (a push write). Its only caller is the `TagChips` actions object (line 631). - Lines 585-590 hold the `

    Your artists

    `. It has no tabIndex and no ref, and it renders whenever `entries.length > 0`, including the filtered-to-zero state. - The filtered-to-zero state replaces the `
      ` with `EmptyState` heading `No artists match these filters` (lines 613-618). - `web/app/routes/watchlist.test.tsx`: - It imports `within` from @testing-library/react, `makeEntry` from `~/lib/test/fixtures`, and `LocationProbe, renderRoute` from `~/lib/test/routeStub`. - - Describe "Watchlist card chip click-to-filter (D-08)" (lines 2147-2220) has the test at 2173, "keeps focus on the label". - - Describe "chip click keeps the clicked card on screen (G-25-4)" (lines 2222+) has the sticky-chip focus test at 2392 and, after 25-19, the five-card run case. + - Describe "Watchlist card chip click-to-filter (D-08)" (lines 2147-2220) holds the test `filters to the tag's carriers, mirrors it in the toolbar, and keeps focus on the label`, which clicks with `userEvent.click` and asserts the label keeps focus. + - Describe "chip click keeps the clicked card on screen (G-25-4)" (lines 2222+) holds the sticky-chip test `keeps a sticky card's chip anchored and focused when the chip filters (WR-01)` (also `userEvent.click`) and, after 25-19, the five-card run case. 25-19 inserts a test into this describe, so locate tests by name, not by line number. - A muted fixture uses `muted_event_types: ["guest_feature"]` (line 1904). - `25-UI-SPEC.md` focus table (lines 416-428). Line 425 is `| Card chip label | Toggled, card still visible | stays on that label button |`. - `25-CONTEXT.md` needs no change. G4's "supersedes UI-SPEC's card-removal focus flow" is about in-card edits, which never remove a card. A chip-click filter is a G4 action that clears stickiness, so a card leaving on a chip toggle is consistent with G4, and no CONTEXT line governs focus. @@ -140,46 +169,52 @@ Facts verified at the planning head `9d3413f`. Plan 25-19 changes only `anchorCa - Task 1: Enter on a leaving card's chip lands focus on the fallback card's pressed chip label, end to end + Task 1: Enter on a leaving card's chip lands focus on the fallback card's pressed chip label, end to end, and a mouse click moves no focus Plan 25-19 is committed: `grep -c 'parentElement' web/app/lib/useScrollAnchor.ts` prints 1 or more. - web/app/components/watchlist/TagChips.tsx, web/app/lib/useScrollAnchor.ts, web/app/lib/useScrollAnchor.test.tsx, web/app/routes/watchlist.tsx, web/app/routes/watchlist.test.tsx + web/app/components/watchlist/TagChips.tsx, web/app/components/watchlist/TagChips.test.tsx, web/app/lib/useScrollAnchor.ts, web/app/routes/watchlist.tsx, web/app/routes/watchlist.test.tsx - `web/app/lib/useScrollAnchor.ts` (whole file, as left by 25-19). - - `web/app/lib/useScrollAnchor.test.tsx` (the five-card harness and run describe that 25-19 added, to extend). - - `web/app/components/watchlist/TagChips.tsx` lines 180-215 (the chip label button and the × with `data-chip-remove`). + - `web/app/components/watchlist/TagChips.tsx` lines 20-30 (`TagActions`) and 180-215 (the chip label button and the × with `data-chip-remove`). + - `web/app/components/watchlist/TagChips.test.tsx` lines 515-560 (`renderFilterable` and the label-toggle test whose exact-argument expectation changes). - `web/app/routes/watchlist.tsx` lines 190-200 and 488-500 (hook call and `toggleFilterTag`). - `web/app/routes/watchlist.test.tsx` lines 2147-2240 (D-08 describe, fixtures, and the `shown` and `search` helpers). - Route, `/?tags=3,7` with Bad Bunny [reggaeton], Daddy Yankee [reggaeton], and Travis Scott [trap]: focus Travis Scott's `Filter by trap`, then press Enter. The URL becomes `?tags=3`, the label is disconnected, and Daddy Yankee's `Filter by reggaeton` (aria-pressed true) has focus. Its `focus` was called with `{ preventScroll: true }`. - Route, same rows: focus Daddy Yankee's `Filter by reggaeton`, then press Enter. The URL becomes `?tags=7`, shown is [Travis Scott], and Travis Scott's `Filter by trap` has focus. - - Hook, five-card harness: a focused chip whose card leaves, with A1 and E5 surviving, calls onFocusLost once with E5's element. When E5 drifted, scrollBy ran before onFocusLost (`mock.invocationCallOrder`). - - Hook: no card survives, so onFocusLost is called once with null and scrollBy is not called. - - Hook: the chip's card stays, so onFocusLost is not called. - - Hook: the chip never held focus at capture (`fireEvent.click`, which moves no focus, the way a Safari mouse click behaves), so onFocusLost is not called. - - Hook: the chip held focus at capture, but focus moved to another element before the key change, so onFocusLost is not called. - - All existing one-argument hook tests and the route tests at lines 2173 and 2392 pass unedited. When the card stays, focus stays on the clicked label. + - Route, same rows, mouse (keyboard-only scope): `await userEvent.click` on Travis Scott's `Filter by trap`. user-event focuses the button on mousedown and clicks with `detail` 1, as Chromium and Firefox do. The URL becomes `?tags=3` and the label is disconnected. The spy on Daddy Yankee's `Filter by reggaeton` `focus` was not called, and `document.activeElement` is `document.body`. + - TagChips: `userEvent.click` on the label calls `toggleFilterTag` with `({ id: 3, name: "reggaeton" }, label, false)`. Focusing the label and pressing Enter calls it with `({ id: 3, name: "reggaeton" }, label, true)`. + - All existing one-argument hook tests pass unedited, and so do the route tests `filters to the tag's carriers, mirrors it in the toolbar, and keeps focus on the label` and `keeps a sticky card's chip anchored and focused when the chip filters (WR-01)`. When the card stays, focus stays on the clicked label. -Per user decision 2026-10-12 (G-25-12, review WR-03: move focus to the fallback anchor card's chip label). +Per user decision 2026-10-12 (G-25-12, review WR-03: move focus to the fallback anchor card's chip label), keyboard toggles only +(user decision 2026-10-12, G-25-12 scope). -RED first. Write the route and hook tests from the behavior block, run the first verify command, and record that focus is on BODY in -the route cases and that onFocusLost is never called. Then implement: +RED first. Write the route and TagChips tests from the behavior block, run the first verify command, and record two failures: focus +is on BODY in the two Enter route cases, and `toggleFilterTag` is called without the third argument. The mouse route case may pass +before the change, because nothing moves focus yet. It pins the keyboard-only scope against the implementation. Then implement: -1. `TagChips.tsx`: add a bare `data-chip-label` attribute to the chip's filter-toggle button, mirroring `data-chip-remove` on the ×. - Change nothing else in the file. +1. `TagChips.tsx`: + - Add a bare `data-chip-label` attribute to the chip's filter-toggle button, mirroring `data-chip-remove` on the ×. + - Change the label's `onClick` to `actions.toggleFilterTag(tag, e.currentTarget, e.detail === 0)`. + - Widen `TagActions.toggleFilterTag` to `(tag: TagRef, anchor?: HTMLElement, viaKeyboard?: boolean): void`. + - At most one comment line, saying `detail` is 0 only for an Enter or Space activation (G-25-12). + - Change nothing else in the file. 2. `useScrollAnchor.ts`: add an optional second parameter `onFocusLost?: (fallback: HTMLElement | null) => void`. - - Capture also records whether the captured element held focus, using `el.contains(document.activeElement)`. + - The returned capture becomes `(el: HTMLElement, viaKeyboard = false) => void`. One-argument callers and the existing + one-argument hook tests compile and behave as before, with no focus move. + - Capture records `viaKeyboard && el.contains(document.activeElement)`: the toggle came from the keyboard and the captured + element held focus. - Wrap the callback with React 19.2's `useEffectEvent`, so the layout effect still depends on `[key]` alone. A new callback identity on each render must never re-run the effect or consume the anchor early. - - In the layout effect, after the scroll correction, check three things: the captured element held focus, it is now - disconnected, and focus actually fell to the document (`document.activeElement` is null or `document.body`). When all three - hold, call the event with the candidate the correction used, or null when no candidate is connected. The function must still - reach this check when no candidate survives, so the null case reports. - - Keep the `(el: HTMLElement) => void` return. - - Extend the header comment by at most one line naming G-25-12. -3. `watchlist.tsx`: add a route function `focusAfterCardLeaves(fallback)` and pass it as the second argument of - `useScrollAnchor(filterKey, ...)`. + - In the layout effect, after the scroll correction, check three things: the recorded keyboard-and-focus flag is true, the + captured element is now disconnected, and focus actually fell to the document (`document.activeElement` is null or + `document.body`). When all three hold, call the event with the candidate the correction used, or null when no candidate is + connected. The function must still reach this check when no candidate survives, so the null case reports. + - Extend the header comment by at most one line naming G-25-12 and the keyboard-only scope. +3. `watchlist.tsx`: + - `toggleFilterTag(tag, anchor?, viaKeyboard?)` forwards the flag as `captureScrollAnchor(anchor, viaKeyboard)`. + - Add a route function `focusAfterCardLeaves(fallback)` and pass it as the second argument of `useScrollAnchor(filterKey, ...)`. - It looks inside the fallback card for `[data-chip-label][aria-pressed="true"]`, then for `[data-chip-label]`. - It focuses the first match with `{ preventScroll: true }`, so focusing never undoes the anchor. - Selectors are constant strings. Never interpolate a tag name into a selector. @@ -188,54 +223,79 @@ the route cases and that onFocusLost is never called. Then implement: - Give it its own `shown` and `search` helpers, plus `afterEach(() => vi.restoreAllMocks())`. - Spy `window.scrollBy` to a no-op in each test. - Drive the keyboard path: call `label.focus()`, then `await userEvent.keyboard("{Enter}")`. + - Drive the mouse path with `await userEvent.click(label)` alone. Don't use `fireEvent.click` here, because its default `detail` + of 0 looks like a keyboard activation. - Spy the target label's `focus` without replacing it, so it still focuses. -5. `useScrollAnchor.test.tsx`: extend 25-19's five-card harness so `useScrollAnchor(key, onFocusLost)` receives a `vi.fn()`. - - Let the chip's own click capture it, apply a survivor set, and bump the key in one handler, the way the route does. - - Keep separate capture and leave controls for the focus-moved-elsewhere case. In that case, call `chip.focus()`, then - `fireEvent.click` the capture control, focus another button, and `fireEvent.click` the leave control. +5. `TagChips.test.tsx`: + - In the test "clicking the label toggles the filter and never attaches or detaches", append `false` to the + `toHaveBeenCalledWith` expectation. That is the only edit to an existing test. + - Add one test that focuses the label, presses Enter with `userEvent.keyboard("{Enter}")`, and expects + `toHaveBeenCalledWith({ id: 3, name: "reggaeton" }, label, true)`. + +The hook's unit cases for `onFocusLost` are in Task 2. Before staging, run `corepack pnpm --dir web exec prettier --write "**/*.{ts,tsx}"`, then run the remaining verify commands (CLAUDE.md DoD item 6). Commit once after GREEN, with hooks enabled and no AI attribution, for example `fix(25-20): move focus to the -fallback card's chip label when the toggled card leaves`. +fallback card's chip label when a keyboard toggle removes the card`. corepack pnpm --dir web exec vitest run app/lib/useScrollAnchor.test.tsx app/routes/watchlist.test.tsx app/components/watchlist/TagChips.test.tsx --coverage.enabled=false - corepack pnpm --dir web run typecheck && [ "$(grep -c 'data-chip-label' web/app/components/watchlist/TagChips.tsx)" -ge 1 ] && grep -qF 'useEffectEvent' web/app/lib/useScrollAnchor.ts && grep -qF 'preventScroll: true' web/app/routes/watchlist.tsx && grep -qF '[data-chip-label][aria-pressed="true"]' web/app/routes/watchlist.tsx && out=$(git diff --name-only 9d3413f -- web/app/lib/useUrlParams.ts web/app/components/ui web/package.json web/pnpm-lock.yaml) && test -z "$out" + corepack pnpm --dir web run typecheck && [ "$(grep -c 'data-chip-label' web/app/components/watchlist/TagChips.tsx)" -ge 1 ] && grep -qF 'detail === 0' web/app/components/watchlist/TagChips.tsx && grep -qF 'useEffectEvent' web/app/lib/useScrollAnchor.ts && grep -qF 'preventScroll: true' web/app/routes/watchlist.tsx && grep -qF '[data-chip-label][aria-pressed="true"]' web/app/routes/watchlist.tsx && out=$(git diff --name-only 9d3413f -- web/app/lib/useUrlParams.ts web/app/components/ui web/package.json web/pnpm-lock.yaml) && test -z "$out" corepack pnpm --dir web exec prettier --check "**/*.{ts,tsx}" && corepack pnpm --dir web test - non-zero exit: a failing hook, route, or TagChips test (before the change the route cases show focus on BODY); an "error TS" line; the `data-chip-label` attribute, `useEffectEvent`, `preventScroll: true`, or the pressed-label selector missing; any change to useUrlParams.ts, components/ui, or the package files since `9d3413f`; prettier listing files under "Code style issues"; any "FAIL" line; or a line beginning "ERROR: Coverage for" + non-zero exit: a failing hook, route, or TagChips test (before the change the Enter route cases show focus on BODY and the TagChips Enter case lacks the `true` argument); a mouse click moving focus; an "error TS" line; the `data-chip-label` attribute, the `detail === 0` keyboard check, `useEffectEvent`, `preventScroll: true`, or the pressed-label selector missing; any change to useUrlParams.ts, components/ui, or the package files since `9d3413f`; prettier listing files under "Code style issues"; any "FAIL" line; or a line beginning "ERROR: Coverage for" - Both route keyboard cases leave focus on the fallback card's pressed `Filter by …` label, never on BODY, and the target's `focus` was called with `{ preventScroll: true }`. - - The five hook onFocusLost cases from the behavior block pass: survivor reported after scrollBy, null, chip stays, never focused, and focus moved elsewhere. - - The route tests at lines 2173 and 2392 and every existing hook test pass unedited. + - The route mouse case (`userEvent.click`, `detail` 1) filters to `?tags=3` and moves no focus: the fallback label's `focus` spy is not called and `document.activeElement` is `document.body`. + - TagChips passes `false` as the third `toggleFilterTag` argument for a mouse click and `true` for Enter. The only change to an existing TagChips test is the appended `false`. + - The route tests `filters to the tag's carriers, mirrors it in the toolbar, and keeps focus on the label` and `keeps a sticky card's chip anchored and focused when the chip filters (WR-01)`, and every existing hook test, pass unedited. - `git diff --name-only 9d3413f -- web/app/lib/useUrlParams.ts web/app/components/ui web/package.json web/pnpm-lock.yaml` prints nothing. - Before the commit, `corepack pnpm --dir web exec prettier --check "**/*.{ts,tsx}"` and the full `corepack pnpm --dir web test` both exit 0 (CLAUDE.md DoD item 6). - A keyboard toggle that removes the card lands focus on the surviving fallback card's pressed chip label. Committed. + A keyboard toggle that removes the card lands focus on the surviving fallback card's pressed chip label, and a mouse click that removes it moves no focus. Committed. - Task 2: Focus the list heading when no card survives, and pin focus through a run of leavers - web/app/routes/watchlist.tsx, web/app/routes/watchlist.test.tsx + Task 2: Focus the list heading when no card survives, pin the hook's keyboard-only focus gates, and pin focus through a run of leavers + web/app/routes/watchlist.tsx, web/app/routes/watchlist.test.tsx, web/app/lib/useScrollAnchor.test.tsx - `web/app/routes/watchlist.tsx` lines 580-640 (section, heading, and the list versus `EmptyState` branch) and the `focusAfterCardLeaves` function from Task 1. - `web/app/routes/watchlist.test.tsx` lines 1900-1915 (muted fixture shape), the G-25-12 describe from Task 1, and 25-19's five-card run case. + - `web/app/lib/useScrollAnchor.ts` (as left by Task 1) and `web/app/lib/useScrollAnchor.test.tsx` (the five-card harness and run describe that 25-19 added, to extend). - Route, `/?tags=3,7&muted=1` with Bad Bunny [reggaeton, muted_event_types ["guest_feature"]] and Travis Scott [trap, unmuted]: shown is [Bad Bunny]. Focus Bad Bunny's `Filter by reggaeton` and press Enter. The `No artists match these filters` empty state shows, and the `Your artists` heading has focus and `tabindex="-1"`. - Route, 25-19's five-card fixture at `/?tags=3,7`: focus Chencho Corleone's `Filter by reggaeton` and press Enter. Eladio Carrion's `Filter by trap` has focus, because it is the same card the scroll anchor uses. - The heading carries `tabindex="-1"` in the normal list state too, and no test that queries it by role breaks. + - Hook, five-card harness, keyboard capture (`chip.focus()`, then `fireEvent.click(chip)`, `detail` 0): the chip's card leaves and A1 and E5 survive. onFocusLost is called once with E5's element. When E5 drifted, scrollBy ran before onFocusLost (`mock.invocationCallOrder`). + - Hook, keyboard capture: no card survives, so onFocusLost is called once with null and scrollBy is not called. + - Hook, keyboard capture: the chip's card stays, so onFocusLost is not called. + - Hook, mouse (keyboard-only scope): `chip.focus()`, then `fireEvent.click(chip, { detail: 1 })`, and the card leaves. onFocusLost is not called, even though the chip held focus. + - Hook: the chip never held focus at capture (`fireEvent.click(chip)` with no prior focus, a programmatic click with `detail` 0), so onFocusLost is not called. + - Hook: the chip held focus at a keyboard capture, but focus moved to another element before the key change, so onFocusLost is not called. -Per user decision 2026-10-12 (G-25-12). The "no card survives" target is Claude's discretion, recorded in the objective. +Per user decision 2026-10-12 (G-25-12), keyboard toggles only (user decision 2026-10-12, G-25-12 scope). The "no card survives" +target is Claude's discretion, recorded in the objective. RED first. Add the two route tests to the G-25-12 describe and confirm that the no-survivor case fails (focus on BODY). The run case -may already pass after Task 1. It pins G-25-11 and G-25-12 together. +may already pass after Task 1. It pins G-25-11 and G-25-12 together. Then add the six hook cases from the behavior block. They pin +the gates Task 1 built and may pass on first run. If one fails, fix the gate in `useScrollAnchor.ts` as part of this task's commit. + +Hook tests, in `useScrollAnchor.test.tsx`: +- Extend 25-19's five-card harness so `useScrollAnchor(key, onFocusLost)` receives a `vi.fn()`. +- The chip's own `onClick` calls `capture(e.currentTarget, e.detail === 0)`, applies a survivor set, and bumps the key in one + handler, the way TagChips and the route do. +- Keep separate capture and leave controls for the focus-moved-elsewhere case. The capture control also passes + `e.detail === 0`, so a default `fireEvent.click` on it is a keyboard capture. In that case, call `chip.focus()`, then + `fireEvent.click` the capture control, focus another button, and `fireEvent.click` the leave control. +- Import `fireEvent` from @testing-library/react. Leave every existing test in the file untouched. Then, in `watchlist.tsx`: - Give the `watchlist-artists-heading` h2 a `useRef` and `tabIndex={-1}`. It already has `outline-none`, and it stays out of the Tab order. -- Make `focusAfterCardLeaves` fall back to that heading when the fallback is null or holds no chip label. +- Make `focusAfterCardLeaves` fall back to that heading when the fallback is null or holds no chip label. It runs only through + onFocusLost, so the heading fallback inherits the keyboard-only gate. - Focus with `{ preventScroll: true }`. Keep comments to one short line naming G-25-12, if any. Before staging, run `corepack pnpm --dir web exec prettier --write @@ -245,15 +305,16 @@ Keep comments to one short line naming G-25-12, if any. Before staging, run `cor corepack pnpm --dir web exec vitest run app/routes/watchlist.test.tsx app/lib/useScrollAnchor.test.tsx --coverage.enabled=false corepack pnpm --dir web run typecheck && grep -qF 'tabIndex={-1}' web/app/routes/watchlist.tsx && corepack pnpm --dir web exec prettier --check "**/*.{ts,tsx}" && corepack pnpm --dir web test - non-zero exit: the no-survivor route case leaves focus on BODY; the run case focuses anything other than Eladio Carrion's `Filter by trap`; an "error TS" line; the heading lacks `tabIndex={-1}`; prettier listing files under "Code style issues"; any "FAIL" line; or a line beginning "ERROR: Coverage for" + non-zero exit: the no-survivor route case leaves focus on BODY; the run case focuses anything other than Eladio Carrion's `Filter by trap`; a hook case calls onFocusLost for a mouse capture, an unfocused chip, or a focus moved elsewhere, or fails to report E5 or null for a keyboard capture; an "error TS" line; the heading lacks `tabIndex={-1}`; prettier listing files under "Code style issues"; any "FAIL" line; or a line beginning "ERROR: Coverage for" - The no-survivor route case shows `No artists match these filters`, and `screen.getByRole("heading", { name: "Your artists" })` has focus and `tabindex="-1"`. - The five-card keyboard case leaves focus on Eladio Carrion's `Filter by trap`. - - Every pre-existing watchlist route test passes unedited. + - The six hook onFocusLost cases pass: keyboard survivor reported after scrollBy, keyboard null, chip stays, mouse click (`detail` 1) not reported, never focused, and focus moved elsewhere. + - Every pre-existing watchlist route test and hook test passes unedited. - Before the commit, `corepack pnpm --dir web exec prettier --check "**/*.{ts,tsx}"` and the full `corepack pnpm --dir web test` both exit 0. - A chip toggle that leaves no card lands focus on the list heading, so focus never falls to . Committed. + A keyboard chip toggle that leaves no card lands focus on the list heading, so focus never falls to , and the hook's keyboard-only gates are pinned by unit tests. Committed. @@ -267,20 +328,20 @@ Make one scoped `Edit`, per user decision 2026-10-12 (G-25-12), and never a whol after the row `| Card chip label | Toggled, card still visible | stays on that label button |`, leaving that row unchanged. The new row is exactly: -``| Card chip label | Toggled, card leaves | a chip label on the fallback anchor card, the same surviving card the scroll anchor uses, preferring a pressed one and focused with `preventScroll`; the `Your artists` heading (`tabIndex={-1}`) when no card survives. Never ``. Applies only when the label held focus when toggled (G-25-12, user decision 2026-10-12) |`` +``| Card chip label | Toggled, card leaves | Keyboard toggle (Enter or Space on the focused label): a chip label on the fallback anchor card, the same surviving card the scroll anchor uses, preferring a pressed one and focused with `preventScroll`; the `Your artists` heading (`tabIndex={-1}`) when no card survives; never ``. Mouse or touch click: focus is left alone (G-25-12, user decision 2026-10-12) |`` (The outer double backticks only delimit the row here. The row itself starts with `|` and ends with `|`.) Change nothing else in the UI-SPEC, and leave 25-CONTEXT.md, 25-UAT.md, and 25-VERIFICATION.md alone. Commit the doc alone with hooks enabled and no AI attribution, for example `docs(25-20): add the 'Toggled, card leaves' focus row to the UI-SPEC`. - f=.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md; [ "$(grep -c '^| Card chip label | Toggled, card leaves |' "$f")" -eq 1 ] && grep '^| Card chip label | Toggled, card leaves |' "$f" | grep -qF 'G-25-12, user decision 2026-10-12' && grep -A1 '^| Card chip label | Toggled, card still visible | stays on that label button |$' "$f" | tail -1 | grep -q '^| Card chip label | Toggled, card leaves |' && grep -qF 'widened to the nearest surviving card by G-25-11, user decision 2026-10-12' "$f" && d=$(git diff -U0 9d3413f -- "$f") && removed=$(printf '%s\n' "$d" | grep -E '^-' | grep -vE '^--- (a/|/dev/null)' | wc -l) && added=$(printf '%s\n' "$d" | grep -E '^\+' | grep -vE '^\+\+\+ (b/|/dev/null)' | wc -l) && echo "removed=$removed added=$added" && test "$removed" -eq 1 && test "$added" -eq 2 && test -z "$(git diff --name-only 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md .planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md)" - non-zero exit: the new row is missing, duplicated, or untagged; it doesn't sit directly after the unchanged "Toggled, card still visible" row; 25-19's D-12 line is gone; the cumulative UI-SPEC diff against `9d3413f` is not exactly one removed and two added lines (25-19's one-line edit plus this row); or CONTEXT, UAT, or VERIFICATION changed + f=.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md; [ "$(grep -c '^| Card chip label | Toggled, card leaves |' "$f")" -eq 1 ] && grep '^| Card chip label | Toggled, card leaves |' "$f" | grep -qF 'G-25-12, user decision 2026-10-12' && grep '^| Card chip label | Toggled, card leaves |' "$f" | grep -qF 'Keyboard toggle (Enter or Space on the focused label)' && grep '^| Card chip label | Toggled, card leaves |' "$f" | grep -qF 'Mouse or touch click: focus is left alone' && grep -A1 '^| Card chip label | Toggled, card still visible | stays on that label button |$' "$f" | tail -1 | grep -q '^| Card chip label | Toggled, card leaves |' && grep -qF 'widened to the nearest surviving card by G-25-11, user decision 2026-10-12' "$f" && d=$(git diff -U0 9d3413f -- "$f") && removed=$(printf '%s\n' "$d" | grep -E '^-' | grep -vE '^--- (a/|/dev/null)' | wc -l) && added=$(printf '%s\n' "$d" | grep -E '^\+' | grep -vE '^\+\+\+ (b/|/dev/null)' | wc -l) && echo "removed=$removed added=$added" && test "$removed" -eq 1 && test "$added" -eq 2 && test -z "$(git diff --name-only 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md .planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md)" && test -z "$(git diff --name-only 1cb2a7b -- .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md)" + non-zero exit: the new row is missing, duplicated, or untagged; it lacks the keyboard-only scope ("Keyboard toggle (Enter or Space on the focused label)" and "Mouse or touch click: focus is left alone"); it doesn't sit directly after the unchanged "Toggled, card still visible" row; 25-19's D-12 line is gone; the cumulative UI-SPEC diff against `9d3413f` is not exactly one removed and two added lines (25-19's one-line edit plus this row); CONTEXT or VERIFICATION changed since `9d3413f`; or UAT changed since `1cb2a7b` (the commit that recorded the 2026-10-12 race waiver) - - The focus table has exactly one "Card chip label | Toggled, card leaves" row, tagged `G-25-12, user decision 2026-10-12`, directly after the unchanged "Toggled, card still visible" row. + - The focus table has exactly one "Card chip label | Toggled, card leaves" row, tagged `G-25-12, user decision 2026-10-12`, directly after the unchanged "Toggled, card still visible" row. It scopes the focus move to keyboard toggles and says a mouse or touch click leaves focus alone. - `git diff -U0 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md` shows exactly one removed and two added content lines. - - 25-CONTEXT.md, 25-UAT.md, and 25-VERIFICATION.md are unchanged since `9d3413f`. + - 25-CONTEXT.md and 25-VERIFICATION.md are unchanged since `9d3413f`, and 25-UAT.md is unchanged since `1cb2a7b`. The UI design contract defines where focus goes when a toggled card leaves. Committed. @@ -299,25 +360,27 @@ attribution, for example `docs(25-20): add the 'Toggled, card leaves' focus row | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-25-53 | Tampering | `focusAfterCardLeaves` querySelector | low | mitigate | The selectors are the constant literals `[data-chip-label][aria-pressed="true"]` and `[data-chip-label]`. A tag name is never interpolated, so a name full of CSS metacharacters can't break or redirect the query. Task 1's verify greps for the constant selector | -| T-25-54 | Denial of service (usability) | Focus moved against the user's intent | low | mitigate | The hook moves focus only when the captured label held focus, is disconnected, and focus fell to the document. Hook tests pin the never-focused and moved-elsewhere cases, and route tests 2173 and 2392 pin the card-stays case | +| T-25-54 | Denial of service (usability) | Focus moved against the user's intent | low | mitigate | The hook moves focus only for a keyboard toggle (click `detail === 0`) whose captured label held focus, is disconnected, and focus fell to the document. Hook tests pin the mouse, never-focused, and moved-elsewhere cases, a route test pins the mouse case, and the route tests `filters to the tag's carriers, mirrors it in the toolbar, and keeps focus on the label` and `keeps a sticky card's chip anchored and focused when the chip filters (WR-01)` pin the card-stays case | | T-25-55 | Tampering / scope creep | URL helper, vendored ui/, package files | low | mitigate | Task 1's verify asserts that `useUrlParams.ts`, `components/ui`, `web/package.json`, and `web/pnpm-lock.yaml` are unchanged since `9d3413f` | | T-25-SC | Tampering | npm installs | low | mitigate | No package is added. `useEffectEvent` ships in the already-locked React 19.2.6 | - `corepack pnpm --dir web exec vitest run app/lib/useScrollAnchor.test.tsx app/routes/watchlist.test.tsx app/components/watchlist/TagChips.test.tsx --coverage.enabled=false` passes. -- Typecheck passes. `data-chip-label`, `useEffectEvent`, `preventScroll: true`, the pressed-label selector, and `tabIndex={-1}` are present. +- Typecheck passes. `data-chip-label`, the `detail === 0` keyboard check, `useEffectEvent`, `preventScroll: true`, the pressed-label selector, and `tabIndex={-1}` are present. - Before each code commit, `corepack pnpm --dir web exec prettier --check "**/*.{ts,tsx}" && corepack pnpm --dir web test` exits 0 (CLAUDE.md DoD item 6). - The cumulative UI-SPEC diff against `9d3413f` is one removed and two added lines, carrying the G-25-11 and G-25-12 tags. A keyboard user who toggles a chip label that removes its card lands on the surviving fallback card's pressed chip label, or on the -`Your artists` heading when nothing survives, and never on (G-25-12). Focus behavior is unchanged when the card stays, and the -UI-SPEC focus table states the new row. +`Your artists` heading when nothing survives, and never on (G-25-12). A mouse or touch click that removes the card moves no +focus (keyboard-only scope, user decision 2026-10-12). Focus behavior is unchanged when the card stays, and the UI-SPEC focus table +states the new row with its keyboard-only scope. Create `.planning/phases/25-find-filter-watchlist-and-history/25-20-SUMMARY.md` when done. Record the RED output (focus on BODY), the -`useEffectEvent` choice, and that the embedded SPA is rebuilt in plan 25-21. +`useEffectEvent` choice, the `detail === 0` keyboard-only mechanism (user decision 2026-10-12), and that the embedded SPA is rebuilt +in plan 25-21. diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-21-PLAN.md b/.planning/phases/25-find-filter-watchlist-and-history/25-21-PLAN.md index 2fb7b36..473b5f5 100644 --- a/.planning/phases/25-find-filter-watchlist-and-history/25-21-PLAN.md +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-21-PLAN.md @@ -29,7 +29,8 @@ must_haves: - "25-UI-SPEC's page-order SearchResultsColumns note records the accessible description, tagged `G-25-13, user decision 2026-10-12`." - "The embedded SPA in `internal/webassets/build/client` is rebuilt from the gap-closure `web/`, so a Go-only build serves all three fixes. Each fix's marker occurs more often under `internal/webassets/build/client/assets` than at the planning head `9d3413f`: `parentElement` (25-19, baseline 9), `data-chip-label` (25-20, baseline 0), and `aria-describedby` (25-21, baseline 3)." - "Every Definition of Done gate passes: go vet, golangci-lint, the integration suite with a coverage profile, the 80% backend coverage gate, sqlc-check (with its sqlc-version-check guard first), typecheck, the prettier check, and the web suite at 70% or more on all four axes." - - "Since `9d3413f`, the only changed web/app files are the seven the gap plans own, and nothing changed in any `*.go` file, queries/, internal/db/, internal/events/, go.mod, go.sum, web/package.json, web/pnpm-lock.yaml, web/app/components/ui/, web/app/lib/useUrlParams.ts, web/app/root.tsx, 25-CONTEXT.md, 25-UAT.md, or 25-VERIFICATION.md." + - "Since `9d3413f`, the only changed web/app files are the eight the gap plans own, and nothing changed in any `*.go` file, queries/, internal/db/, internal/events/, go.mod, go.sum, web/package.json, web/pnpm-lock.yaml, web/app/components/ui/, web/app/lib/useUrlParams.ts, web/app/root.tsx, 25-CONTEXT.md, or 25-VERIFICATION.md. 25-UAT.md is unchanged since `1cb2a7b`, the commit that recorded the 2026-10-12 race waiver." + - "Local integration runs skip `-race` under the user's accepted exception (user decision 2026-10-12, 25-UAT.md ## Execution Waivers, 'CI-only race extended' for 25-19..25-21). CI's Linux test job stays the race gate." artifacts: - path: web/app/components/watchlist/SearchResultsColumns.tsx provides: "Exported DEEZER_ADD_HINT, a per-row useId hint span (sr-only), and aria-describedby on the disabled Deezer button" @@ -84,9 +85,10 @@ Tooling on this box (25-14 and 25-18 precedent): underlying commands, including each target's prerequisites. - `go: no such tool "covdata"` for `internal/testutil` is a known environment quirk (25-10 SUMMARY deviation 2). -Race (assumption, surfaced to the user): `go test -race` can't run here, because cgo is disabled. For 25-14 on 2026-10-11 the user -accepted CI-only race, and 25-18 carried it forward. This plan changes no Go source (the blast-radius check pins every `*.go` file), -so the same limitation applies, and CI's Linux job stays the race gate. If the user objects, stop before committing and ask. +Race (accepted exception, user decision 2026-10-12): `go test -race` can't run here, because cgo is disabled. The user accepted +CI-only race for gap-closure plans 25-19..25-21 on 2026-10-12, recorded in 25-UAT.md ## Execution Waivers ("CI-only race extended"). +That waiver is the source of this plan's exception, not the 25-14 carry-forward. This plan changes no Go source (the blast-radius +check pins every `*.go` file), and CI's Linux job stays the race gate. Output: the described Deezer button, the amended UI-SPEC line, the refreshed `internal/webassets/build/client`, and a green gate run. @@ -198,13 +200,13 @@ Keep the line's leading whitespace and the rest of the text unchanged, with no w `docs(25-21): record the Deezer hint's accessible description in the UI-SPEC`. - f=.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md; [ "$(grep -c 'SearchBox "Search artists" + SearchResultsColumns' "$f")" -eq 1 ] && grep -F 'SearchBox "Search artists" + SearchResultsColumns' "$f" | grep -qF 'aria-describedby on an sr-only span with a per-row useId id, with no visual change (G-25-13, user decision 2026-10-12))' && grep -qF 'widened to the nearest surviving card by G-25-11, user decision 2026-10-12' "$f" && grep -q '^| Card chip label | Toggled, card leaves |.*G-25-12, user decision 2026-10-12' "$f" && d=$(git diff -U0 9d3413f -- "$f") && removed=$(printf '%s\n' "$d" | grep -E '^-' | grep -vE '^--- (a/|/dev/null)' | wc -l) && added=$(printf '%s\n' "$d" | grep -E '^\+' | grep -vE '^\+\+\+ (b/|/dev/null)' | wc -l) && echo "removed=$removed added=$added" && test "$removed" -eq 2 && test "$added" -eq 3 && test -z "$(git diff --name-only 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md .planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md)" - non-zero exit: the page-order line is missing, duplicated, or lacks the G-25-13 text; 25-19's D-12 line or 25-20's focus row is gone; the cumulative UI-SPEC diff against `9d3413f` is not exactly two removed and three added lines; or CONTEXT, UAT, or VERIFICATION changed + f=.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md; [ "$(grep -c 'SearchBox "Search artists" + SearchResultsColumns' "$f")" -eq 1 ] && grep -F 'SearchBox "Search artists" + SearchResultsColumns' "$f" | grep -qF 'aria-describedby on an sr-only span with a per-row useId id, with no visual change (G-25-13, user decision 2026-10-12))' && grep -qF 'widened to the nearest surviving card by G-25-11, user decision 2026-10-12' "$f" && grep -q '^| Card chip label | Toggled, card leaves |.*G-25-12, user decision 2026-10-12' "$f" && d=$(git diff -U0 9d3413f -- "$f") && removed=$(printf '%s\n' "$d" | grep -E '^-' | grep -vE '^--- (a/|/dev/null)' | wc -l) && added=$(printf '%s\n' "$d" | grep -E '^\+' | grep -vE '^\+\+\+ (b/|/dev/null)' | wc -l) && echo "removed=$removed added=$added" && test "$removed" -eq 2 && test "$added" -eq 3 && test -z "$(git diff --name-only 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md .planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md)" && test -z "$(git diff --name-only 1cb2a7b -- .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md)" + non-zero exit: the page-order line is missing, duplicated, or lacks the G-25-13 text; 25-19's D-12 line or 25-20's focus row is gone; the cumulative UI-SPEC diff against `9d3413f` is not exactly two removed and three added lines; CONTEXT or VERIFICATION changed since `9d3413f`; or UAT changed since `1cb2a7b` (the commit that recorded the 2026-10-12 race waiver) - The single page-order SearchResultsColumns line ends with the G-25-13 accessible-description clause. - `git diff -U0 9d3413f -- .planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md` shows exactly two removed and three added content lines, from 25-19, 25-20, and this task. - - 25-CONTEXT.md, 25-UAT.md, and 25-VERIFICATION.md are unchanged since `9d3413f`. + - 25-CONTEXT.md and 25-VERIFICATION.md are unchanged since `9d3413f`, and 25-UAT.md is unchanged since `1cb2a7b`. The UI design contract states the Deezer hint's accessible description. Committed. @@ -216,7 +218,8 @@ Keep the line's leading whitespace and the rest of the text unchanged, with no w - `.claude/CLAUDE.md` "Definition of Done" (gate order; never `--no-verify`). - `Makefile` `web` target (line 139), `test-integration` (`COVER_PKGS`), `coverage-gate`, and `sqlc-check` recipes. - - `.planning/phases/25-find-filter-watchlist-and-history/25-18-PLAN.md` Tasks 1 and 3 (the same rebuild, marker method, and gate chain) and `25-18-SUMMARY.md` "Accepted exceptions". + - `.planning/phases/25-find-filter-watchlist-and-history/25-18-PLAN.md` Tasks 1 and 3 (the same rebuild, marker method, and gate chain) and `25-18-SUMMARY.md` "Accepted exceptions" (section format only). + - `.planning/phases/25-find-filter-watchlist-and-history/25-UAT.md` ## Execution Waivers (the 2026-10-12 "CI-only race extended" line is the source of this plan's race exception). Rebuild by running the `make web` recipe directly: @@ -255,8 +258,9 @@ G-25-11..13 fixes`. The deleted files are superseded hashed assets, which is int In the SUMMARY, record: - The `before -> after` marker counts. -- An `Accepted exceptions` heading carrying the line `Accepted exception: CI-only race (carried from user decision 2026-10-11, - 25-14)`. The reason: cgo is disabled locally, no Go source changed in 25-19 to 25-21, and CI's Linux test job runs `-race`. +- An `Accepted exceptions` heading carrying the line `Accepted exception: CI-only race (user decision 2026-10-12, 25-UAT.md)`. The + source is the 25-UAT.md ## Execution Waivers line "CI-only race extended", which covers 25-19..25-21. The reason: cgo is disabled + locally, no Go source changed in 25-19 to 25-21, and CI's Linux test job runs `-race`. corepack pnpm --dir web install --frozen-lockfile && corepack pnpm --dir web run build && rm -rf internal/webassets/build/client && mkdir -p internal/webassets/build && cp -r web/build/client internal/webassets/build/client && out=$(git status --porcelain internal/webassets/build/client) && test -n "$out" @@ -267,11 +271,11 @@ In the SUMMARY, record: any vet or lint diagnostic; any `FAIL` or `--- FAIL` line in the test log other than internal/testutil's known `go: no such tool "covdata"` failure; coverage below 80; `SQLC_VERSION` missing from the Makefile or a `sqlc version mismatch` (the guard runs before generate); or a diff under internal/db/sqlc/ corepack pnpm --dir web run typecheck && corepack pnpm --dir web exec prettier --check "**/*.{ts,tsx}" && corepack pnpm --dir web test non-zero exit, any "error TS" line, prettier listing files under "Code style issues", any "FAIL" line, or a line beginning "ERROR: Coverage for" - out=$(git diff --name-only 9d3413f -- '*.go' queries internal/db internal/events go.mod go.sum web/package.json web/pnpm-lock.yaml web/app/components/ui web/app/lib/useUrlParams.ts web/app/root.tsx .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md .planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md) && test -z "$out" && changed=$(git diff --name-only 9d3413f -- web/app) && extra=$(printf '%s\n' "$changed" | grep -vxE 'web/app/(lib/useScrollAnchor\.ts|lib/useScrollAnchor\.test\.tsx|routes/watchlist\.tsx|routes/watchlist\.test\.tsx|components/watchlist/TagChips\.tsx|components/watchlist/SearchResultsColumns\.tsx|components/watchlist/SearchResultsColumns\.test\.tsx)'; true) && echo "unexpected web/app changes: ${extra:-none}" && test -z "$extra" - non-zero exit: a Go source file, the SQL, generated code, the events service, a migration, a dependency file, the vendored ui/ components, useUrlParams, root.tsx, 25-CONTEXT.md, 25-UAT.md, or 25-VERIFICATION.md changed since `9d3413f`; or a web/app file outside the seven gap-plan files changed (the printed paths name it) + out=$(git diff --name-only 9d3413f -- '*.go' queries internal/db internal/events go.mod go.sum web/package.json web/pnpm-lock.yaml web/app/components/ui web/app/lib/useUrlParams.ts web/app/root.tsx .planning/phases/25-find-filter-watchlist-and-history/25-CONTEXT.md .planning/phases/25-find-filter-watchlist-and-history/25-VERIFICATION.md) && test -z "$out" && test -z "$(git diff --name-only 1cb2a7b -- .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md)" && grep -qF 'CI-only race extended (user decision 2026-10-12)' .planning/phases/25-find-filter-watchlist-and-history/25-UAT.md && changed=$(git diff --name-only 9d3413f -- web/app) && extra=$(printf '%s\n' "$changed" | grep -vxE 'web/app/(lib/useScrollAnchor\.ts|lib/useScrollAnchor\.test\.tsx|routes/watchlist\.tsx|routes/watchlist\.test\.tsx|components/watchlist/TagChips\.tsx|components/watchlist/TagChips\.test\.tsx|components/watchlist/SearchResultsColumns\.tsx|components/watchlist/SearchResultsColumns\.test\.tsx)'; true) && echo "unexpected web/app changes: ${extra:-none}" && test -z "$extra" + non-zero exit: a Go source file, the SQL, generated code, the events service, a migration, a dependency file, the vendored ui/ components, useUrlParams, root.tsx, 25-CONTEXT.md, or 25-VERIFICATION.md changed since `9d3413f`; 25-UAT.md changed since `1cb2a7b`, or its 2026-10-12 "CI-only race extended" waiver line is missing; or a web/app file outside the eight gap-plan files changed (the printed paths name it) - Run the built binary (or `docker compose up` with the rebuilt image) against the dev DB with 50+ artists. On the Watchlist, scrolled far down: (1) at `?tags=A,B`, un-press a chip on a card whose next and previous cards also carry only that tag (G-25-11); (2) repeat (1) with the keyboard (Tab to the chip label, press Enter), then repeat it on a filter where no card survives (G-25-12); (3) search `Bad Bunny` and move a screen reader (NVDA or VoiceOver) onto a Deezer row's disabled button, then hover the truncated pill (G-25-13). - (1) The nearest surviving card stays where it was on screen, clamped only when the filtered page is shorter than the window. (2) The focus ring lands on that surviving card's pressed chip label without moving the page. When nothing survives, focus lands on the `Your artists` heading, never on the top of the page. (3) The screen reader announces `Search MusicBrainz to add, button, unavailable` followed by the hint, with no visible change to the row, and hovering still shows the native tooltip. + Run the built binary (or `docker compose up` with the rebuilt image) against the dev DB with 50+ artists. On the Watchlist, scrolled far down: (1) at `?tags=A,B`, un-press a chip on a card whose next and previous cards also carry only that tag (G-25-11); (2) repeat (1) with the keyboard (Tab to the chip label, press Enter), then repeat it on a filter where no card survives, and confirm that the mouse click in (1) moved no focus (G-25-12, keyboard only); (3) search `Bad Bunny` and move a screen reader (NVDA or VoiceOver) onto a Deezer row's disabled button, then hover the truncated pill (G-25-13). + (1) The nearest surviving card stays where it was on screen, clamped only when the filtered page is shorter than the window. (2) The focus ring lands on that surviving card's pressed chip label without moving the page. When nothing survives, focus lands on the `Your artists` heading, never on the top of the page. After the mouse click in (1), no focus ring appears on another card or on the heading. (3) The screen reader announces `Search MusicBrainz to add, button, unavailable` followed by the hint, with no visible change to the row, and hovering still shows the native tooltip. Real-browser layout, focus rings, and assistive-tech output (human_verify_mode: end-of-phase). jsdom stubs layout and has no screen reader. These are the /gsd-verify-work re-test items for G-25-11, G-25-12, and G-25-13. @@ -280,8 +284,8 @@ In the SUMMARY, record: - Each of `parentElement`, `data-chip-label`, and `aria-describedby` occurs more often under `internal/webassets/build/client/assets` than in the `9d3413f` tree. - Backend coverage prints 80 or more, and the web suite reports 70% or more on statements, branches, functions, and lines. - The sqlc version guard passes before `sqlc generate` runs, and the sqlc diff is clean. - - Both blast-radius checks against `9d3413f` are empty. - - The SUMMARY records `Accepted exception: CI-only race (carried from user decision 2026-10-11, 25-14)` with its reason, plus the marker counts. + - Both blast-radius checks against `9d3413f` are empty, and 25-UAT.md is unchanged since `1cb2a7b` and still carries the "CI-only race extended" waiver. + - The SUMMARY records `Accepted exception: CI-only race (user decision 2026-10-12, 25-UAT.md)` with its reason, plus the marker counts. The Go binary serves G-25-11, G-25-12, and G-25-13, every Definition of Done gate is green, the gap closure touched only its intended surface, and the re-test items are ready for /gsd-verify-work. Committed. @@ -304,7 +308,7 @@ In the SUMMARY, record: | T-25-56 | Tampering (XSS) | Deezer hint span | low | mitigate | The span's only child is the static `DEEZER_ADD_HINT` constant, rendered as JSX text. No catalogue string feeds the description, and the id comes from React's `useId`, not from a Deezer value | | T-25-SC | Tampering | npm installs during the rebuild | high | mitigate | No package is added. The rebuild uses `--frozen-lockfile`, and the blast-radius diff asserts that `web/package.json` and `web/pnpm-lock.yaml` are unchanged since `9d3413f` | | T-25-57 | Tampering | Stale embedded bundle | medium | mitigate | The rebuild is verified on its own. The bundle must change, and each fix's marker (`parentElement`, `data-chip-label`, `aria-describedby`) must occur more often than in the `9d3413f` tree | -| T-25-58 | Elevation / scope creep | Go source, SQL, vendored components, decision and verification docs | low | mitigate | The blast-radius diff over `*.go`, queries/, internal/db, internal/events, ui/, useUrlParams.ts, root.tsx, 25-CONTEXT.md, 25-UAT.md, and 25-VERIFICATION.md must be empty, and web/app changes must stay within the seven gap-plan files | +| T-25-58 | Elevation / scope creep | Go source, SQL, vendored components, decision and verification docs | low | mitigate | The blast-radius diff against `9d3413f` over `*.go`, queries/, internal/db, internal/events, ui/, useUrlParams.ts, root.tsx, 25-CONTEXT.md, and 25-VERIFICATION.md must be empty, 25-UAT.md must be unchanged since `1cb2a7b`, and web/app changes must stay within the eight gap-plan files | | T-25-59 | Repudiation | Spec drift | low | mitigate | Task 2's verify fails unless the cumulative UI-SPEC diff against `9d3413f` is exactly two removed and three added lines carrying the G-25-11, G-25-12, and G-25-13 tags | @@ -313,7 +317,7 @@ In the SUMMARY, record: - The UI-SPEC carries the three G-25-11/12/13 lines, with a cumulative diff of two removed and three added lines against `9d3413f`. - The bundle carries all three markers above their `9d3413f` counts, and `go build ./...` compiles the embed. - The Go chain, the web chain, and both blast-radius checks exit 0, apart from the documented covdata line. -- After the SUMMARY is written: `grep -qF 'Accepted exception: CI-only race (carried from user decision 2026-10-11, 25-14)' .planning/phases/25-find-filter-watchlist-and-history/25-21-SUMMARY.md` exits 0. +- After the SUMMARY is written: `grep -qF 'Accepted exception: CI-only race (user decision 2026-10-12, 25-UAT.md)' .planning/phases/25-find-filter-watchlist-and-history/25-21-SUMMARY.md` exits 0. @@ -324,6 +328,6 @@ three gaps are ready for re-verification. Create `.planning/phases/25-find-filter-watchlist-and-history/25-21-SUMMARY.md` when done. Its `Accepted exceptions` section must carry -the line `Accepted exception: CI-only race (carried from user decision 2026-10-11, 25-14)` with the reason. Also list the -`before -> after` marker counts. +the line `Accepted exception: CI-only race (user decision 2026-10-12, 25-UAT.md)` with the reason, citing the 25-UAT.md ## Execution +Waivers line "CI-only race extended" as its source. Also list the `before -> after` marker counts. From a4f8b0913dac4dee5a36ff36aa12b8e8fc6380e1 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 17:08:34 -0500 Subject: [PATCH 121/142] fix(25-19): anchor on the nearest surviving card when a run of cards leaves - anchorCandidates lists every sibling card nearest-first, next before previous - hook and route tests cover a run of adjacent leaving cards (G-25-11) --- web/app/lib/useScrollAnchor.test.tsx | 128 +++++++++++++++++++++++++++ web/app/lib/useScrollAnchor.ts | 15 +++- web/app/routes/watchlist.test.tsx | 42 +++++++++ 3 files changed, 182 insertions(+), 3 deletions(-) diff --git a/web/app/lib/useScrollAnchor.test.tsx b/web/app/lib/useScrollAnchor.test.tsx index 28af260..4df3e91 100644 --- a/web/app/lib/useScrollAnchor.test.tsx +++ b/web/app/lib/useScrollAnchor.test.tsx @@ -238,7 +238,135 @@ describe("useScrollAnchor fallback candidates (WR-02)", () => { }) }) +const RUN = ["A1", "B2", "C3", "D4", "E5"] + +function RunHarness() { + const [key, setKey] = useState("a") + const [kept, setKept] = useState(RUN) + const chip = useRef(null) + const capture = useScrollAnchor(key) + const keep = (ids: string[]) => () => { + setKept(ids) + setKey((k) => k + "x") + } + return ( + <> +
        + {RUN.map( + (id) => + kept.includes(id) && ( +
      • + {id === "C3" && ( + + )} +
      • + ) + )} +
      + + + + + ) +} + +describe("useScrollAnchor run of leaving cards (G-25-11)", () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + function setup(tops: Record) { + const scrollBy = vi.spyOn(window, "scrollBy").mockImplementation(() => {}) + render() + for (const id of [...RUN, "chip"]) { + vi.spyOn( + screen.getByTestId(id), + "getBoundingClientRect" + ).mockImplementation( + () => + ({ + top: tops[id], + bottom: tops[id] + 10, + left: 0, + right: 10, + width: 10, + height: 10, + x: 0, + y: tops[id], + toJSON: () => ({}), + }) as DOMRect + ) + } + const click = (name: string) => + userEvent.click(screen.getByRole("button", { name })) + return { scrollBy, click } + } + + const tops = () => ({ A1: 0, B2: 200, C3: 400, D4: 600, E5: 800, chip: 410 }) + + it("corrects by the next-side survivor when it ties on distance", async () => { + const t = tops() + const { scrollBy, click } = setup(t) + + await click("capture") + t.E5 = 650 + t.A1 = -50 + await click("keep ends") + + expect(scrollBy).toHaveBeenCalledTimes(1) + expect(scrollBy).toHaveBeenCalledWith(0, -150) + }) + + it("falls back to the previous side when the next side runs out", async () => { + const t = tops() + const { scrollBy, click } = setup(t) + + await click("capture") + t.A1 = -30 + await click("keep first") + + expect(scrollBy).toHaveBeenCalledTimes(1) + expect(scrollBy).toHaveBeenCalledWith(0, -30) + }) +}) + describe("anchorCandidates", () => { + it("lists every sibling card nearest-first, next before previous", () => { + render( +
        + {RUN.map((id) => ( +
      • + {id === "C3" &&
      • + ))} +
      + ) + + expect(anchorCandidates(screen.getByTestId("chip"))).toEqual( + ["chip", "D4", "B2", "E5", "A1"].map((id) => screen.getByTestId(id)) + ) + }) + + it("continues on the next side when the previous side runs out", () => { + render( +
        + {RUN.map((id) => ( +
      • + {id === "B2" &&
      • + ))} +
      + ) + + expect(anchorCandidates(screen.getByTestId("chip"))).toEqual( + ["chip", "C3", "A1", "D4", "E5"].map((id) => screen.getByTestId(id)) + ) + }) + it("lists the chip, then the next and previous cards", () => { render(
        diff --git a/web/app/lib/useScrollAnchor.ts b/web/app/lib/useScrollAnchor.ts index 21747eb..7992570 100644 --- a/web/app/lib/useScrollAnchor.ts +++ b/web/app/lib/useScrollAnchor.ts @@ -1,12 +1,21 @@ import { useCallback, useLayoutEffect, useRef } from "react" -// Chip first, then the card's next and previous siblings: a fallback when the -// clicked card itself leaves the filtered list (WR-02). +// Chip first, then every sibling card nearest-first (next before previous at +// each distance): fallbacks if the clicked card leaves the filtered list (G-25-11). export function anchorCandidates(el: HTMLElement): HTMLElement[] { const card = el.closest("li") - return [el, card?.nextElementSibling, card?.previousElementSibling].filter( + const parent = card?.parentElement + if (!card || !parent) return [el] + const cards = Array.from(parent.children).filter( (c): c is HTMLElement => c instanceof HTMLElement ) + const at = cards.indexOf(card) + const out = [el] + for (let d = 1; at + d < cards.length || at - d >= 0; d++) { + if (at + d < cards.length) out.push(cards[at + d]) + if (at - d >= 0) out.push(cards[at - d]) + } + return out } // G-25-4: a filter can collapse the page under the viewport; keep the clicked diff --git a/web/app/routes/watchlist.test.tsx b/web/app/routes/watchlist.test.tsx index b78dc0d..74dc7b7 100644 --- a/web/app/routes/watchlist.test.tsx +++ b/web/app/routes/watchlist.test.tsx @@ -2389,6 +2389,48 @@ describe("chip click keeps the clicked card on screen (G-25-4)", () => { expect(scrollBy).toHaveBeenCalledWith(0, -400) }) + it("corrects by the nearest survivor's drift when a run of cards leaves (G-25-11)", async () => { + const scrollBy = vi.spyOn(window, "scrollBy").mockImplementation(() => {}) + const trap = { id: 7, name: "trap" } + mockListWatchlist.mockResolvedValue([ + makeEntry({ id: 1, name: "Arcangel", tags: [trap] }), + makeEntry({ id: 2, name: "Bad Bunny", tags: [reggaeton] }), + makeEntry({ id: 3, name: "Chencho Corleone", tags: [reggaeton] }), + makeEntry({ id: 4, name: "Daddy Yankee", tags: [reggaeton] }), + makeEntry({ id: 5, name: "Eladio Carrion", tags: [trap] }), + ]) + renderRoute(WatchlistWithProbe, "/", "/?tags=3,7") + await screen.findByText("Eladio Carrion") + const eladio = screen.getByText("Eladio Carrion").closest("li")! + // Keyed on the list's child count so the reads differ however often measured. + vi.spyOn(eladio, "getBoundingClientRect").mockImplementation(() => { + const top = eladio.parentElement!.children.length === 5 ? 900 : 300 + return { + top, + bottom: top + 100, + left: 0, + right: 300, + width: 300, + height: 100, + x: 0, + y: top, + toJSON: () => ({}), + } + }) + + await userEvent.click( + within(screen.getByText("Chencho Corleone").closest("li")!).getByRole( + "button", + { name: "Filter by reggaeton" } + ) + ) + + expect(search()).toBe("?tags=7") + expect(shown()).toEqual(["Arcangel", "Eladio Carrion"]) + expect(scrollBy).toHaveBeenCalledTimes(1) + expect(scrollBy).toHaveBeenCalledWith(0, -600) + }) + it("keeps a sticky card's chip anchored and focused when the chip filters (WR-01)", async () => { const scrollBy = vi.spyOn(window, "scrollBy").mockImplementation(() => {}) const trap = { id: 7, name: "trap" } From 9c5199229bf832987ee4bde0aa36bc6e478e2f88 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 17:08:46 -0500 Subject: [PATCH 122/142] docs(25-19): record the widened fallback anchor in the UI-SPEC D-12 bullet --- .../phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md b/.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md index 7039a83..dae1015 100644 --- a/.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md @@ -396,7 +396,7 @@ announcement per change, and all six modes are visible at once. ### Router & sticky-state rules -- **Scroll reset and anchoring (D-12, G-25-4):** `web/app/root.tsx:37` mounts ``, and react-router 7.18.2 calls `window.scrollTo(0, 0)` on every navigation, push or replace, unless it passes `preventScrollReset: true`. Every filter, sort, and typing change is a navigation, so Watchlist and History write params only through one shared helper that always passes `{ preventScrollReset: true }`, plus `replace: true` where this spec says replace. That stops the router from resetting scroll, but a filter that shrinks the list can leave the page shorter than the old `scrollY`, and the browser then clamps it. So a chip-label click also anchors the clicked chip: `useScrollAnchor` records its `getBoundingClientRect().top` before the tags param is written, and a layout effect scrolls by the drift after the re-render. The clicked card keeps its on-screen position, and on a short page the browser clamps it. If the clicked card leaves the filtered list, the anchor falls back to the clicked card's next sibling card, then its previous one, whichever is still in the list; with neither left, no correction runs (WR-02, user decision 2026-10-11). +- **Scroll reset and anchoring (D-12, G-25-4):** `web/app/root.tsx:37` mounts ``, and react-router 7.18.2 calls `window.scrollTo(0, 0)` on every navigation, push or replace, unless it passes `preventScrollReset: true`. Every filter, sort, and typing change is a navigation, so Watchlist and History write params only through one shared helper that always passes `{ preventScrollReset: true }`, plus `replace: true` where this spec says replace. That stops the router from resetting scroll, but a filter that shrinks the list can leave the page shorter than the old `scrollY`, and the browser then clamps it. So a chip-label click also anchors the clicked chip: `useScrollAnchor` records its `getBoundingClientRect().top` before the tags param is written, and a layout effect scrolls by the drift after the re-render. The clicked card keeps its on-screen position, and on a short page the browser clamps it. If the clicked card leaves the filtered list, the anchor falls back to the nearest card still in the list, searching outward from the clicked card one card at a time and trying the next card before the previous one at each distance, so a run of adjacent leaving cards never collapses the page under the viewport; only when no card survives does no correction run (WR-02, user decision 2026-10-11; widened to the nearest surviving card by G-25-11, user decision 2026-10-12). - **Sticky set (G4):** keep an explicit `stickyIds` set. A card is visible when it matches the filters or `stickyIds.has(id)`, and an in-card edit adds its card's id. Only the G4 user actions clear the set (search text, sort, tag selection, a toggle, Clear filters, a chip-click filter, reload). An `entries` change never clears or recomputes it, because `watchlist.tsx` calls `refresh()` (a full `GET /watchlist`) after add-from-search, after an Undo restore, and after a failed remove. The set is keyed to the filter it was made under, so a filter change from a G4 action or from Back/Forward drops it in the same render as the URL change. The route's own tag rewrites (a Manage tags merge or delete of a selected tag, the first-load tag-id check, and URL canonicalization) carry it to the filter they land on (WR-01, WR-03, user decision 2026-10-11). - **Adds are sticky (G4):** an artist added from search (`handleAddSearchResult`) or restored by Undo joins `stickyIds` (id from the POST response), so an add under an active filter never succeeds invisibly. - **Tag-id validation runs once (D-13):** URL tag ids are checked against the payload-derived set only on the first successful payload load. After that the tag filter changes only by user action or the Manage tags `onDeleted`/`onMerged` callbacks; `refresh()` never mutates it. From 41c74871186f1f9796405229a4d362c334a11e25 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 17:09:08 -0500 Subject: [PATCH 123/142] docs(25-19): add plan summary --- .../25-19-SUMMARY.md | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 .planning/phases/25-find-filter-watchlist-and-history/25-19-SUMMARY.md diff --git a/.planning/phases/25-find-filter-watchlist-and-history/25-19-SUMMARY.md b/.planning/phases/25-find-filter-watchlist-and-history/25-19-SUMMARY.md new file mode 100644 index 0000000..319a7dd --- /dev/null +++ b/.planning/phases/25-find-filter-watchlist-and-history/25-19-SUMMARY.md @@ -0,0 +1,144 @@ +--- +phase: 25-find-filter-watchlist-and-history +plan: 19 +subsystem: ui +tags: [react, scroll-anchor, vitest, watchlist, gap-closure] + +requires: + - phase: 25-find-filter-watchlist-and-history + provides: "useScrollAnchor two-neighbour fallback (25-17, WR-02)" +provides: + - "anchorCandidates: chip, then every sibling card nearest-first (next before previous at each distance)" + - "Hook, ordering and route tests for a run of adjacent leaving cards" + - "25-UI-SPEC D-12 scroll bullet records the widened fallback rule" +affects: [25-20, 25-21] + +actuals: + tokens: 4600 + tasks: 2 + commits: 2 +plan_head_before: 5969c99c9414223a4f965d8f67367548873e78b5 + +tech-stack: + added: [] + patterns: + - "Scroll anchor measures all sibling cards at capture and corrects by the first still-connected one" + +key-files: + created: [] + modified: + - web/app/lib/useScrollAnchor.ts + - web/app/lib/useScrollAnchor.test.tsx + - web/app/routes/watchlist.test.tsx + - .planning/phases/25-find-filter-watchlist-and-history/25-UI-SPEC.md + +key-decisions: + - "G-25-11 (user decision 2026-10-12): fallback widens to the nearest surviving card, with no distance cap (a cap would bring back the G-25-4 collapse for a longer run)." + - "At each distance the next card is tried before the previous one, keeping the 25-17 next-then-previous order as the distance-1 case." + +requirements-completed: [WLVW-04] + +coverage: + - id: D1 + description: "A run of adjacent leaving cards anchors on the nearest surviving card instead of collapsing the page" + requirement: WLVW-04 + verification: + - kind: unit + ref: "web/app/lib/useScrollAnchor.test.tsx#useScrollAnchor run of leaving cards (G-25-11)" + status: pass + - kind: integration + ref: "web/app/routes/watchlist.test.tsx#corrects by the nearest survivor's drift when a run of cards leaves (G-25-11)" + status: pass + human_judgment: false + - id: D2 + description: "UI-SPEC D-12 scroll bullet states the widened fallback rule" + verification: + - kind: other + ref: "Task 2 grep and git-diff verify (exactly one removed and one added line)" + status: pass + human_judgment: false + +duration: 2min +completed: 2026-10-12 +status: complete +--- + +# Phase 25 Plan 19: Widen the scroll-anchor fallback Summary + +**`anchorCandidates` now returns the chip plus every sibling card nearest-first (next before previous at each distance), so un-pressing a chip that removes a run of adjacent cards still anchors on the nearest survivor.** + +## Performance + +- **Duration:** 2 min +- **Started:** 2026-10-12T22:06:18Z +- **Completed:** 2026-10-12T22:08:50Z +- **Tasks:** 2 +- **Files modified:** 4 + +## Accomplishments +- Closed G-25-11 (review WR-01): the fallback no longer stops at the two immediate neighbours, so a run of leaving cards cannot bring back the G-25-4 collapse. +- Capture API unchanged (`useScrollAnchor(key): (el) => void`), so `watchlist.tsx` and `TagChips.tsx` are untouched. +- Route test at `/?tags=3,7` (Arcangel, Bad Bunny, Chencho Corleone, Daddy Yankee, Eladio Carrion): un-pressing Chencho's reggaeton chip gives `?tags=7`, shows Arcangel and Eladio Carrion, and scrolls once by Eladio's drift (0, -600). +- UI-SPEC D-12 bullet rewritten with the widened rule, tagged `G-25-11, user decision 2026-10-12`. + +## Task Commits + +1. **Task 1: Run of leaving cards anchors on the nearest survivor** - `a4f8b09` (fix, TDD tracer; RED and GREEN in one commit per plan) +2. **Task 2: Record the widened rule in UI-SPEC D-12** - `9c51992` (docs) + +**Plan metadata:** committed separately (docs: complete plan). + +## RED evidence + +Before the `anchorCandidates` rewrite, `vitest run app/lib/useScrollAnchor.test.tsx app/routes/watchlist.test.tsx` gave 5 failed, 89 passed: +- hook "corrects by the next-side survivor when it ties on distance": `expected "bound " to be called 1 times, but got 0 times` +- hook "falls back to the previous side when the next side runs out": same (scrollBy never called) +- `anchorCandidates` "lists every sibling card nearest-first": received `[chip, D4, B2]`, missing `E5, A1` +- `anchorCandidates` "continues on the next side when the previous side runs out": received `[chip, C3, A1]`, missing `D4, E5` +- route "corrects by the nearest survivor's drift when a run of cards leaves": `scrollBy` called 0 times + +After the rewrite all 94 tests in the two files pass; the full web suite, typecheck and prettier check pass. + +## Files Created/Modified +- `web/app/lib/useScrollAnchor.ts` - `anchorCandidates` walks outward over `card.parentElement.children` +- `web/app/lib/useScrollAnchor.test.tsx` - `RunHarness`, two run-case hook tests, two ordering tests +- `web/app/routes/watchlist.test.tsx` - route case for a run of leaving cards +- `25-UI-SPEC.md` - D-12 scroll bullet final sentence + +## Decisions Made +- Recorded under key-decisions above: no distance cap; next before previous at each distance. +- 25-CONTEXT.md left unchanged, as the plan directs (no CONTEXT line governs the fallback order). + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered + +None. + +## Known Stubs + +None. + +## Threat Flags + +None. Client-only layout logic; no new endpoints, auth paths or schema. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness +- Plan 25-20 (G-25-12) can use the same fallback card as its focus target. +- The embedded SPA (`internal/webassets/build/client`) is not rebuilt here; that happens in plan 25-21. + +## Self-Check: PASSED + +- Files modified exist and match the plan's `files_modified`. +- Commits `a4f8b09` and `9c51992` exist on the branch; `git rev-list --count 5969c99..HEAD` = 2. +- Task 1 and Task 2 acceptance criteria re-run green (tests, typecheck, prettier check, full web test, unchanged-file guards, UI-SPEC one-line diff). + +--- +*Phase: 25-find-filter-watchlist-and-history* +*Completed: 2026-10-12* From 742f5794c565954d336f2c51341ed2ddc7f4b4c5 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 17:09:20 -0500 Subject: [PATCH 124/142] docs(25-19): complete widen scroll-anchor fallback plan --- .planning/ROADMAP.md | 6 +++--- .planning/STATE.md | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 682fc34..1ca7c64 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -193,7 +193,7 @@ Plans: 4. The user can filter History by a tag alongside the existing filters. It lists events for artists carrying that tag, including artists since removed from the watchlist, and it never shows an event older than the retention window, either on the first page or via "load older". 5. Opening the Watchlist makes one request regardless of watchlist size. Searching, sorting, and filtering then happen instantly with no further requests. -**Plans:** 18/21 plans executed +**Plans:** 19/21 plans executed Plans: **Wave 1** @@ -249,7 +249,7 @@ Plans: **UAT gap closure (round 3) — Wave 1** *(from 25-UAT.md gaps G-25-11, G-25-12, G-25-13, with the user's 2026-10-12 decisions; waves are sequential because each plan edits its own 25-UI-SPEC line)* -- [ ] 25-19-PLAN.md — When the clicked card leaves, the scroll anchor falls back to the nearest surviving card in either direction (next before previous at each distance), so a run of leaving cards never collapses the page. Includes hook and route run-case tests and the widened UI-SPEC D-12 rule (G-25-11) +- [x] 25-19-PLAN.md — When the clicked card leaves, the scroll anchor falls back to the nearest surviving card in either direction (next before previous at each distance), so a run of leaving cards never collapses the page. Includes hook and route run-case tests and the widened UI-SPEC D-12 rule (G-25-11) **UAT gap closure (round 3) — Wave 2** *(blocked on 25-19)* @@ -362,7 +362,7 @@ Phases execute in numeric order: 24 → 25 → 26 → 27 → 28 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 24. Artist Tags & Notes | 14/14 | Complete | 2026-10-06 | -| 25. Find & Filter — Watchlist and History | 18/18 | In Progress| | +| 25. Find & Filter — Watchlist and History | 19/21 | In Progress| | | 26. Bulk Edit & Remove | 0/? | Not started | - | | 27. Paste-a-List Bulk Add | 0/? | Not started | - | | 28. Tags on Discord Notifications | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 7151fc0..15d3e25 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,16 +5,16 @@ milestone_name: Watchlist Organization current_phase: 25 current_phase_name: Find & Filter — Watchlist and History status: executing -stopped_at: Completed 25-18-PLAN.md -last_updated: "2026-10-12T20:16:10.854Z" +stopped_at: Completed 25-19-PLAN.md +last_updated: "2026-10-12T22:09:16.147Z" last_activity: 2026-10-12 last_activity_desc: Phase 25 execution started -state_head: 48d663be1beaafff4837eb10c4128bd8629f4c7f +state_head: 41c74871186f1f9796405229a4d362c334a11e25 progress: total_phases: 5 completed_phases: 1 - total_plans: 32 - completed_plans: 32 + total_plans: 35 + completed_plans: 33 percent: 20 --- @@ -30,7 +30,7 @@ See: .planning/PROJECT.md (updated 2026-10-06) ## Current Position Phase: 25 (Find & Filter — Watchlist and History) — EXECUTING -Plan: 5 of 18 +Plan: 2 of 21 Status: Ready to execute Last activity: 2026-10-12 — Phase 25 execution started @@ -180,6 +180,7 @@ Last activity: 2026-10-12 — Phase 25 execution started | Phase 25 P16 | 4 min | 1 tasks | 2 files | | Phase 25 P17 | 5 min | 2 tasks | 3 files | | Phase 25 P18 | 5 min | 3 tasks | 3 files | +| Phase 25 P19 | 2 min | 2 tasks | 4 files | ## Accumulated Context @@ -422,6 +423,7 @@ Recent decisions affecting current work: - [Phase 25]: 25-16: Deezer hint title and 40% cap live on a wrapper span (a disabled button gets no pointer events); Button is w-full inside it (WR-04) - [Phase 25]: 25-17: useScrollAnchor falls back to the clicked card's next, then previous, sibling when the chip leaves the DOM (WR-02, user decision 2026-10-11); list heading excluded - [Phase 25]: 25-18: WR-01..WR-04 decisions recorded in UI-SPEC; CONTEXT G4 amended for WR-03 (user decision 2026-10-11) +- [Phase 25]: [Phase 25-19]: G-25-11 scroll anchor fallback widened to the nearest surviving card (no distance cap, next before previous at each distance), user decision 2026-10-12 ### Pending Todos @@ -517,8 +519,8 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-12T20:16:10.768Z -Stopped at: Completed 25-18-PLAN.md +Last session: 2026-10-12T22:09:16.043Z +Stopped at: Completed 25-19-PLAN.md Resume file: None ## Operator Next Steps From c9ea53c10957660068ab3bee0439e7c3089ed2c3 Mon Sep 17 00:00:00 2001 From: Caliber66 Date: Mon, 12 Oct 2026 17:14:41 -0500 Subject: [PATCH 125/142] fix(25-20): move focus to the fallback card's chip label when a keyboard toggle removes the card - TagChips passes click detail === 0 as viaKeyboard; chip label gains data-chip-label - useScrollAnchor reports the surviving card via useEffectEvent when a keyboard capture's focus is lost - route focuses the fallback card's pressed chip label with preventScroll --- .../components/watchlist/TagChips.test.tsx | 17 +++- web/app/components/watchlist/TagChips.tsx | 12 ++- web/app/lib/useScrollAnchor.ts | 53 +++++++++--- web/app/routes/watchlist.test.tsx | 86 +++++++++++++++++++ web/app/routes/watchlist.tsx | 19 +++- 5 files changed, 167 insertions(+), 20 deletions(-) diff --git a/web/app/components/watchlist/TagChips.test.tsx b/web/app/components/watchlist/TagChips.test.tsx index fc24e72..74a7dfa 100644 --- a/web/app/components/watchlist/TagChips.test.tsx +++ b/web/app/components/watchlist/TagChips.test.tsx @@ -553,13 +553,28 @@ describe("TagChips — chip label filter toggle (D-08)", () => { expect(toggleFilterTag).toHaveBeenCalledWith( { id: 3, name: "reggaeton" }, - label + label, + false ) expect(mockAttachTag).not.toHaveBeenCalled() expect(mockDetachTag).not.toHaveBeenCalled() expect(label).toHaveFocus() }) + it("Enter on the focused label reports a keyboard toggle", async () => { + const { toggleFilterTag } = renderFilterable() + const label = screen.getByRole("button", { name: "Filter by reggaeton" }) + + label.focus() + await userEvent.keyboard("{Enter}") + + expect(toggleFilterTag).toHaveBeenCalledWith( + { id: 3, name: "reggaeton" }, + label, + true + ) + }) + it("shows the active state with aria-pressed, a Check, and an inset ring", () => { renderFilterable(new Set([3])) diff --git a/web/app/components/watchlist/TagChips.tsx b/web/app/components/watchlist/TagChips.tsx index d7c0bb9..8f9928d 100644 --- a/web/app/components/watchlist/TagChips.tsx +++ b/web/app/components/watchlist/TagChips.tsx @@ -24,7 +24,11 @@ export interface TagActions { addTag(entryId: number, tag: TagRef, index?: number): void removeTag(entryId: number, tagId: number): void filterTagIds: ReadonlySet - toggleFilterTag(tag: TagRef, anchor?: HTMLElement): void + toggleFilterTag( + tag: TagRef, + anchor?: HTMLElement, + viaKeyboard?: boolean + ): void } // pendingIdCounter backs the temp ids of optimistic chips (D-24). @@ -189,8 +193,12 @@ export function TagChips({ entry, actions, announce }: TagChipsProps) { type="button" aria-pressed={active} aria-label={`Filter by ${tag.name}`} + data-chip-label className="-ml-2 flex h-6 min-w-0 cursor-pointer items-center gap-1 rounded-4xl pr-1 pl-2 hover:bg-foreground/10 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none" - onClick={(e) => actions.toggleFilterTag(tag, e.currentTarget)} + // detail is 0 only for an Enter or Space activation (G-25-12). + onClick={(e) => + actions.toggleFilterTag(tag, e.currentTarget, e.detail === 0) + } > {active && (