From 5dffe51b950a001cf606afed5230800ea2e3687d Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 8 Oct 2026 06:33:37 -0400 Subject: [PATCH 01/22] feat(web): duplicates report flags identical audio under different titles (#3885) An exact-tier group whose copies carry different titles means at least one file's tags are wrong, and the recording the other title names may be missing from the library. WWW (2020) was this: "WWW" was a second copy of the instrumental, the vocal was absent, and nothing said so. The report now names the titles and says what it implies, so the absence surfaces at the moment of choosing which copy to keep. Titles compare case- and whitespace-insensitively. Acoustic-tier groups are left alone: across encodings a "Remastered" suffix is routine, not a mislabel. Co-Authored-By: Claude Opus 5.5 --- web/src/routes/admin/duplicates/+page.svelte | 38 ++++++++++++- .../admin/duplicates/duplicates.test.ts | 55 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/web/src/routes/admin/duplicates/+page.svelte b/web/src/routes/admin/duplicates/+page.svelte index 5cf7a768..6553cd82 100644 --- a/web/src/routes/admin/duplicates/+page.svelte +++ b/web/src/routes/admin/duplicates/+page.svelte @@ -1,6 +1,6 @@ + + e.key === 'Escape' && (open = false)} /> + +
+ + + {#if open} + + {/if} +
diff --git a/web/src/lib/components/NotificationBell.test.ts b/web/src/lib/components/NotificationBell.test.ts new file mode 100644 index 00000000..19fafdd7 --- /dev/null +++ b/web/src/lib/components/NotificationBell.test.ts @@ -0,0 +1,157 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/svelte'; +import { mockQuery } from '../../test-utils/query'; + +const invalidateMock = vi.fn(); +vi.mock('@tanstack/svelte-query', async (orig) => { + const actual = (await orig()) as Record; + return { ...actual, useQueryClient: () => ({ invalidateQueries: invalidateMock }) }; +}); + +vi.mock('$lib/api/notifications', async (orig) => { + const actual = (await orig()) as Record; + return { + badgeLabel: actual.badgeLabel, + createUnreadCountQuery: vi.fn(), + createNotificationsQuery: vi.fn(), + markNotificationRead: vi.fn().mockResolvedValue(undefined), + markAllNotificationsRead: vi.fn().mockResolvedValue(undefined) + }; +}); + +vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() })); + +import NotificationBell from './NotificationBell.svelte'; +import { goto } from '$app/navigation'; +import { + badgeLabel, + createNotificationsQuery, + createUnreadCountQuery, + markAllNotificationsRead, + markNotificationRead, + type NotificationItem +} from '$lib/api/notifications'; + +function item(over: Partial = {}): NotificationItem { + return { + id: 'n1', + kind: 'request_completed', + title: 'Arrived: Boards of Canada – Geogaddi', + body: 'Your request is in the library.', + link: '/albums/al-1', + created_at: new Date().toISOString(), + read_at: null, + ...over + }; +} + +function setInbox(items: NotificationItem[], unread = items.filter((i) => !i.read_at).length) { + (createUnreadCountQuery as ReturnType).mockReturnValue(mockQuery({ data: unread })); + (createNotificationsQuery as ReturnType).mockReturnValue( + mockQuery({ data: { items, unread_count: unread } }) + ); +} + +async function openPanel() { + await fireEvent.click(screen.getByRole('button', { name: /notifications/i })); +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('badgeLabel', () => { + test('is empty at zero, the count to nine, then 9+', () => { + expect(badgeLabel(0)).toBe(''); + expect(badgeLabel(1)).toBe('1'); + expect(badgeLabel(9)).toBe('9'); + expect(badgeLabel(10)).toBe('9+'); + expect(badgeLabel(250)).toBe('9+'); + }); +}); + +describe('NotificationBell', () => { + test('shows the unread count on the badge and in the label', () => { + setInbox([item(), item({ id: 'n2' }), item({ id: 'n3' })]); + render(NotificationBell); + expect(screen.getByTestId('notification-badge')).toHaveTextContent('3'); + expect(screen.getByRole('button', { name: 'Notifications, 3 unread' })).toBeInTheDocument(); + }); + + test('caps the badge at 9+', () => { + setInbox([item()], 42); + render(NotificationBell); + expect(screen.getByTestId('notification-badge')).toHaveTextContent('9+'); + }); + + test('no badge when everything is read', () => { + setInbox([item({ read_at: new Date().toISOString() })]); + render(NotificationBell); + expect(screen.queryByTestId('notification-badge')).toBeNull(); + expect(screen.getByRole('button', { name: 'Notifications' })).toBeInTheDocument(); + }); + + test('lists each kind with the server-rendered title and body, unread marked', async () => { + setInbox([ + item({ id: 'a', kind: 'request_approved', title: 'Request approved', body: 'Geogaddi is on its way.' }), + item({ + id: 'b', + kind: 'tracks_missing', + title: '4 tracks went missing', + body: 'Their files are no longer where the library expects them.', + link: '/admin/missing-files', + read_at: new Date().toISOString() + }) + ]); + render(NotificationBell); + await openPanel(); + + const rows = screen.getAllByTestId('notification-item'); + expect(rows).toHaveLength(2); + expect(rows[0]).toHaveTextContent('Request approved'); + expect(rows[0]).toHaveTextContent('Geogaddi is on its way.'); + expect(rows[0]).toHaveAttribute('data-unread', 'true'); + expect(rows[1]).toHaveTextContent('4 tracks went missing'); + expect(rows[1]).not.toHaveAttribute('data-unread'); + }); + + test('clicking an unread item marks it read and opens its subject', async () => { + setInbox([item({ id: 'n7', link: '/albums/al-7' })]); + render(NotificationBell); + await openPanel(); + await fireEvent.click(screen.getByTestId('notification-item')); + + await waitFor(() => expect(goto).toHaveBeenCalledWith('/albums/al-7')); + expect(markNotificationRead).toHaveBeenCalledWith('n7'); + expect(invalidateMock).toHaveBeenCalledWith({ queryKey: ['notifications'] }); + expect(screen.queryByRole('dialog', { name: 'Notifications' })).toBeNull(); + }); + + test('clicking a read item only opens it', async () => { + setInbox([item({ read_at: new Date().toISOString(), link: '/requests' })]); + render(NotificationBell); + await openPanel(); + await fireEvent.click(screen.getByTestId('notification-item')); + + await waitFor(() => expect(goto).toHaveBeenCalledWith('/requests')); + expect(markNotificationRead).not.toHaveBeenCalled(); + }); + + test('mark all read clears everything and refreshes', async () => { + setInbox([item(), item({ id: 'n2' })]); + render(NotificationBell); + await openPanel(); + await fireEvent.click(screen.getByRole('button', { name: 'Mark all read' })); + + await waitFor(() => expect(markAllNotificationsRead).toHaveBeenCalled()); + expect(invalidateMock).toHaveBeenCalledWith({ queryKey: ['notifications'] }); + }); + + test('an empty inbox says so and offers no mark all', async () => { + setInbox([]); + render(NotificationBell); + await openPanel(); + expect(screen.getByText('Nothing waiting for you.')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Mark all read' })).toBeNull(); + }); +}); diff --git a/web/src/lib/components/Shell.svelte b/web/src/lib/components/Shell.svelte index b2ac1e39..abc1e392 100644 --- a/web/src/lib/components/Shell.svelte +++ b/web/src/lib/components/Shell.svelte @@ -6,6 +6,7 @@ import { player } from '$lib/player/store.svelte'; import { appName } from '$lib/branding'; import MinstrelMark from './MinstrelMark.svelte'; + import NotificationBell from './NotificationBell.svelte'; import PlayerBar from './PlayerBar.svelte'; import SearchInput from './SearchInput.svelte'; @@ -92,11 +93,13 @@ {/each} -
+
+ +
+

+ {:else if form === null} +

Loading the email schedule…

+ {:else} +
+ + + +
+ +
+ +
+ {/if} + diff --git a/web/src/lib/components/NotificationEmailCard.test.ts b/web/src/lib/components/NotificationEmailCard.test.ts new file mode 100644 index 00000000..bdcc1fb3 --- /dev/null +++ b/web/src/lib/components/NotificationEmailCard.test.ts @@ -0,0 +1,66 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/svelte'; + +vi.mock('$lib/api/admin', () => ({ + getNotificationEmailSettings: vi.fn(), + updateNotificationEmailSettings: vi.fn() +})); + +vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() })); + +import NotificationEmailCard from './NotificationEmailCard.svelte'; +import { getNotificationEmailSettings, updateNotificationEmailSettings } from '$lib/api/admin'; +import { pushToast } from '$lib/stores/toast.svelte'; + +afterEach(() => vi.clearAllMocks()); + +async function renderCard() { + vi.mocked(getNotificationEmailSettings).mockResolvedValue({ summary_hour: 9, batch_window_minutes: 60 }); + render(NotificationEmailCard); + return screen.findByRole('combobox', { name: /new music summary/i }); +} + +describe('NotificationEmailCard', () => { + test('shows the summary hour and the batch window', async () => { + const hour = await renderCard(); + expect(hour).toHaveValue('9'); + expect(screen.getByRole('option', { name: '09:00' })).toBeInTheDocument(); + expect(screen.getByRole('spinbutton', { name: /batch everything else/i })).toHaveValue(60); + }); + + test('save stays off until something changes, then sends both values', async () => { + const hour = await renderCard(); + const save = screen.getByRole('button', { name: 'Save' }); + expect(save).toBeDisabled(); + + vi.mocked(updateNotificationEmailSettings).mockResolvedValue({ summary_hour: 7, batch_window_minutes: 60 }); + await fireEvent.change(hour, { target: { value: '7' } }); + expect(save).toBeEnabled(); + await fireEvent.click(save); + + await waitFor(() => + expect(updateNotificationEmailSettings).toHaveBeenCalledWith({ summary_hour: 7, batch_window_minutes: 60 }) + ); + await waitFor(() => expect(save).toBeDisabled()); + }); + + test("a refused save shows the server's reason", async () => { + await renderCard(); + vi.mocked(updateNotificationEmailSettings).mockRejectedValue({ + code: 'invalid_setting', + message: 'batch_window_minutes must be 15-1440', + status: 400 + }); + const batch = screen.getByRole('spinbutton', { name: /batch everything else/i }); + await fireEvent.input(batch, { target: { value: '5' } }); + await fireEvent.click(screen.getByRole('button', { name: 'Save' })); + + await waitFor(() => expect(pushToast).toHaveBeenCalledWith(expect.stringContaining('15-1440'), 'error')); + }); + + test('a failed load offers a retry', async () => { + vi.mocked(getNotificationEmailSettings).mockRejectedValue(new Error('down')); + render(NotificationEmailCard); + expect(await screen.findByRole('button', { name: 'Try again' })).toBeInTheDocument(); + }); +}); diff --git a/web/src/routes/admin/integrations/+page.svelte b/web/src/routes/admin/integrations/+page.svelte index 34c9cf82..12f2ce23 100644 --- a/web/src/routes/admin/integrations/+page.svelte +++ b/web/src/routes/admin/integrations/+page.svelte @@ -30,6 +30,7 @@ import AcoustIDCard from '$lib/components/AcoustIDCard.svelte'; import NetworkSettingsCard from '$lib/components/NetworkSettingsCard.svelte'; import PublicAddressCard from '$lib/components/PublicAddressCard.svelte'; + import NotificationEmailCard from '$lib/components/NotificationEmailCard.svelte'; import type { LidarrConfig, LidarrTestResult } from '$lib/api/types'; // Lidarr connection panel. The "saved api key" is masked as "***" on GET — @@ -754,7 +755,7 @@

Email (SMTP)

- Configure outgoing email for password resets. The "Send test email" + Configure outgoing email for password resets and notifications. The "Send test email" button verifies your config end-to-end.

@@ -811,6 +812,8 @@
+ + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/test/java/com/fabledsword/minstrel/events/ReconnectBackoffTest.kt b/android/app/src/test/java/com/fabledsword/minstrel/events/ReconnectBackoffTest.kt new file mode 100644 index 00000000..f21eebac --- /dev/null +++ b/android/app/src/test/java/com/fabledsword/minstrel/events/ReconnectBackoffTest.kt @@ -0,0 +1,24 @@ +package com.fabledsword.minstrel.events + +import org.junit.jupiter.api.Test +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ReconnectBackoffTest { + @Test + fun `doubles from two seconds to a five minute cap`() { + val ladder = generateSequence(ReconnectBackoff.BASE_MS) { ReconnectBackoff.next(it) }.take(10).toList() + assertEquals(listOf(2_000L, 4_000L, 8_000L, 16_000L, 32_000L, 64_000L, 128_000L, 256_000L), ladder.take(8)) + assertEquals(300_000L, ladder[8]) + assertEquals(300_000L, ladder[9]) + } + + @Test + fun `jitter stays within a quarter either way and does spread`() { + val random = Random(42) + val waits = List(1_000) { ReconnectBackoff.jittered(100_000L, random) } + assertTrue(waits.all { it in 75_000L..125_000L }, "out of bounds: ${waits.minOrNull()}..${waits.maxOrNull()}") + assertTrue(waits.toSet().size > 100, "jitter should spread reconnects") + } +} diff --git a/android/app/src/test/java/com/fabledsword/minstrel/notifications/delivery/DeliveryPlanTest.kt b/android/app/src/test/java/com/fabledsword/minstrel/notifications/delivery/DeliveryPlanTest.kt new file mode 100644 index 00000000..e4e2c3a7 --- /dev/null +++ b/android/app/src/test/java/com/fabledsword/minstrel/notifications/delivery/DeliveryPlanTest.kt @@ -0,0 +1,128 @@ +package com.fabledsword.minstrel.notifications.delivery + +import com.fabledsword.minstrel.api.endpoints.NotificationKindSettingWire +import com.fabledsword.minstrel.api.endpoints.NotificationSettingsWire +import com.fabledsword.minstrel.cache.db.entities.CachedNotificationEntity +import kotlinx.datetime.Instant +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class DeliveryPlanTest { + private val t0 = Instant.parse("2026-10-08T12:00:00Z") + + private fun notice( + id: String, + minutes: Long, + kind: String = "request_approved", + read: Boolean = false, + ) = CachedNotificationEntity( + id = id, + kind = kind, + title = "t-$id", + body = "b-$id", + link = "/requests", + createdAt = Instant.fromEpochMilliseconds(t0.toEpochMilliseconds() + minutes * 60_000), + readAt = if (read) t0 else null, + ) + + private val allOn: (String) -> Boolean = { true } + + @Test + fun `a first look sets the mark to the newest and announces nothing`() { + val page = listOf(notice("b", 5), notice("a", 1)) + val plan = planCatchUp(page, mark = null, phoneOn = allOn) + assertTrue(plan.announce.isEmpty()) + assertEquals(page[0].createdAt, plan.upTo) + } + + @Test + fun `a first look at an empty inbox still lets the first notice through`() { + val first = planCatchUp(emptyList(), mark = null, phoneOn = allOn) + assertEquals(Instant.DISTANT_PAST, first.upTo) + val next = planCatchUp(listOf(notice("a", 1)), mark = first.upTo, phoneOn = allOn) + assertEquals(listOf("a"), next.announce.map { it.id }) + } + + @Test + fun `announces unread notices newer than the mark, oldest first, and moves the mark`() { + val page = listOf(notice("c", 9), notice("b", 6), notice("old", 2), notice("read", 7, read = true)) + val plan = planCatchUp(page, mark = notice("m", 3).createdAt, phoneOn = allOn) + assertEquals(listOf("b", "c"), plan.announce.map { it.id }) + assertEquals(page[0].createdAt, plan.upTo) + } + + @Test + fun `nothing new leaves the mark where it was`() { + val mark = notice("m", 10).createdAt + val plan = planCatchUp(listOf(notice("a", 1)), mark = mark, phoneOn = allOn) + assertTrue(plan.announce.isEmpty()) + assertEquals(mark, plan.upTo) + } + + @Test + fun `a kind with the phone off is passed over, and not announced later either`() { + val page = listOf(notice("h", 5, kind = "tracks_missing"), notice("r", 4)) + val plan = planCatchUp(page, mark = t0, phoneOn = { it != "tracks_missing" }) + assertEquals(listOf("r"), plan.announce.map { it.id }) + assertEquals(page[0].createdAt, plan.upTo, "the mark passes it") + } + + @Test + fun `phone prefs come from the cached settings, with every kind on when none are cached`() { + val settings = NotificationSettingsWire( + kinds = listOf( + NotificationKindSettingWire("request_approved", false, inbox = true, phone = false, email = true), + NotificationKindSettingWire("request_completed", false, inbox = false, phone = true, email = true), + NotificationKindSettingWire("request_rejected", false, inbox = true, phone = true, email = true), + ), + emailAvailable = true, + ) + val phoneOn = phoneOnFor(settings) + assertFalse(phoneOn("request_approved")) + assertFalse(phoneOn("request_completed"), "phone rides on the inbox") + assertTrue(phoneOn("request_rejected")) + assertTrue(phoneOnFor(null)("tracks_missing")) + } + + @Test + fun `three get a line each, more become one line with the count`() { + assertNull(announcementFor(emptyList())) + val three = (1..3).map { notice("n$it", it.toLong()) } + assertEquals(Announcement.Each(three), announcementFor(three)) + + val five = (1..5).map { notice("n$it", it.toLong()) } + val pile = assertIs(announcementFor(five)) + assertEquals(5, pile.count) + assertEquals(ShadeChannel.YOUR_REQUESTS, pile.channel) + assertEquals("5 new notifications", pileText(pile.count)) + } + + @Test + fun `a pile of admin notices goes to library health, a mixed one does not`() { + val admin = (1..4).map { notice("a$it", it.toLong(), kind = "tracks_missing") } + assertEquals(ShadeChannel.LIBRARY_HEALTH, assertIs(announcementFor(admin)).channel) + val mixed = admin + notice("r", 9) + assertEquals(ShadeChannel.YOUR_REQUESTS, assertIs(announcementFor(mixed)).channel) + } + + @Test + fun `listener kinds are their requests, the rest is library health`() { + listOf("request_approved", "request_rejected", "request_completed").forEach { + assertEquals(ShadeChannel.YOUR_REQUESTS, shadeChannelFor(it)) + } + listOf("request_pending", "quarantine_flagged", "scan_failed", "tracks_missing").forEach { + assertEquals(ShadeChannel.LIBRARY_HEALTH, shadeChannelFor(it)) + } + } + + @Test + fun `the service runs only signed in with background delivery on`() { + assertTrue(deliveryWanted(signedIn = true, enabled = true)) + assertFalse(deliveryWanted(signedIn = true, enabled = false)) + assertFalse(deliveryWanted(signedIn = false, enabled = true)) + } +} From 9be5bbae3cfe41d8484da8b8cf657b782c6a9b16 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 8 Oct 2026 09:24:20 -0400 Subject: [PATCH 13/22] fix(discover): cap the taste-matched arm per album and artist before its LIMIT (#5356) Summed tag weight rewards a track for carrying many of the user's tags, so on the deploy two artists whose every track carries the whole lo-fi profile took all 120 rows of the taste-unheard query. capByAlbumAndArtist ran after the LIMIT and left 6, and the arm with the lowest skip rate (12% against ~23%) handed its slots to dormant and random. The query now ranks within album, then within artist over what the album cap kept, before the LIMIT: the same walk the Go cap makes, so the bucket fills from as many artists as match. The caps come from the Go constants. Co-Authored-By: Claude Opus 5.5 --- internal/db/dbq/discover.sql.go | 86 +++++++++++----- internal/db/queries/discover.sql | 73 +++++++++----- internal/playlists/discover.go | 5 +- internal/playlists/discover_taste_db_test.go | 100 +++++++++++++++++++ 4 files changed, 212 insertions(+), 52 deletions(-) create mode 100644 internal/playlists/discover_taste_db_test.go diff --git a/internal/db/dbq/discover.sql.go b/internal/db/dbq/discover.sql.go index 7abd362a..3689aeba 100644 --- a/internal/db/dbq/discover.sql.go +++ b/internal/db/dbq/discover.sql.go @@ -216,35 +216,55 @@ func (q *Queries) ListRandomUnheardTracksForDiscover(ctx context.Context, arg Li } const listTasteUnheardTracksForDiscover = `-- name: ListTasteUnheardTracksForDiscover :many -SELECT t.id, t.album_id, t.artist_id - FROM tracks t - JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g_split(g) ON true - JOIN taste_profile_tags nt ON nt.user_id = $1 AND trim(g_split.g) = nt.tag - WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone - AND nt.weight > 0 - AND trim(g_split.g) <> '' - AND NOT EXISTS ( - SELECT 1 FROM play_events pe - WHERE pe.user_id = $1 - AND pe.track_id = t.id - AND pe.was_skipped = false - ) - AND NOT EXISTS ( - SELECT 1 FROM general_likes gl - WHERE gl.user_id = $1 AND gl.track_id = t.id - ) - AND NOT EXISTS ( - SELECT 1 FROM lidarr_quarantine q - WHERE q.user_id = $1 AND q.track_id = t.id - ) - GROUP BY t.id, t.album_id, t.artist_id - ORDER BY SUM(nt.weight) DESC, md5(t.id::text || $2::text) +WITH scored AS ( + SELECT t.id, t.album_id, t.artist_id, + SUM(nt.weight) AS weight, + md5(t.id::text || $2::text) AS tiebreak + FROM tracks t + JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g_split(g) ON true + JOIN taste_profile_tags nt ON nt.user_id = $3 AND trim(g_split.g) = nt.tag + WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone + AND nt.weight > 0 + AND trim(g_split.g) <> '' + AND NOT EXISTS ( + SELECT 1 FROM play_events pe + WHERE pe.user_id = $3 + AND pe.track_id = t.id + AND pe.was_skipped = false + ) + AND NOT EXISTS ( + SELECT 1 FROM general_likes gl + WHERE gl.user_id = $3 AND gl.track_id = t.id + ) + AND NOT EXISTS ( + SELECT 1 FROM lidarr_quarantine q + WHERE q.user_id = $3 AND q.track_id = t.id + ) + GROUP BY t.id, t.album_id, t.artist_id +), +album_capped AS ( + SELECT s.id, s.album_id, s.artist_id, s.weight, s.tiebreak, + row_number() OVER (PARTITION BY s.album_id ORDER BY s.weight DESC, s.tiebreak) AS album_rank + FROM scored s +), +artist_capped AS ( + SELECT a.id, a.album_id, a.artist_id, a.weight, a.tiebreak, a.album_rank, + row_number() OVER (PARTITION BY a.artist_id ORDER BY a.weight DESC, a.tiebreak) AS artist_rank + FROM album_capped a + WHERE a.album_rank <= $4::int +) +SELECT c.id, c.album_id, c.artist_id + FROM artist_capped c + WHERE c.artist_rank <= $1::int + ORDER BY c.weight DESC, c.tiebreak LIMIT 120 ` type ListTasteUnheardTracksForDiscoverParams struct { - UserID pgtype.UUID - Column2 string + MaxPerArtist int32 + DateSeed string + UserID pgtype.UUID + MaxPerAlbum int32 } type ListTasteUnheardTracksForDiscoverRow struct { @@ -261,9 +281,21 @@ type ListTasteUnheardTracksForDiscoverRow struct { // [;,]). Same exclusion filters as the other buckets. Returns nothing // when the user has no taste tags yet (cold start), so the caller // redistributes its slots to the other buckets. Stamped 'taste_unheard'. -// $1 = user_id, $2 = date string for md5 tiebreak ordering. +// +// The per-album and per-artist caps apply BEFORE the LIMIT (#5356). Summed +// weight rewards a track for carrying many of the user's tags, so a few +// artists whose every track is tagged with the whole profile take every row +// of a plain LIMIT; the caller's caps then left 6 of 120 on the deploy, and +// the best-performing arm handed its slots to the others. Ranking within +// album, then within artist over what the album cap kept, is the same walk +// capByAlbumAndArtist makes, so the caller's caps keep everything here. func (q *Queries) ListTasteUnheardTracksForDiscover(ctx context.Context, arg ListTasteUnheardTracksForDiscoverParams) ([]ListTasteUnheardTracksForDiscoverRow, error) { - rows, err := q.db.Query(ctx, listTasteUnheardTracksForDiscover, arg.UserID, arg.Column2) + rows, err := q.db.Query(ctx, listTasteUnheardTracksForDiscover, + arg.MaxPerArtist, + arg.DateSeed, + arg.UserID, + arg.MaxPerAlbum, + ) if err != nil { return nil, err } diff --git a/internal/db/queries/discover.sql b/internal/db/queries/discover.sql index 86385609..2c297610 100644 --- a/internal/db/queries/discover.sql +++ b/internal/db/queries/discover.sql @@ -115,28 +115,53 @@ SELECT t.id, t.album_id, t.artist_id -- [;,]). Same exclusion filters as the other buckets. Returns nothing -- when the user has no taste tags yet (cold start), so the caller -- redistributes its slots to the other buckets. Stamped 'taste_unheard'. --- $1 = user_id, $2 = date string for md5 tiebreak ordering. -SELECT t.id, t.album_id, t.artist_id - FROM tracks t - JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g_split(g) ON true - JOIN taste_profile_tags nt ON nt.user_id = $1 AND trim(g_split.g) = nt.tag - WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone - AND nt.weight > 0 - AND trim(g_split.g) <> '' - AND NOT EXISTS ( - SELECT 1 FROM play_events pe - WHERE pe.user_id = $1 - AND pe.track_id = t.id - AND pe.was_skipped = false - ) - AND NOT EXISTS ( - SELECT 1 FROM general_likes gl - WHERE gl.user_id = $1 AND gl.track_id = t.id - ) - AND NOT EXISTS ( - SELECT 1 FROM lidarr_quarantine q - WHERE q.user_id = $1 AND q.track_id = t.id - ) - GROUP BY t.id, t.album_id, t.artist_id - ORDER BY SUM(nt.weight) DESC, md5(t.id::text || $2::text) +-- +-- The per-album and per-artist caps apply BEFORE the LIMIT (#5356). Summed +-- weight rewards a track for carrying many of the user's tags, so a few +-- artists whose every track is tagged with the whole profile take every row +-- of a plain LIMIT; the caller's caps then left 6 of 120 on the deploy, and +-- the best-performing arm handed its slots to the others. Ranking within +-- album, then within artist over what the album cap kept, is the same walk +-- capByAlbumAndArtist makes, so the caller's caps keep everything here. +WITH scored AS ( + SELECT t.id, t.album_id, t.artist_id, + SUM(nt.weight) AS weight, + md5(t.id::text || sqlc.arg(date_seed)::text) AS tiebreak + FROM tracks t + JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g_split(g) ON true + JOIN taste_profile_tags nt ON nt.user_id = sqlc.arg(user_id) AND trim(g_split.g) = nt.tag + WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone + AND nt.weight > 0 + AND trim(g_split.g) <> '' + AND NOT EXISTS ( + SELECT 1 FROM play_events pe + WHERE pe.user_id = sqlc.arg(user_id) + AND pe.track_id = t.id + AND pe.was_skipped = false + ) + AND NOT EXISTS ( + SELECT 1 FROM general_likes gl + WHERE gl.user_id = sqlc.arg(user_id) AND gl.track_id = t.id + ) + AND NOT EXISTS ( + SELECT 1 FROM lidarr_quarantine q + WHERE q.user_id = sqlc.arg(user_id) AND q.track_id = t.id + ) + GROUP BY t.id, t.album_id, t.artist_id +), +album_capped AS ( + SELECT s.*, + row_number() OVER (PARTITION BY s.album_id ORDER BY s.weight DESC, s.tiebreak) AS album_rank + FROM scored s +), +artist_capped AS ( + SELECT a.*, + row_number() OVER (PARTITION BY a.artist_id ORDER BY a.weight DESC, a.tiebreak) AS artist_rank + FROM album_capped a + WHERE a.album_rank <= sqlc.arg(max_per_album)::int +) +SELECT c.id, c.album_id, c.artist_id + FROM artist_capped c + WHERE c.artist_rank <= sqlc.arg(max_per_artist)::int + ORDER BY c.weight DESC, c.tiebreak LIMIT 120; diff --git a/internal/playlists/discover.go b/internal/playlists/discover.go index f70c658d..e30d3c31 100644 --- a/internal/playlists/discover.go +++ b/internal/playlists/discover.go @@ -95,8 +95,11 @@ type discoverPools struct { } func loadDiscoverPools(ctx context.Context, q *dbq.Queries, logger *slog.Logger, userID pgtype.UUID, dateStr string) discoverPools { + // The caps go into the query too (#5356): applied only here, after its + // LIMIT, they left a heavily tagged artist or two owning the whole bucket. tasteRows, err := q.ListTasteUnheardTracksForDiscover(ctx, dbq.ListTasteUnheardTracksForDiscoverParams{ - UserID: userID, Column2: dateStr, + UserID: userID, DateSeed: dateStr, + MaxPerAlbum: discoverMaxTracksPerAlbum, MaxPerArtist: discoverMaxTracksPerArtist, }) if err != nil { logger.Warn("discover: taste-unheard bucket failed; continuing with empty pool", diff --git a/internal/playlists/discover_taste_db_test.go b/internal/playlists/discover_taste_db_test.go new file mode 100644 index 00000000..81772972 --- /dev/null +++ b/internal/playlists/discover_taste_db_test.go @@ -0,0 +1,100 @@ +package playlists_test + +import ( + "context" + "fmt" + "testing" + + "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" + + "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" +) + +func setGenre(t *testing.T, pool *pgxpool.Pool, trackID pgtype.UUID, genre string) { + t.Helper() + if _, err := pool.Exec(context.Background(), + `UPDATE tracks SET genre = $2 WHERE id = $1`, trackID, genre); err != nil { + t.Fatalf("set genre: %v", err) + } +} + +func seedTasteTag(t *testing.T, pool *pgxpool.Pool, userID pgtype.UUID, tag string, weight float64) { + t.Helper() + if _, err := pool.Exec(context.Background(), + `INSERT INTO taste_profile_tags (user_id, tag, weight) VALUES ($1, $2, $3) + ON CONFLICT (user_id, tag) DO UPDATE SET weight = EXCLUDED.weight`, + userID, tag, weight); err != nil { + t.Fatalf("seed taste tag: %v", err) + } +} + +// TestListTasteUnheardTracksForDiscover_CapsBeforeLimit is the deploy's shape +// (#5356): one artist whose every track carries the whole taste profile +// outscores everything on summed weight. Capped only after the LIMIT, that +// artist took all 120 rows and the bucket shrank to its 3. Capped before, the +// artist keeps 3 and the single-tag artists below it fill the rest. +func TestListTasteUnheardTracksForDiscover_CapsBeforeLimit(t *testing.T) { + pool := newPool(t) + ctx := context.Background() + u := seedUser(t, pool, "tastecap") + + profile := []string{"Lo-Fi", "Downtempo", "Hip Hop", "Instrumental", "Chillwave"} + for _, tag := range profile { + seedTasteTag(t, pool, u.ID, tag, 10) + } + allTags := "Lo-Fi; Downtempo; Hip Hop; Instrumental; Chillwave" + + // 13 albums x 10 tracks = 130, more than the query's LIMIT of 120. + heavy := seedTrack(t, pool, "Heavy 0", "Heavy Artist") + setGenre(t, pool, heavy.ID, allTags) + album := heavy.AlbumID + for i := 1; i < 130; i++ { + if i%10 == 0 { + album = seedAlbumForArtist(t, pool, fmt.Sprintf("Heavy Album %d", i/10), heavy.ArtistID) + } + tr := seedTrackForArtist(t, pool, fmt.Sprintf("Heavy %d", i), album, heavy.ArtistID) + setGenre(t, pool, tr.ID, allTags) + } + + // Twelve artists with one track each, matching a single profile tag. + const light = 12 + for i := 0; i < light; i++ { + tr := seedTrack(t, pool, fmt.Sprintf("Light %d", i), fmt.Sprintf("Light Artist %d", i)) + setGenre(t, pool, tr.ID, profile[i%len(profile)]) + } + + rows, err := dbq.New(pool).ListTasteUnheardTracksForDiscover(ctx, dbq.ListTasteUnheardTracksForDiscoverParams{ + UserID: u.ID, DateSeed: "2026-10-08", MaxPerAlbum: 2, MaxPerArtist: 3, + }) + if err != nil { + t.Fatalf("list taste unheard: %v", err) + } + + perArtist := map[pgtype.UUID]int{} + perAlbum := map[pgtype.UUID]int{} + for _, r := range rows { + perArtist[r.ArtistID]++ + perAlbum[r.AlbumID]++ + } + if got := perArtist[heavy.ArtistID]; got != 3 { + t.Errorf("heavy artist rows = %d, want exactly the artist cap of 3", got) + } + for id, n := range perAlbum { + if n > 2 { + t.Errorf("album %v has %d rows, over the album cap of 2", id, n) + } + } + if got, want := len(perArtist), 1+light; got != want { + t.Errorf("distinct artists = %d, want %d (the heavy artist plus every single-tag artist)", got, want) + } + if got, want := len(rows), 3+light; got != want { + t.Errorf("rows = %d, want %d", got, want) + } + // Still ranked by weight: the heavy artist's three lead. + for i := 0; i < 3 && i < len(rows); i++ { + if rows[i].ArtistID != heavy.ArtistID { + t.Errorf("row %d is not the heavy artist's; higher summed weight should rank first", i) + } + } +} From 39c33e4306eaa929d0c85792b43c9e507ad728b0 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 8 Oct 2026 14:19:16 +0000 Subject: [PATCH 14/22] chore(deps): update dependency @sveltejs/vite-plugin-svelte to v7 --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 228ea3fa..04bc8213 100644 --- a/web/package.json +++ b/web/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.8.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", "@types/node": "^25.9.1", From 5dfbe1361edb9c3011731fa11fb618e6adf13f45 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 8 Oct 2026 14:19:18 +0000 Subject: [PATCH 15/22] chore(deps): update dependency vite to v8 --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 228ea3fa..0763e3f5 100644 --- a/web/package.json +++ b/web/package.json @@ -25,7 +25,7 @@ "svelte-check": "^4.4.8", "tailwindcss": "^3.4.14", "typescript": "^5.6.3", - "vite": "^5.4.10", + "vite": "^8.0.0", "vitest": "^2.1.4" }, "dependencies": { From 67f1975f53ed475944c4f3903749f41b502af8d2 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 8 Oct 2026 14:19:20 +0000 Subject: [PATCH 16/22] chore(deps): update dependency vitest to v5 --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 228ea3fa..3ad8b959 100644 --- a/web/package.json +++ b/web/package.json @@ -26,7 +26,7 @@ "tailwindcss": "^3.4.14", "typescript": "^5.6.3", "vite": "^5.4.10", - "vitest": "^2.1.4" + "vitest": "^5.0.0" }, "dependencies": { "@neodrag/svelte": "^2.3.3", From c83933b61aa847b3c5ac63e456e62b585b8386fb Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 8 Oct 2026 14:45:00 +0000 Subject: [PATCH 17/22] chore(deps): update dependency @sveltejs/adapter-static to v4 --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 5e23ebc4..f2deba11 100644 --- a/web/package.json +++ b/web/package.json @@ -12,7 +12,7 @@ "tokens": "node scripts/tokens-to-css.js" }, "devDependencies": { - "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/adapter-static": "^4.0.0", "@sveltejs/kit": "^2.8.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@testing-library/jest-dom": "^6.9.1", From 0500f6aac5a65b4c4c3d9e39f94e63a3b9e7ff47 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 8 Oct 2026 14:45:01 +0000 Subject: [PATCH 18/22] chore(deps): update dependency @sveltejs/kit to v3 --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 5e23ebc4..a532ff7c 100644 --- a/web/package.json +++ b/web/package.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.8.0", + "@sveltejs/kit": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", From 0202c8633867c382195ba4aab1ea80c3a2430c75 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 8 Oct 2026 14:45:03 +0000 Subject: [PATCH 19/22] chore(deps): update dependency tailwindcss to v4 --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 5e23ebc4..94981215 100644 --- a/web/package.json +++ b/web/package.json @@ -23,7 +23,7 @@ "postcss": "^8.5.15", "svelte": "^5.1.9", "svelte-check": "^4.4.8", - "tailwindcss": "^3.4.14", + "tailwindcss": "^4.0.0", "typescript": "^5.6.3", "vite": "^8.0.0", "vitest": "^5.0.0" From fecd030b6852e327869173b4b485db769a6466ad Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 8 Oct 2026 10:46:07 -0400 Subject: [PATCH 20/22] chore(deps): Tailwind 4 (#5021) Renovate's tailwindcss bump (PR #150) plus the migration it needs: - Theme moves from tailwind.config.js into app.css as `@theme inline`, mapping the same FabledSword tokens. tailwind.config.js is gone. - PostCSS runs @tailwindcss/postcss; autoprefixer is dropped, since Tailwind 4 prefixes through Lightning CSS. - Class renames from @tailwindcss/upgrade 4.3.3, reviewed: outline-none -> outline-hidden, focus-visible:outline -> outline-solid, shadow -> shadow-sm, shadow-sm -> shadow-xs, flex-shrink-0 -> shrink-0. Bare `rounded` stays: v4 keeps it at 0.25rem, as before. - Three v3 preflight defaults kept in a base layer so nothing changes on screen: gray-200 default border colour, gray-400 placeholder text and the pointer cursor on buttons. - The unused class-based dark variant is not carried over; no template uses `dark:`. Clears the five high and two moderate npm audit findings that came in through Tailwind 3 (braces, chokidar, micromatch, fast-glob, postcss-selector-parser). Co-Authored-By: Claude Opus 5.5 --- web/package-lock.json | 1589 +++++++---------- web/package.json | 4 +- web/postcss.config.cjs | 3 +- web/src/app.css | 75 +- web/src/lib/components/AcoustIDCard.svelte | 6 +- web/src/lib/components/ActiveSessions.svelte | 4 +- web/src/lib/components/AlbumCard.svelte | 2 +- .../lib/components/CompactTrackCard.svelte | 2 +- .../lib/components/DiscoverResultCard.svelte | 4 +- .../components/FingerprintSettingsCard.svelte | 6 +- web/src/lib/components/FlagPopover.svelte | 4 +- web/src/lib/components/HistoryRow.svelte | 4 +- .../lib/components/HorizontalScrollRow.svelte | 2 +- .../components/LoudnessSettingsCard.svelte | 4 +- .../lib/components/NetworkSettingsCard.svelte | 4 +- .../lib/components/NotificationBell.svelte | 6 +- .../components/NotificationEmailCard.svelte | 6 +- web/src/lib/components/PlayerBar.svelte | 8 +- web/src/lib/components/PlaylistCard.svelte | 4 +- .../lib/components/PlaylistTrackRow.svelte | 8 +- .../lib/components/PublicAddressCard.svelte | 4 +- web/src/lib/components/QueueTrackRow.svelte | 8 +- web/src/lib/components/QuickFilter.svelte | 2 +- .../ReacquisitionSettingsCard.svelte | 14 +- web/src/lib/components/SearchInput.svelte | 2 +- web/src/lib/components/Shell.svelte | 6 +- web/src/lib/components/SuggestionFeed.svelte | 2 +- web/src/lib/components/TrackRow.svelte | 2 +- web/src/routes/admin/duplicates/+page.svelte | 2 +- .../routes/admin/integrations/+page.svelte | 28 +- .../routes/admin/missing-files/+page.svelte | 4 +- web/src/routes/admin/quarantine/+page.svelte | 2 +- web/src/routes/admin/requests/+page.svelte | 6 +- web/src/routes/admin/tuning/+page.svelte | 6 +- web/src/routes/admin/users/+page.svelte | 12 +- web/src/routes/artists/[id]/+page.svelte | 2 +- web/src/routes/discover/+page.svelte | 2 +- web/src/routes/forgot-password/+page.svelte | 4 +- web/src/routes/library/genres/+page.svelte | 4 +- web/src/routes/login/+page.svelte | 6 +- web/src/routes/now-playing/+page.svelte | 2 +- web/src/routes/playlists/[id]/+page.svelte | 4 +- web/src/routes/register/+page.svelte | 14 +- .../reset-password/[token]/+page.svelte | 6 +- web/src/routes/settings/+page.svelte | 12 +- web/tailwind.config.js | 53 - 46 files changed, 795 insertions(+), 1159 deletions(-) delete mode 100644 web/tailwind.config.js diff --git a/web/package-lock.json b/web/package-lock.json index 77ed526a..ff7b1992 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -16,15 +16,15 @@ "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.8.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", "@types/node": "^25.9.1", - "autoprefixer": "^10.5.0", "jsdom": "^25.0.1", "postcss": "^8.5.15", "svelte": "^5.1.9", "svelte-check": "^4.4.8", - "tailwindcss": "^3.4.14", + "tailwindcss": "^4.3.3", "typescript": "^5.6.3", "vite": "^8.0.0", "vitest": "^5.0.0" @@ -709,44 +709,6 @@ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@oxc-project/types": { "version": "0.153.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", @@ -1103,6 +1065,577 @@ "@jridgewell/sourcemap-codec": "^1.6.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, "node_modules/@tanstack/query-core": { "version": "5.90.2", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", @@ -1361,47 +1894,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, "node_modules/aria-query": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", @@ -1428,43 +1920,6 @@ "dev": true, "license": "MIT" }, - "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -1474,77 +1929,6 @@ "node": ">= 0.4" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.27", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", - "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", - "dev": true, - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.29.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", - "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "baseline-browser-mapping": "^2.11.26", - "caniuse-lite": "^1.0.30001813", - "electron-to-chromium": "^1.5.439", - "node-releases": "^2.0.57", - "update-browserslist-db": "^1.3.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1559,36 +1943,6 @@ "node": ">= 0.4" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001814", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", - "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, "node_modules/chai": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", @@ -1636,16 +1990,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -1663,19 +2007,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -1780,20 +2111,6 @@ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==" }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, "node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", @@ -1816,11 +2133,18 @@ "node": ">= 0.4" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.445", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.445.tgz", - "integrity": "sha512-iX7o1nF+qNb3Bv5mLYgqfgF9Znh9e/8MKXdmbb8Gv2Q47OKIzTwEPD1oda1HrgEwYatTLJj0wQjHvIFt8Db53Q==", - "dev": true + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } }, "node_modules/entities": { "version": "6.0.1", @@ -1927,15 +2251,6 @@ "@esbuild/win32-x64": "0.28.2" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", @@ -1977,46 +2292,6 @@ "node": ">=12.0.0" } }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2035,19 +2310,6 @@ } } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -2064,20 +2326,6 @@ "node": ">= 6" } }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2142,19 +2390,6 @@ "node": ">= 0.4" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2168,6 +2403,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2273,68 +2514,6 @@ "node": ">=8" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -2352,13 +2531,12 @@ } }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, - "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-tokens": { @@ -2668,26 +2846,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -2739,43 +2897,6 @@ "node": ">= 0.4" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2836,18 +2957,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", @@ -2866,25 +2975,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/node-releases": { - "version": "2.0.57", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", - "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nwsapi": { "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", @@ -2892,26 +2982,6 @@ "dev": true, "license": "MIT" }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/obug": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", @@ -2938,13 +3008,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2964,26 +3027,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/postcss": { "version": "8.5.29", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", @@ -3012,139 +3055,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -3170,27 +3080,6 @@ "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -3198,16 +3087,6 @@ "dev": true, "license": "MIT" }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3236,39 +3115,6 @@ "node": ">=8" } }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rolldown": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", @@ -3309,30 +3155,6 @@ "dev": true, "license": "MIT" }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -3416,42 +3238,6 @@ "node": ">=8" } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/svelte": { "version": "5.57.1", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.1.tgz", @@ -3514,128 +3300,22 @@ "license": "MIT" }, "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true }, - "node_modules/tailwindcss/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, "engines": { - "node": ">= 8.10.0" + "node": ">=6" }, "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tailwindcss/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tailwindcss/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tinybench": { @@ -3692,19 +3372,6 @@ "dev": true, "license": "MIT" }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -3741,13 +3408,6 @@ "node": ">=18" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3769,43 +3429,6 @@ "dev": true, "license": "MIT" }, - "node_modules/update-browserslist-db": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", - "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, "node_modules/vite": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", diff --git a/web/package.json b/web/package.json index 94981215..46bc2986 100644 --- a/web/package.json +++ b/web/package.json @@ -15,15 +15,15 @@ "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.8.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", "@types/node": "^25.9.1", - "autoprefixer": "^10.5.0", "jsdom": "^25.0.1", "postcss": "^8.5.15", "svelte": "^5.1.9", "svelte-check": "^4.4.8", - "tailwindcss": "^4.0.0", + "tailwindcss": "^4.3.3", "typescript": "^5.6.3", "vite": "^8.0.0", "vitest": "^5.0.0" diff --git a/web/postcss.config.cjs b/web/postcss.config.cjs index 5cbc2c7d..8efcc731 100644 --- a/web/postcss.config.cjs +++ b/web/postcss.config.cjs @@ -1,6 +1,5 @@ module.exports = { plugins: { - tailwindcss: {}, - autoprefixer: {} + '@tailwindcss/postcss': {}, } }; diff --git a/web/src/app.css b/web/src/app.css index d44306c1..31ac15c9 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,8 +1,75 @@ -@import './lib/styles/tokens.generated.css'; +@import './lib/styles/tokens.generated.css' layer(base); -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; + +/* The FabledSword tokens as Tailwind's theme. `inline` makes each utility emit + * the var(--fs-*) itself, so a theme swap that redefines the tokens reaches the + * utilities without going through a second variable. */ +@theme inline { + --color-background: var(--fs-obsidian); + + --color-surface: var(--fs-iron); + --color-surface-hover: var(--fs-slate); + + --color-border: var(--fs-pewter); + + --color-text-primary: var(--fs-parchment); + --color-text-secondary: var(--fs-vellum); + --color-text-muted: var(--fs-ash); + + --color-action-primary: var(--fs-moss); + --color-action-secondary: var(--fs-bronze); + --color-action-destructive: var(--fs-oxblood); + --color-action-fg: var(--fs-on-action); + + --color-accent: var(--fs-accent); + --color-accent-tint: color-mix(in srgb, var(--fs-accent) 12%, transparent); + --color-accent-fg: var(--fs-accent-fg); + + --color-warning: var(--fs-warning); + --color-warning-fg: var(--fs-warning-fg); + + --color-error: var(--fs-error); + --color-error-fg: var(--fs-error-fg); + + --color-info: var(--fs-info); + --color-info-fg: var(--fs-info-fg); + + --radius-sm: var(--fs-radius-sm); + --radius-md: var(--fs-radius-md); + --radius-lg: var(--fs-radius-lg); + --radius-xl: var(--fs-radius-xl); + + --font-display: var(--fs-font-display); + --font-sans: var(--fs-font-body); + --font-mono: var(--fs-font-mono); +} + +/* Tailwind 3's preflight defaults that 4 changed (#5021). Kept so the upgrade + * changes nothing on screen; each can go once the elements relying on it name + * the value themselves. + * - A border with no colour utility was gray-200; 4 makes it currentcolor. + * - Placeholder text was gray-400; 4 makes it the text colour at 50%. + * - Buttons had a pointer cursor; 4 leaves the browser default. */ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + + input::placeholder, + textarea::placeholder { + color: var(--color-gray-400); + } + + button:not(:disabled), + [role='button']:not(:disabled) { + cursor: pointer; + } +} :root { color-scheme: dark; diff --git a/web/src/lib/components/AcoustIDCard.svelte b/web/src/lib/components/AcoustIDCard.svelte index 3de30bb6..25682242 100644 --- a/web/src/lib/components/AcoustIDCard.svelte +++ b/web/src/lib/components/AcoustIDCard.svelte @@ -125,7 +125,7 @@ const rowClass = 'flex items-center justify-between gap-4 py-2'; const inputClass = 'rounded-md border border-border bg-background px-3 py-1.5 text-sm text-text-primary ' + - 'placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent'; + 'placeholder:text-text-muted focus:outline-hidden focus:ring-2 focus:ring-accent';
@@ -238,7 +238,7 @@ diff --git a/web/src/lib/components/QuickFilter.svelte b/web/src/lib/components/QuickFilter.svelte index 6ffe640e..55b4594e 100644 --- a/web/src/lib/components/QuickFilter.svelte +++ b/web/src/lib/components/QuickFilter.svelte @@ -58,7 +58,7 @@ oninput={onInput} onkeydown={onKey} class="w-full rounded border border-border bg-surface pl-7 pr-7 py-1 text-sm - focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent" + focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent" /> {#if raw}