From 8dce395da01ead2ff531ac6645313893776aa1de Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 20 Jul 2026 22:59:32 +0200 Subject: [PATCH 01/98] feat(ux): Change rating badge position --- components/media/card/MediaCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index da80f3e4..8ed64fdd 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -156,7 +156,7 @@ export function MediaCard({ {!hideRating && (
From dee5d8817e1e33016b0d775fb0f4ecd94e093f60 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 00:27:44 +0200 Subject: [PATCH 02/98] feat(ui): masque le badge de note pour les titres sans note (upcomings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un titre non encore sorti — ou obscur — a vote_average = 0, ce qui affichait un badge doré « N/A » à étoile. Trompeur : il suggère une note là où il n'y en a pas. Le badge n'est désormais rendu que si vote_average > 0, aussi bien sur les cartes (MediaCard) que sur la bannière détail (MediaBanner). Décision prise côté serveur à partir de la donnée, donc correcte dès le premier rendu — pas de dépendance à l'hydratation client (une tentative via useIsUnreleased ne mettait pas à jour le badge dans le shell prérendu par Cache Components). Vérifié dans le navigateur sur un build standalone de production : « The End of Oak Street » (sortie août 2026, note 0) n'affiche plus de badge ; « Fight Club » affiche toujours 8.4. --- components/media/card/MediaCard.tsx | 11 +++++++---- components/media/detail/MediaBanner.tsx | 10 +++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index 8ed64fdd..567ad98d 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -53,6 +53,11 @@ export function MediaCard({ const { t, lang } = useTranslation(); const locale = getLocale(lang); + // Une note absente (upcoming, titre obscur) affichait un « N/A » doré à étoile, qui + // suggère une note inexistante. On masque le badge : décidé côté serveur, donc affiché + // correctement dès le premier rendu sans dépendre de l'hydratation. + const showRatingBadge = !hideRating && media.vote_average > 0; + const href = localizedHref(lang, getMediaHref(media)); const isWatched = watchlistEntry?.status === 'watched'; const badgeEntry = watchlistEntry ?? media.watchlistEntry; @@ -153,7 +158,7 @@ export function MediaCard({
{action}
)} - {!hideRating && ( + {showRatingBadge && (
diff --git a/components/media/detail/MediaBanner.tsx b/components/media/detail/MediaBanner.tsx index 62ccfb78..aeac1308 100644 --- a/components/media/detail/MediaBanner.tsx +++ b/components/media/detail/MediaBanner.tsx @@ -100,13 +100,9 @@ export function MediaBanner({ )}
- 0 - ? voteAverage.toFixed(1) - : t.movie.notRated - } - /> + {voteAverage && voteAverage > 0 ? ( + + ) : null} {communityBadge} From b020ad2eb49e433d809d83138783c106efee9c16 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 01:02:01 +0200 Subject: [PATCH 03/98] =?UTF-8?q?fix(ui):=20radius=20GlowBorder=20pilot?= =?UTF-8?q?=C3=A9=20par=20token=20+=20mat=C3=A9riau=20glass-popover=20lisi?= =?UTF-8?q?ble=20sur=20tout=20contenu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/globals.css | 11 +++++++++++ components/dashboard/DashboardHero.tsx | 2 +- components/effects/GlowBorder.tsx | 10 +++++----- components/explorer/SpotlightPick.tsx | 2 +- components/home/CTASection.tsx | 2 +- components/home/HeroSection.tsx | 2 +- components/ui/dropdown-menu.tsx | 4 ++-- components/ui/popover.tsx | 2 +- 8 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/globals.css b/app/globals.css index 80e349f7..cc81653e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -449,6 +449,17 @@ a { backdrop-filter: var(--glass-filter); } + /* Floating menus/popovers portaled over arbitrary content (posters, heroes) — + near-opaque so text stays readable whatever sits behind, in both themes. */ + .glass-popover { + --glass-filter: blur(40px) saturate(150%); + background-color: color-mix(in srgb, var(--surface) 85%, transparent); + -webkit-backdrop-filter: var(--glass-filter); + backdrop-filter: var(--glass-filter); + border: 1px solid var(--border); + border-top-color: var(--glass-border-hover); + } + /* Glass layered over media / dark backdrops (cards, buttons on posters). */ .glass-overlay { --glass-filter: blur(40px) saturate(150%); diff --git a/components/dashboard/DashboardHero.tsx b/components/dashboard/DashboardHero.tsx index c5c83906..02193c96 100644 --- a/components/dashboard/DashboardHero.tsx +++ b/components/dashboard/DashboardHero.tsx @@ -115,7 +115,7 @@ export async function DashboardHero({ )}
- + {cta} diff --git a/components/effects/GlowBorder.tsx b/components/effects/GlowBorder.tsx index 74608333..a09cab65 100644 --- a/components/effects/GlowBorder.tsx +++ b/components/effects/GlowBorder.tsx @@ -3,16 +3,16 @@ import { cn } from '@/lib/utils'; interface GlowBorderProps { children: ReactNode; - radius?: number; + radius?: string; pad?: number; run?: boolean; className?: string; } -/** Wraps content with an animated rotating light-sheen border, like a specular reflection. */ +/** Wraps content with an animated rotating light-sheen border; pass the child's radius token so the sheen stays concentric with its corners. */ export function GlowBorder({ children, - radius = 16, + radius = 'var(--radius-xl)', pad = 1.5, run = true, className, @@ -20,7 +20,7 @@ export function GlowBorder({ return (
diff --git a/components/explorer/SpotlightPick.tsx b/components/explorer/SpotlightPick.tsx index a5bbfb30..ecb87b85 100644 --- a/components/explorer/SpotlightPick.tsx +++ b/components/explorer/SpotlightPick.tsx @@ -27,7 +27,7 @@ export async function SpotlightPick({ return (
- + - + {t.home.cta.button} diff --git a/components/home/HeroSection.tsx b/components/home/HeroSection.tsx index f8acd565..890050ec 100644 --- a/components/home/HeroSection.tsx +++ b/components/home/HeroSection.tsx @@ -69,7 +69,7 @@ export default async function HeroSection({ href={localizedHref(lang, '/signup')} className="group/cta focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-2xl" > - + {t.hero.cta} diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index 6afa434f..adde2e5f 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -45,7 +45,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - 'glass-surface text-text data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-search-dropdown', + 'glass-popover text-text data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-search-dropdown', className )} {...props} @@ -238,7 +238,7 @@ function DropdownMenuSubContent({ Date: Sat, 25 Jul 2026 02:41:47 +0200 Subject: [PATCH 04/98] =?UTF-8?q?refactor(ui):=20=C3=A9chelle=20radius=20m?= =?UTF-8?q?onotone=20en=20tokens=20+=20glass-popover=20sur=20SearchDropdow?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fin du doublon xl/2xl : sm 4 · md 8 · lg 12 · xl 16 · 2xl 24, le 3xl disparaît (rounded-2xl→xl, rounded-3xl→2xl — zéro changement visuel) - Tokens sémantiques --radius-banner (22px) et --radius-app-icon (22.5%) remplacent les derniers rounded-[Npx] arbitraires (BottomTabBar, SpotlightPick, icône PWA) - TiltCard prend un radius token string comme GlowBorder, plus jamais un nombre - SearchDropdown passe sur glass-popover : lisible sur tout contenu, dans les 2 thèmes --- app/[lang]/(protected)/explorer/page.tsx | 2 +- app/globals.css | 3 +++ components/dashboard/BentoStats.tsx | 2 +- components/dashboard/DashboardHero.tsx | 4 ++-- components/dashboard/DashboardSkeletons.tsx | 4 ++-- components/effects/TiltCard.tsx | 4 ++-- components/explorer/SpotlightPick.tsx | 4 ++-- components/home/CTASection.tsx | 10 +++++----- components/home/FeatureCard.tsx | 4 ++-- components/home/HeroSection.tsx | 8 ++++---- components/library/LibraryTabs.tsx | 2 +- components/navigation/BottomTabBar.tsx | 2 +- components/profile/PlaylistGrid.tsx | 2 +- components/profile/ProfileHero.tsx | 2 +- components/profile/ProfileHeroSkeleton.tsx | 2 +- components/search/SearchDropdown.tsx | 6 +++--- components/shared/PWAInstallPrompt.tsx | 2 +- 17 files changed, 33 insertions(+), 30 deletions(-) diff --git a/app/[lang]/(protected)/explorer/page.tsx b/app/[lang]/(protected)/explorer/page.tsx index c46f9fc0..2324d48a 100644 --- a/app/[lang]/(protected)/explorer/page.tsx +++ b/app/[lang]/(protected)/explorer/page.tsx @@ -176,7 +176,7 @@ async function UpcomingSection({ function SpotlightTrendingSkeleton() { return ( <> - + ); diff --git a/app/globals.css b/app/globals.css index cc81653e..bfbbe4fe 100644 --- a/app/globals.css +++ b/app/globals.css @@ -44,9 +44,12 @@ --radius-md: 0.5rem; --radius-lg: 0.75rem; --radius-xl: 1rem; + --radius-2xl: 1.5rem; --radius-full: 9999px; --radius-poster: 0.375rem; --radius-cinema: 0.5rem; + --radius-banner: 1.375rem; + --radius-app-icon: 22.5%; --shadow-card: var(--shadow-card); --shadow-card-sm: var(--shadow-card-sm); diff --git a/components/dashboard/BentoStats.tsx b/components/dashboard/BentoStats.tsx index 6f6f7b14..6f548f82 100644 --- a/components/dashboard/BentoStats.tsx +++ b/components/dashboard/BentoStats.tsx @@ -31,7 +31,7 @@ export function BentoStats({ {cells.map(({ icon: Icon, value, label }, i) => (
{i === 0 && }
diff --git a/components/dashboard/DashboardHero.tsx b/components/dashboard/DashboardHero.tsx index 02193c96..3206f695 100644 --- a/components/dashboard/DashboardHero.tsx +++ b/components/dashboard/DashboardHero.tsx @@ -43,10 +43,10 @@ export async function DashboardHero({ return (
- + {item.backdropPath ? ( ; + return ; } export function BentoStatsSkeleton() { return (
{Array.from({ length: 3 }).map((_, i) => ( - + ))}
); diff --git a/components/effects/TiltCard.tsx b/components/effects/TiltCard.tsx index b7b1eb53..980fb52e 100644 --- a/components/effects/TiltCard.tsx +++ b/components/effects/TiltCard.tsx @@ -6,7 +6,7 @@ import { cn } from '@/lib/utils'; interface TiltCardProps { children: ReactNode; max?: number; - radius?: number; + radius?: string; glow?: boolean; className?: string; onClick?: () => void; @@ -31,7 +31,7 @@ const GLOW_SIZE = 360; export function TiltCard({ children, max = 10, - radius = 14, + radius = 'var(--radius-xl)', glow = true, className, onClick, diff --git a/components/explorer/SpotlightPick.tsx b/components/explorer/SpotlightPick.tsx index ecb87b85..ec3d4690 100644 --- a/components/explorer/SpotlightPick.tsx +++ b/components/explorer/SpotlightPick.tsx @@ -27,10 +27,10 @@ export async function SpotlightPick({ return (
- + {item.backdrop_path ? ( -
+
@@ -28,10 +28,10 @@ export default async function CTASection({ lang }: { lang: Language }) {
- - + + {t.home.cta.button} @@ -40,7 +40,7 @@ export default async function CTASection({ lang }: { lang: Language }) { {t.home.cta.alreadyHave} diff --git a/components/home/FeatureCard.tsx b/components/home/FeatureCard.tsx index e8717bdb..c438e96e 100644 --- a/components/home/FeatureCard.tsx +++ b/components/home/FeatureCard.tsx @@ -6,8 +6,8 @@ export default function FeatureCard({ description, }: FeatureCardProps) { return ( -
-
+
+
{icon}

diff --git a/components/home/HeroSection.tsx b/components/home/HeroSection.tsx index 890050ec..6e87285f 100644 --- a/components/home/HeroSection.tsx +++ b/components/home/HeroSection.tsx @@ -67,10 +67,10 @@ export default async function HeroSection({ > - - + + {t.hero.cta} @@ -79,7 +79,7 @@ export default async function HeroSection({ {t.hero.login} diff --git a/components/library/LibraryTabs.tsx b/components/library/LibraryTabs.tsx index eb63b8b1..515f95ed 100644 --- a/components/library/LibraryTabs.tsx +++ b/components/library/LibraryTabs.tsx @@ -136,7 +136,7 @@ export function LibraryTabs({ return (
diff --git a/components/navigation/BottomTabBar.tsx b/components/navigation/BottomTabBar.tsx index f6a55be2..0d446538 100644 --- a/components/navigation/BottomTabBar.tsx +++ b/components/navigation/BottomTabBar.tsx @@ -18,7 +18,7 @@ export function BottomTabBar({ username }: BottomTabBarProps) { aria-hidden="true" className="pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-linear-to-t from-background/60 to-transparent" /> -
+
{tabs.map(({ href, label, icon: Icon, active }) => ( -
+
diff --git a/components/profile/ProfileHero.tsx b/components/profile/ProfileHero.tsx index a58f8647..01e6bdc5 100644 --- a/components/profile/ProfileHero.tsx +++ b/components/profile/ProfileHero.tsx @@ -126,7 +126,7 @@ export function ProfileHero({ }>; return ( -
+
diff --git a/components/profile/ProfileHeroSkeleton.tsx b/components/profile/ProfileHeroSkeleton.tsx index 2ab23c67..9b09e086 100644 --- a/components/profile/ProfileHeroSkeleton.tsx +++ b/components/profile/ProfileHeroSkeleton.tsx @@ -2,7 +2,7 @@ import { Skeleton } from '@/components/ui/skeleton'; export function ProfileHeroSkeleton() { return ( -
+
diff --git a/components/search/SearchDropdown.tsx b/components/search/SearchDropdown.tsx index afed56d5..f63eb20e 100644 --- a/components/search/SearchDropdown.tsx +++ b/components/search/SearchDropdown.tsx @@ -47,7 +47,7 @@ export function SearchDropdown({ if (query.length >= 2 && !isLoading && isEmpty) { return (
@@ -64,7 +64,7 @@ export function SearchDropdown({ if (isUserSearch) { return ( -
+
{users.length} {t.pages.search.usersFound} @@ -147,7 +147,7 @@ export function SearchDropdown({ } return ( -
+
{t.pages.search.suggestionsCount.replace( '${count}', diff --git a/components/shared/PWAInstallPrompt.tsx b/components/shared/PWAInstallPrompt.tsx index 253f7973..f094d05c 100644 --- a/components/shared/PWAInstallPrompt.tsx +++ b/components/shared/PWAInstallPrompt.tsx @@ -39,7 +39,7 @@ export function PWAInstallPrompt() { alt="ReelMark" width={44} height={44} - className="rounded-[10px] shrink-0" + className="rounded-(--radius-app-icon) shrink-0" />
From 64932b109fa3f38af856902a35f831827c18a9a4 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 02:42:36 +0200 Subject: [PATCH 05/98] =?UTF-8?q?chore(ui):=20supprime=20le=20Tooltip=20sa?= =?UTF-8?q?ns=20consommateur=20et=20sa=20d=C3=A9pendance=20Radix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tooltip.tsx référençait des tokens inexistants (bg-foreground) et n'était monté nulle part hors du provider - TooltipProvider retiré de app/providers.tsx, @radix-ui/react-tooltip désinstallé --- app/providers.tsx | 3 +- components/ui/tooltip.tsx | 57 ---------------------------------- package.json | 1 - pnpm-lock.yaml | 64 --------------------------------------- 4 files changed, 1 insertion(+), 124 deletions(-) delete mode 100644 components/ui/tooltip.tsx diff --git a/app/providers.tsx b/app/providers.tsx index fac4afe8..59e0badb 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -2,7 +2,6 @@ import { type ReactNode } from 'react'; import { LanguageProvider } from '@/lib/i18n/context'; -import { TooltipProvider } from '@/components/ui/tooltip'; import type { Language } from '@/lib/i18n/translations'; interface ProvidersProps { @@ -13,7 +12,7 @@ interface ProvidersProps { export function Providers({ children, initialLang }: ProvidersProps) { return ( - {children} + {children} ); } diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx deleted file mode 100644 index 69a06c5a..00000000 --- a/components/ui/tooltip.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as TooltipPrimitive from '@radix-ui/react-tooltip'; - -import { cn } from '@/lib/utils'; - -function TooltipProvider({ - delayDuration = 0, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function Tooltip({ - ...props -}: React.ComponentProps) { - return ; -} - -function TooltipTrigger({ - ...props -}: React.ComponentProps) { - return ; -} - -function TooltipContent({ - className, - sideOffset = 0, - children, - ...props -}: React.ComponentProps) { - return ( - - - {children} - - - - ); -} - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/package.json b/package.json index 3eeafe47..10e4888b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@radix-ui/react-separator": "^1.1.11", "@radix-ui/react-slot": "^1.3.0", "@radix-ui/react-switch": "^1.3.2", - "@radix-ui/react-tooltip": "^1.2.11", "@sentry/nextjs": "^10.63.0", "@serwist/next": "^9.5.11", "@supabase/ssr": "^0.10.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0ab4808..0a8b897b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,9 +31,6 @@ importers: '@radix-ui/react-switch': specifier: ^1.3.2 version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-tooltip': - specifier: ^1.2.11 - version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@sentry/nextjs': specifier: ^10.63.0 version: 10.66.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@8.1.1)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19)) @@ -1267,22 +1264,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tooltip@1.2.12': - resolution: - { - integrity: sha512-U3HoftgWnmla78vzQbLvKKb7bUYJxoiiqYFzp1wu/TBMyDqMZSuCl3aRICsD6EfVEwcJD2mumGDGUXLFVqQHKA==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-use-callback-ref@1.1.2': resolution: { @@ -1379,22 +1360,6 @@ packages: '@types/react': optional: true - '@radix-ui/react-visually-hidden@1.2.7': - resolution: - { - integrity: sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/rect@1.1.2': resolution: { @@ -6781,26 +6746,6 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-tooltip@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.5 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 @@ -6854,15 +6799,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-visually-hidden@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/rect@1.1.2': {} '@rolldown/binding-android-arm64@1.1.5': From ea2657669ab57ae2ebb440befe91b0fdedd3a130 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 02:43:03 +0200 Subject: [PATCH 06/98] =?UTF-8?q?fix(net):=20timeouts,=20erreurs=20r=C3=A9?= =?UTF-8?q?seau=20en=20data=20et=20cooldown=20quota=20Watchmode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TMDB : timeout 10s, erreurs réseau (DNS/reset/hang) retournées en data au lieu de traverser la frontière use cache et tuer le rendu ; 404 d'ids morts cachés 1h - Watchmode : timeout 8s + cooldown global 5 min après 429/401/402 — plus un seul appel dépensé pendant l'épuisement du quota mensuel - iTunes lookup (icônes providers) : timeout 5s - lib/monotonic-now.ts : horloge monotone pour les durées — Date.now() pendant un prerender Cache Components avorte le shell en 500 --- lib/monotonic-now.ts | 14 ++++++++++ lib/tmdb/client.ts | 57 +++++++++++++++++++++++++++++--------- lib/watchmode/app-store.ts | 2 +- lib/watchmode/client.ts | 24 +++++++++++++++- 4 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 lib/monotonic-now.ts diff --git a/lib/monotonic-now.ts b/lib/monotonic-now.ts new file mode 100644 index 00000000..17d135a5 --- /dev/null +++ b/lib/monotonic-now.ts @@ -0,0 +1,14 @@ +/** + * Milliseconds from a monotonic clock, safe inside prerenders where Next forbids + * wall-clock reads (`Date.now()` aborts a Cache Components shell generation). + * Only meaningful for measuring durations — never for dates. + */ +export function monotonicNowMs(): number { + if ( + typeof process !== 'undefined' && + typeof process.hrtime?.bigint === 'function' + ) { + return Number(process.hrtime.bigint() / BigInt(1_000_000)); + } + return Date.now(); +} diff --git a/lib/tmdb/client.ts b/lib/tmdb/client.ts index a2bcaea8..658a223b 100644 --- a/lib/tmdb/client.ts +++ b/lib/tmdb/client.ts @@ -57,14 +57,21 @@ export async function getImageLanguageFilter(lang?: Language): Promise { type TMDBResult = { ok: true; data: T } | { ok: false; status: number; statusText: string }; -const FAILED_LOOKUP_CACHE = { stale: 0, revalidate: 60, expire: 300 }; +const TRANSIENT_FAILURE_CACHE = { stale: 0, revalidate: 60, expire: 300 }; +const MISSING_ENTRY_CACHE = { stale: 0, revalidate: 3600, expire: 86400 }; +const TMDB_TIMEOUT_MS = 10_000; + +function retryDelay(attempt: number): Promise { + return new Promise((resolve) => setTimeout(resolve, 300 * (attempt + 1))); +} /** - * Cached TMDB call. **Never throws on an HTTP failure**: an exception crossing a - * `"use cache"` boundary comes back digested, from the `Cache` environment, and takes down - * the surrounding render even when the caller has a try/catch. A TMDB 404 is routine — a - * deleted title, a recommendations endpoint with no entry — so failures are returned as - * data and turned back into an exception by `fetchTMDB`, outside the cache scope. + * Cached TMDB call. **Never throws**: an exception crossing a `"use cache"` boundary + * comes back digested, from the `Cache` environment, and takes down the surrounding + * render even when the caller has a try/catch. HTTP failures (a 404 is routine — deleted + * title, recommendations endpoint with no entry) AND network errors (timeout, DNS, reset) + * are returned as data and turned back into an exception by `fetchTMDB`, outside the + * cache scope. Dead 404s stay cached longer than transient failures. */ async function fetchTMDBUrl( url: string, @@ -75,9 +82,25 @@ async function fetchTMDBUrl( cacheLife({ stale: 300, revalidate, expire: revalidate * 24 }); for (let attempt = 0; ; attempt++) { - const response = await fetch(url, { - headers: { Authorization: `Bearer ${TMDB_READ_ACCESS_TOKEN}` }, - }); + let response: Response; + try { + response = await fetch(url, { + headers: { Authorization: `Bearer ${TMDB_READ_ACCESS_TOKEN}` }, + signal: AbortSignal.timeout(TMDB_TIMEOUT_MS), + }); + } catch (error) { + if (attempt < retries) { + await retryDelay(attempt); + continue; + } + cacheLife(TRANSIENT_FAILURE_CACHE); + return { + ok: false, + status: 0, + statusText: + error instanceof Error ? error.name : 'NetworkError', + }; + } if (response.ok) return { ok: true, data: (await response.json()) as T }; @@ -85,14 +108,22 @@ async function fetchTMDBUrl( const isRetryable = response.status === 429 || response.status >= 500; if (isRetryable && attempt < retries) { const retryAfter = Number(response.headers.get('retry-after')); - const delayMs = - retryAfter > 0 ? retryAfter * 1000 : 300 * (attempt + 1); - await new Promise((resolve) => setTimeout(resolve, delayMs)); + if (retryAfter > 0) { + await new Promise((resolve) => + setTimeout(resolve, retryAfter * 1000) + ); + } else { + await retryDelay(attempt); + } continue; } // Lowest cacheLife wins, so a failure is not frozen for the success TTL. - cacheLife(FAILED_LOOKUP_CACHE); + cacheLife( + response.status === 404 + ? MISSING_ENTRY_CACHE + : TRANSIENT_FAILURE_CACHE + ); return { ok: false, status: response.status, diff --git a/lib/watchmode/app-store.ts b/lib/watchmode/app-store.ts index a2dd7985..a334d0c8 100644 --- a/lib/watchmode/app-store.ts +++ b/lib/watchmode/app-store.ts @@ -75,7 +75,7 @@ export async function getAppStoreIconMap( const uniqueIds = [...new Set(Object.values(PROVIDER_APP_IDS))]; const res = await fetch( `https://itunes.apple.com/lookup?id=${uniqueIds.join(',')}&entity=software&country=${country.toLowerCase()}`, - { next: { revalidate: 604800 } } + { next: { revalidate: 604800 }, signal: AbortSignal.timeout(5000) } ); if (!res.ok) return new Map(); diff --git a/lib/watchmode/client.ts b/lib/watchmode/client.ts index 6992acd0..aa8fd90e 100644 --- a/lib/watchmode/client.ts +++ b/lib/watchmode/client.ts @@ -1,3 +1,5 @@ +import { monotonicNowMs } from '@/lib/monotonic-now'; + const WATCHMODE_API_KEY = process.env.WATCHMODE_API_KEY; const BASE_URL = 'https://api.watchmode.com/v1'; @@ -36,9 +38,17 @@ export interface WatchmodeRegion { plan_enabled: boolean; } +const WATCHMODE_TIMEOUT_MS = 8_000; +const QUOTA_COOLDOWN_MS = 5 * 60_000; +const QUOTA_STATUSES = new Set([401, 402, 429]); + +let quotaCooldownUntil = 0; + /** * Fetches data from the Watchmode API with Next.js cache. * API key is injected as a query param (server-side only). + * A quota/rate-limit response (401/402/429) opens a 5 min cooldown during which every + * call fails fast without spending a request — the monthly quota is small. * * @param path - API path including query string if needed. * @param revalidate - Cache TTL in seconds. @@ -49,10 +59,22 @@ export async function fetchWatchmode( ): Promise { if (!WATCHMODE_API_KEY) throw new Error('WATCHMODE_API_KEY is not defined'); + if (monotonicNowMs() < quotaCooldownUntil) { + throw new Error('Watchmode API Error: quota cooldown active'); + } + const sep = path.includes('?') ? '&' : '?'; const url = `${BASE_URL}${path}${sep}apiKey=${WATCHMODE_API_KEY}`; - const res = await fetch(url, { next: { revalidate } }); + const res = await fetch(url, { + next: { revalidate }, + signal: AbortSignal.timeout(WATCHMODE_TIMEOUT_MS), + }); + + if (QUOTA_STATUSES.has(res.status)) { + quotaCooldownUntil = monotonicNowMs() + QUOTA_COOLDOWN_MS; + } + if (!res.ok) throw new Error(`Watchmode API Error: ${res.status} ${res.statusText}`); return res.json(); From 228ee39dcef7514dfc28bfdeca4bdf5e163dd6cf Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 02:44:52 +0200 Subject: [PATCH 07/98] =?UTF-8?q?feat(observability):=20remont=C3=A9e=20Bu?= =?UTF-8?q?gsink=20compl=C3=A8te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reportSwallowed envoie chaque fallback avalé à Bugsink en warning, dédupliqué 5 min par label+message (horloge monotone, safe en prerender) - ErrorCard capture les erreurs client sans digest (les erreurs serveur arrivent déjà via onRequestError) — les 12 error.tsx transmettent désormais l'objet error - global-error.tsx capture les crashs de shell --- .../(protected)/explorer/search/error.tsx | 3 +- .../(protected)/notifications/error.tsx | 3 +- .../(protected)/profile/[username]/error.tsx | 3 +- app/[lang]/collection/[id]/error.tsx | 3 +- app/[lang]/crew/[id]/error.tsx | 3 +- app/[lang]/error.tsx | 3 +- app/[lang]/movie/[id]/error.tsx | 3 +- app/[lang]/movie/[id]/similar/error.tsx | 3 +- app/[lang]/playlist/[id]/error.tsx | 3 +- app/[lang]/tv/[id]/error.tsx | 3 +- .../tv/[id]/season/[seasonNumber]/error.tsx | 3 +- app/[lang]/tv/[id]/similar/error.tsx | 3 +- app/global-error.tsx | 8 ++++- components/ui/ErrorCard.tsx | 14 +++++++- lib/report.ts | 33 ++++++++++++++++--- 15 files changed, 73 insertions(+), 18 deletions(-) diff --git a/app/[lang]/(protected)/explorer/search/error.tsx b/app/[lang]/(protected)/explorer/search/error.tsx index 6274cdb5..6a684693 100644 --- a/app/[lang]/(protected)/explorer/search/error.tsx +++ b/app/[lang]/(protected)/explorer/search/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function SearchError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/(protected)/notifications/error.tsx b/app/[lang]/(protected)/notifications/error.tsx index 1c2c46d0..14c80f09 100644 --- a/app/[lang]/(protected)/notifications/error.tsx +++ b/app/[lang]/(protected)/notifications/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function NotificationsError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/(protected)/profile/[username]/error.tsx b/app/[lang]/(protected)/profile/[username]/error.tsx index 09d65db7..d4a8b6e3 100644 --- a/app/[lang]/(protected)/profile/[username]/error.tsx +++ b/app/[lang]/(protected)/profile/[username]/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function ProfileError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/collection/[id]/error.tsx b/app/[lang]/collection/[id]/error.tsx index 6133a035..b2307a77 100644 --- a/app/[lang]/collection/[id]/error.tsx +++ b/app/[lang]/collection/[id]/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function CollectionError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/crew/[id]/error.tsx b/app/[lang]/crew/[id]/error.tsx index 269d7ce2..0a23e947 100644 --- a/app/[lang]/crew/[id]/error.tsx +++ b/app/[lang]/crew/[id]/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function CrewError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/error.tsx b/app/[lang]/error.tsx index 80653ced..00a2212a 100644 --- a/app/[lang]/error.tsx +++ b/app/[lang]/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function GlobalError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/movie/[id]/error.tsx b/app/[lang]/movie/[id]/error.tsx index 0049263a..30f745c0 100644 --- a/app/[lang]/movie/[id]/error.tsx +++ b/app/[lang]/movie/[id]/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function MovieError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/movie/[id]/similar/error.tsx b/app/[lang]/movie/[id]/similar/error.tsx index 5029aa2d..6ef547a0 100644 --- a/app/[lang]/movie/[id]/similar/error.tsx +++ b/app/[lang]/movie/[id]/similar/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function SimilarMoviesError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/playlist/[id]/error.tsx b/app/[lang]/playlist/[id]/error.tsx index 66c72945..43db482a 100644 --- a/app/[lang]/playlist/[id]/error.tsx +++ b/app/[lang]/playlist/[id]/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function PlaylistError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/tv/[id]/error.tsx b/app/[lang]/tv/[id]/error.tsx index 26679613..1825212f 100644 --- a/app/[lang]/tv/[id]/error.tsx +++ b/app/[lang]/tv/[id]/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function TvShowError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/tv/[id]/season/[seasonNumber]/error.tsx b/app/[lang]/tv/[id]/season/[seasonNumber]/error.tsx index c8d3c21e..43a3d939 100644 --- a/app/[lang]/tv/[id]/season/[seasonNumber]/error.tsx +++ b/app/[lang]/tv/[id]/season/[seasonNumber]/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function SeasonError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/[lang]/tv/[id]/similar/error.tsx b/app/[lang]/tv/[id]/similar/error.tsx index 921ee0a4..c49df658 100644 --- a/app/[lang]/tv/[id]/similar/error.tsx +++ b/app/[lang]/tv/[id]/similar/error.tsx @@ -3,10 +3,11 @@ import { ErrorCard } from '@/components/ui/ErrorCard'; export default function SimilarTvShowsError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + return ; } diff --git a/app/global-error.tsx b/app/global-error.tsx index 7f856036..868b9599 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -1,8 +1,9 @@ 'use client'; import './globals.css'; +import * as Sentry from '@sentry/nextjs'; import { translations, type Language } from '@/lib/i18n/translations'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Inter, Bebas_Neue } from 'next/font/google'; const sans = Inter({ @@ -19,11 +20,16 @@ const display = Bebas_Neue({ }); export default function GlobalError({ + error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + const [lang] = useState(() => { if (typeof document === 'undefined') return 'en'; const match = document.cookie.match( diff --git a/components/ui/ErrorCard.tsx b/components/ui/ErrorCard.tsx index 5fd84463..41f67e2c 100644 --- a/components/ui/ErrorCard.tsx +++ b/components/ui/ErrorCard.tsx @@ -1,5 +1,7 @@ 'use client'; +import { useEffect } from 'react'; +import * as Sentry from '@sentry/nextjs'; import { AlertTriangle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import Link from 'next/link'; @@ -7,18 +9,28 @@ import { useTranslation } from '@/lib/i18n/context'; import type { LucideIcon } from 'lucide-react'; interface ErrorCardProps { + error?: Error & { digest?: string }; reset: () => void; icon?: LucideIcon; backHref?: '/explorer' | '/dashboard' | null; } -/** Centered glass error card with retry + optional back link. */ +/** + * Centered glass error card with retry + optional back link. Client-only errors + * (no digest) are reported to Sentry here — server errors already arrive via + * `onRequestError` and would be duplicated. + */ export function ErrorCard({ + error, reset, icon: Icon = AlertTriangle, backHref = '/explorer', }: ErrorCardProps) { const { t } = useTranslation(); + + useEffect(() => { + if (error && !error.digest) Sentry.captureException(error); + }, [error]); const backLabel = backHref === '/dashboard' ? t.common.errorBackHome diff --git a/lib/report.ts b/lib/report.ts index b6b02c8a..97adf962 100644 --- a/lib/report.ts +++ b/lib/report.ts @@ -1,15 +1,40 @@ import * as Sentry from '@sentry/nextjs'; import { unstable_rethrow } from 'next/navigation'; +import { monotonicNowMs } from '@/lib/monotonic-now'; + +const FORWARD_DEDUP_MS = 5 * 60_000; +const lastForwardedAt = new Map(); + +function shouldForward(key: string): boolean { + const now = monotonicNowMs(); + const last = lastForwardedAt.get(key); + if (last !== undefined && now - last < FORWARD_DEDUP_MS) return false; + if (lastForwardedAt.size > 500) { + for (const [k, t] of lastForwardedAt) { + if (now - t > FORWARD_DEDUP_MS) lastForwardedAt.delete(k); + } + } + lastForwardedAt.set(key, now); + return true; +} /** - * Logs a swallowed fallback error with a stable tag so degraded paths stay visible in - * production logs. Next's own control-flow signals (`redirect`, `notFound`, and the halt - * that stops a Cache Components prerender) are rethrown instead — swallowing those would - * turn a redirect into a blank section. + * Logs a swallowed fallback error with a stable tag and forwards it to Sentry as a + * warning, deduped per label+message for 5 min so routine failures (dead TMDB ids, + * quota cooldowns) surface without flooding. Next's own control-flow signals + * (`redirect`, `notFound`, and the halt that stops a Cache Components prerender) are + * rethrown instead — swallowing those would turn a redirect into a blank section. */ export function reportSwallowed(label: string, error: unknown): void { unstable_rethrow(error); console.warn(`[${label}]`, error); + + const message = error instanceof Error ? error.message : String(error); + if (!shouldForward(`${label}:${message}`)) return; + Sentry.captureException( + error instanceof Error ? error : new Error(`[${label}] ${message}`), + { level: 'warning', tags: { label } } + ); } /** Logs and reports to Sentry an error on a critical path (data import/export, watchlist sync). */ From 857ee9469cc3884a33502cf180e858682f7b2373 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 02:45:40 +0200 Subject: [PATCH 08/98] =?UTF-8?q?fix(data):=20compteurs=20et=20listes=20tr?= =?UTF-8?q?onqu=C3=A9s=20au=20cap=20PostgREST=20de=201000=20lignes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Le profil chargeait la watchlist avec .limit(1000) : au-delà, compteurs et grilles faux (2074 entrées réelles → 299/699 affichés) ; passage sur fetchAllRows, vérifié en rendu réel : 471 à voir / 1601 vus, conformes au SQL - Même correction : épisodes du continue watching, progression d'une série, notes utilisateur, amis, demandes en attente, playlists (caps 100 arbitraires) - Le badge Avis affiche un vrai count SQL au lieu de la taille de la première page --- .../(protected)/profile/[username]/page.tsx | 27 ++++++++++---- app/actions/continue-watching.ts | 25 ++++++++----- app/actions/episodes.ts | 18 ++++++--- app/actions/friends.ts | 37 +++++++++++-------- app/actions/playlists.ts | 19 ++++++---- app/actions/reviews.ts | 26 ++++++++----- components/profile/ProfileTabs.tsx | 4 +- 7 files changed, 100 insertions(+), 56 deletions(-) diff --git a/app/[lang]/(protected)/profile/[username]/page.tsx b/app/[lang]/(protected)/profile/[username]/page.tsx index cc357796..4ed86674 100644 --- a/app/[lang]/(protected)/profile/[username]/page.tsx +++ b/app/[lang]/(protected)/profile/[username]/page.tsx @@ -27,6 +27,7 @@ import { getGenres } from '@/lib/tmdb'; import { ListMetadataBackfill } from '@/components/library/ListMetadataBackfill'; import type { WatchlistEntry } from '@/types/tmdb'; import { WATCHLIST_COLUMNS } from '@/lib/supabase/columns'; +import { fetchAllRows } from '@/lib/supabase/pagination'; interface Props { params: Promise<{ lang: Language; username: string }>; @@ -89,6 +90,7 @@ async function ProfileTabsSection({ const [ privacy, reviewsPage, + reviewsCountData, playlists, allFriendEntries, watchlistData, @@ -98,14 +100,21 @@ async function ProfileTabsSection({ ] = await Promise.all([ getPrivacySettings(profileUserId), getUserReviews(profileUserId), + supabase + .from('reviews') + .select('id', { count: 'exact', head: true }) + .eq('user_id', profileUserId), getUserPlaylists(profileUserId), getFriendsWithProfiles(profileUserId), - supabase - .from('watchlist') - .select(WATCHLIST_COLUMNS) - .eq('user_id', profileUserId) - .order('created_at', { ascending: false }) - .limit(1000), + fetchAllRows((from, to) => + supabase + .from('watchlist') + .select(WATCHLIST_COLUMNS) + .eq('user_id', profileUserId) + .order('created_at', { ascending: false }) + .order('id') + .range(from, to) + ), isOwnProfile ? getPendingRequestsWithProfiles() : Promise.resolve([]), getGenres(lang), isOwnProfile @@ -113,7 +122,7 @@ async function ProfileTabsSection({ : Promise.resolve>({}), ]); - const watchlist = (watchlistData.data ?? []) as WatchlistEntry[]; + const watchlist = watchlistData as WatchlistEntry[]; function canView(visibility: string): boolean { if (isOwnProfile) return true; @@ -132,6 +141,9 @@ async function ProfileTabsSection({ const filteredReviews = canView(privacy.reviews_visibility) ? reviewsPage.reviews : []; + const reviewsCount = canView(privacy.reviews_visibility) + ? (reviewsCountData.count ?? 0) + : 0; const initialReviewsCursor = canView(privacy.reviews_visibility) ? reviewsPage.nextCursor : null; @@ -146,6 +158,7 @@ async function ProfileTabsSection({ toWatch={toWatch} watched={watched} reviews={filteredReviews} + reviewsCount={reviewsCount} initialReviewsCursor={initialReviewsCursor} profileUserId={profileUserId} playlists={playlists} diff --git a/app/actions/continue-watching.ts b/app/actions/continue-watching.ts index 054e36fe..e5ea06c1 100644 --- a/app/actions/continue-watching.ts +++ b/app/actions/continue-watching.ts @@ -2,6 +2,7 @@ import { getOptionalUser } from '@/lib/supabase/auth-helpers'; import { getWatchlistWithProgress } from '@/lib/data/watchlist'; +import { fetchAllRows } from '@/lib/supabase/pagination'; import { getSeasonDetails } from '@/lib/tmdb'; import { getCachedTvShowDetails } from '@/lib/tmdb/cached'; import { findNextEpisode, type SeasonEpisodeCount } from '@/lib/next-episode'; @@ -138,17 +139,23 @@ export async function getContinueWatching(): Promise { }) .slice(0, SHOW_LIMIT); - const { data: watches } = await supabase - .from('episode_watches') - .select('tv_id, season_number, episode_number') - .eq('user_id', userId) - .in( - 'tv_id', - selected.map((entry) => entry.media_id) - ); + const watches = await fetchAllRows((from, to) => + supabase + .from('episode_watches') + .select('tv_id, season_number, episode_number') + .eq('user_id', userId) + .in( + 'tv_id', + selected.map((entry) => entry.media_id) + ) + .order('tv_id') + .order('season_number') + .order('episode_number') + .range(from, to) + ); const watchedByShow = new Map(); - for (const watch of watches ?? []) { + for (const watch of watches) { const show = watchedByShow.get(watch.tv_id) ?? new Map(); const episodes = show.get(watch.season_number) ?? new Set(); episodes.add(watch.episode_number); diff --git a/app/actions/episodes.ts b/app/actions/episodes.ts index 63f40ed2..4b1776a8 100644 --- a/app/actions/episodes.ts +++ b/app/actions/episodes.ts @@ -9,6 +9,7 @@ import { revalidateLocalizedAfterResponse, } from '@/app/actions/_helpers'; import { getCachedTvShowDetails } from '@/lib/tmdb/cached'; +import { fetchAllRows } from '@/lib/supabase/pagination'; import { reportCritical, reportSwallowed } from '@/lib/report'; import type { SupabaseServerClient } from '@/lib/supabase/server'; import type { WatchStatus } from '@/types/tmdb'; @@ -394,14 +395,19 @@ export async function getTvShowWatchProgress( if (!userId) return new Map(); - const { data: watches } = await supabase - .from('episode_watches') - .select('season_number') - .eq('user_id', userId) - .eq('tv_id', tvId); + const watches = await fetchAllRows((from, to) => + supabase + .from('episode_watches') + .select('season_number, episode_number') + .eq('user_id', userId) + .eq('tv_id', tvId) + .order('season_number') + .order('episode_number') + .range(from, to) + ); const progress = new Map(); - for (const w of watches ?? []) { + for (const w of watches) { progress.set(w.season_number, (progress.get(w.season_number) ?? 0) + 1); } return progress; diff --git a/app/actions/friends.ts b/app/actions/friends.ts index 9546b125..0d643ee1 100644 --- a/app/actions/friends.ts +++ b/app/actions/friends.ts @@ -4,6 +4,7 @@ import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; import { createAdminClient } from '@/lib/supabase/server'; import { getPrivacySettings } from '@/app/actions/profile'; import { FRIENDSHIP_COLUMNS } from '@/lib/supabase/columns'; +import { fetchAllRows } from '@/lib/supabase/pagination'; import { resolveAvatarUrl } from '@/lib/avatar'; import { revalidateProfileAfterResponse } from '@/app/actions/_helpers'; import { sendFriendPush } from '@/lib/push/notify-friend'; @@ -24,15 +25,17 @@ export async function getPendingRequestsWithProfiles(): Promise< > { const { supabase, userId } = await getAuthenticatedUser(); - const { data: rawPending, error } = await supabase - .from('friendships') - .select(FRIENDSHIP_COLUMNS) - .eq('addressee_id', userId) - .eq('status', 'pending') - .limit(100); + const rawPending = await fetchAllRows((from, to) => + supabase + .from('friendships') + .select(FRIENDSHIP_COLUMNS) + .eq('addressee_id', userId) + .eq('status', 'pending') + .order('id') + .range(from, to) + ); - if (error) throw new Error(error.message); - if (!rawPending || rawPending.length === 0) return []; + if (rawPending.length === 0) return []; const pending = rawPending as Friendship[]; const requesterIds = pending.map((f) => f.requester_id); @@ -81,15 +84,17 @@ export async function getFriendsWithProfiles( const supabase = createAdminClient(); - const { data: rawFriends, error } = await supabase - .from('friendships') - .select(FRIENDSHIP_COLUMNS) - .or(`requester_id.eq.${userId},addressee_id.eq.${userId}`) - .eq('status', 'accepted') - .limit(100); + const rawFriends = await fetchAllRows((from, to) => + supabase + .from('friendships') + .select(FRIENDSHIP_COLUMNS) + .or(`requester_id.eq.${userId},addressee_id.eq.${userId}`) + .eq('status', 'accepted') + .order('id') + .range(from, to) + ); - if (error) throw new Error(error.message); - if (!rawFriends || rawFriends.length === 0) return []; + if (rawFriends.length === 0) return []; const friends = rawFriends as Friendship[]; const friendUserIds = friends.map((f) => diff --git a/app/actions/playlists.ts b/app/actions/playlists.ts index 96b8408f..11e877e9 100644 --- a/app/actions/playlists.ts +++ b/app/actions/playlists.ts @@ -5,6 +5,7 @@ import { getOptionalUser, } from '@/lib/supabase/auth-helpers'; import { getTranslations } from '@/lib/i18n/server'; +import { fetchAllRows } from '@/lib/supabase/pagination'; import { revalidateProfileAfterResponse, revalidatePlaylistMetaAfterResponse, @@ -25,15 +26,17 @@ import type { Playlist, PrivacyVisibility } from '@/types/profile'; export async function getUserPlaylists(userId: string): Promise { const { supabase } = await getAuthenticatedUser(); - const { data, error } = await supabase - .from('playlists') - .select('*, items:playlist_items(*)') - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .limit(100); + const data = await fetchAllRows((from, to) => + supabase + .from('playlists') + .select('*, items:playlist_items(*)') + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .order('id') + .range(from, to) + ); - if (error) throw new Error(error.message); - return (data as Playlist[]) ?? []; + return data as Playlist[]; } /** diff --git a/app/actions/reviews.ts b/app/actions/reviews.ts index 4e04d618..1fd0d364 100644 --- a/app/actions/reviews.ts +++ b/app/actions/reviews.ts @@ -5,6 +5,8 @@ import { getOptionalUser, } from '@/lib/supabase/auth-helpers'; import { createClient } from '@/lib/supabase/server'; +import { fetchAllRows } from '@/lib/supabase/pagination'; +import { reportSwallowed } from '@/lib/report'; import { getTranslations } from '@/lib/i18n/server'; import { validateRating, @@ -77,17 +79,23 @@ export async function getUserReviewRatings( ): Promise> { const supabase = await createClient(); - const { data, error } = await supabase - .from('reviews') - .select('media_id, media_type, rating') - .eq('user_id', userId) - .in('media_type', ['movie', 'tv']) - .not('rating', 'is', null); - - if (error) return {}; + const data = await fetchAllRows((from, to) => + supabase + .from('reviews') + .select('media_id, media_type, rating') + .eq('user_id', userId) + .in('media_type', ['movie', 'tv']) + .not('rating', 'is', null) + .order('media_type') + .order('media_id') + .range(from, to) + ).catch((error: unknown) => { + reportSwallowed('reviews:ratings', error); + return []; + }); const map: Record = {}; - for (const row of data ?? []) { + for (const row of data) { if (row.rating !== null) { map[`${row.media_type}-${row.media_id}`] = row.rating; } diff --git a/components/profile/ProfileTabs.tsx b/components/profile/ProfileTabs.tsx index ceaff580..759200c2 100644 --- a/components/profile/ProfileTabs.tsx +++ b/components/profile/ProfileTabs.tsx @@ -24,6 +24,7 @@ interface ProfileTabsProps { toWatch: WatchlistEntry[]; watched: WatchlistEntry[]; reviews: Review[]; + reviewsCount: number; initialReviewsCursor: string | null; profileUserId: string; playlists: Playlist[]; @@ -40,6 +41,7 @@ export function ProfileTabs({ toWatch, watched, reviews, + reviewsCount, initialReviewsCursor, profileUserId, playlists, @@ -72,7 +74,7 @@ export function ProfileTabs({ { id: 'reviews', label: t.profile.tabs.reviews, - count: reviews.length, + count: reviewsCount, }, { id: 'playlists', From 10b987704bb0e97989cbe136f041ebe4d3ecf69d Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 03:07:17 +0200 Subject: [PATCH 09/98] =?UTF-8?q?fix(ui):=20badges=20et=20boutons=20des=20?= =?UTF-8?q?cards=20adapt=C3=A9s=20au=20th=C3=A8me=20light?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Les chips posées sur les vignettes (badge S/E, Marquer vu, note, statut) mélangeaient scrim sombre fixe et texte thémé — illisibles en light - Passage sur le matériau surface opaque (bg-surface + border-border) : blanc en light, noir en dark, automatique, sans blur (interdit en répété) - Badge note des posters : text-rating-gold (or vif en dark, ambre en light) - Scrims à texte blanc sur image (play trailers, X playlist) conservés tels quels --- components/dashboard/ContinueWatchingSection.tsx | 4 ++-- components/media/card/MediaCard.tsx | 10 ++++------ components/media/tv/EpisodeCard.tsx | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/components/dashboard/ContinueWatchingSection.tsx b/components/dashboard/ContinueWatchingSection.tsx index 6515dfac..6151aa60 100644 --- a/components/dashboard/ContinueWatchingSection.tsx +++ b/components/dashboard/ContinueWatchingSection.tsx @@ -159,7 +159,7 @@ function ContinueWatchingCard({ )} {episodeCode && ( - + {episodeCode} )} @@ -197,7 +197,7 @@ function ContinueWatchingCard({ disabled={loading} aria-label={t.movie.markEpisodeWatched} className={cn( - 'flex min-h-10 cursor-pointer items-center gap-2 rounded-md border border-border/10 border-t-border/20 bg-poster-overlay-heavy px-3 py-2 text-xs font-medium text-muted shadow-card-sm transition-colors', + 'flex min-h-10 cursor-pointer items-center gap-2 rounded-md border border-border/10 border-t-border/20 bg-surface px-3 py-2 text-xs font-medium text-muted shadow-card-sm transition-colors', 'hover:border-border hover:bg-surface-2 hover:text-text', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', 'disabled:cursor-not-allowed disabled:opacity-50' diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index 567ad98d..98f98b02 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -116,7 +116,7 @@ export function MediaCard({ )} {compact && badgeEntry && ( -
+
{badgeEntry.status === 'watched' ? ( -
+
)} diff --git a/components/media/tv/EpisodeCard.tsx b/components/media/tv/EpisodeCard.tsx index 7ca0a9a2..e8a6d94c 100644 --- a/components/media/tv/EpisodeCard.tsx +++ b/components/media/tv/EpisodeCard.tsx @@ -90,12 +90,12 @@ export function EpisodeCard({ {noImage}
)} -
+
E{episode.episode_number.toString().padStart(2, '0')}
From 1c4757cf2d49f33453ffe255ed1b56953664197d Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 03:48:55 +0200 Subject: [PATCH 10/98] feat(don): Add donation page and links CTA --- app/[lang]/layout.tsx | 8 ++- app/[lang]/support/loading.tsx | 32 ++++++++++ app/[lang]/support/page.tsx | 84 +++++++++++++++++++++++++ app/sitemap.ts | 5 ++ components/settings/SettingsContent.tsx | 2 + components/settings/SupportSettings.tsx | 39 ++++++++++++ components/support/DonateButton.tsx | 26 ++++++++ lib/i18n/messages/common.ts | 2 + lib/i18n/messages/support.ts | 55 ++++++++++++++++ lib/i18n/translations.ts | 3 + lib/support.ts | 2 + tests/e2e/support.spec.ts | 33 ++++++++++ 12 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 app/[lang]/support/loading.tsx create mode 100644 app/[lang]/support/page.tsx create mode 100644 components/settings/SupportSettings.tsx create mode 100644 components/support/DonateButton.tsx create mode 100644 lib/i18n/messages/support.ts create mode 100644 lib/support.ts create mode 100644 tests/e2e/support.spec.ts diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx index b57ff69a..d0e4a233 100644 --- a/app/[lang]/layout.tsx +++ b/app/[lang]/layout.tsx @@ -382,9 +382,15 @@ export default async function RootLayout({

© {COPYRIGHT_YEAR} ReelMark

)} diff --git a/components/settings/SupportSettings.tsx b/components/settings/SupportSettings.tsx new file mode 100644 index 00000000..8df9b446 --- /dev/null +++ b/components/settings/SupportSettings.tsx @@ -0,0 +1,39 @@ +'use client'; + +import Link from 'next/link'; +import { Heart } from 'lucide-react'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { DonateButton } from '@/components/support/DonateButton'; +import { useTranslation } from '@/lib/i18n/context'; +import { localizedHref } from '@/lib/i18n/utils'; + +export function SupportSettings() { + const { t, lang } = useTranslation(); + + return ( + + + + + {t.support.card.title} + + {t.support.card.description} + + + + + {t.support.card.link} + + + + ); +} diff --git a/components/support/DonateButton.tsx b/components/support/DonateButton.tsx new file mode 100644 index 00000000..fc5bc99d --- /dev/null +++ b/components/support/DonateButton.tsx @@ -0,0 +1,26 @@ +import { Heart } from 'lucide-react'; +import type { VariantProps } from 'class-variance-authority'; +import { buttonVariants } from '@/components/ui/button'; +import { DONATION_URL } from '@/lib/support'; +import { cn } from '@/lib/utils'; + +interface DonateButtonProps { + label: string; + size?: VariantProps['size']; + className?: string; +} + +/** Outbound donation link styled as a button — the only component that renders the donation URL. */ +export function DonateButton({ label, size, className }: DonateButtonProps) { + return ( + + + {label} + + ); +} diff --git a/lib/i18n/messages/common.ts b/lib/i18n/messages/common.ts index ea1b495f..7bbc9d65 100644 --- a/lib/i18n/messages/common.ts +++ b/lib/i18n/messages/common.ts @@ -33,6 +33,7 @@ const fr = { clearSearch: 'Effacer la recherche', close: 'Fermer', skipToMainContent: 'Aller au contenu principal', + footerNav: 'Liens de pied de page', notFoundTitle: 'Page introuvable', notFoundDescription: "La page que vous cherchez n'existe pas ou a été déplacée.", @@ -73,6 +74,7 @@ const en = { clearSearch: 'Clear search', close: 'Close', skipToMainContent: 'Skip to main content', + footerNav: 'Footer links', notFoundTitle: 'Page not found', notFoundDescription: 'The page you are looking for does not exist or has been moved.', diff --git a/lib/i18n/messages/support.ts b/lib/i18n/messages/support.ts new file mode 100644 index 00000000..ead7a5c7 --- /dev/null +++ b/lib/i18n/messages/support.ts @@ -0,0 +1,55 @@ +const fr = { + title: 'Soutenir ReelMark', + subtitle: 'Gratuit, sans publicité, sans revente de données.', + intro: "ReelMark est développé et payé par une seule personne, sur son temps libre. L'application restera gratuite et complète pour tout le monde : les dons servent uniquement à couvrir ce qu'elle coûte pour tourner.", + costsTitle: 'Ce que finance ton don', + costs: { + hosting: 'Hébergement des serveurs', + database: 'Base de données et authentification', + apis: 'Accès aux APIs de données de films et de séries', + domain: 'Nom de domaine et certificats', + }, + noPerksTitle: 'Aucune contrepartie', + noPerksBody: + "Un don ne débloque rien : pas de badge, pas de fonctionnalité réservée, pas d'accès anticipé. ReelMark est identique pour les donateurs et pour tous les autres, et le restera.", + cta: 'Faire un don', + ctaNote: + 'Le lien ouvre Revolut dans un nouvel onglet. ReelMark ne voit, ne traite et ne conserve aucune donnée de paiement.', + thanks: 'Merci — même un café, ça compte.', + nav: 'Soutenir', + card: { + title: 'Soutenir ReelMark', + description: + "L'application est gratuite et sans publicité. Si elle t'est utile, tu peux aider à couvrir ses frais.", + link: 'En savoir plus', + }, +}; + +const en = { + title: 'Support ReelMark', + subtitle: 'Free, ad-free, and your data is never sold.', + intro: 'ReelMark is built and paid for by one person, in their spare time. The app will stay free and complete for everyone: donations only cover what it costs to run.', + costsTitle: 'What your donation pays for', + costs: { + hosting: 'Server hosting', + database: 'Database and authentication', + apis: 'Access to movie and TV data APIs', + domain: 'Domain name and certificates', + }, + noPerksTitle: 'No perks in return', + noPerksBody: + 'A donation unlocks nothing: no badge, no reserved feature, no early access. ReelMark is the same for donors and for everyone else, and it will stay that way.', + cta: 'Donate', + ctaNote: + 'The link opens Revolut in a new tab. ReelMark never sees, processes, or stores any payment data.', + thanks: 'Thank you — even a coffee helps.', + nav: 'Support', + card: { + title: 'Support ReelMark', + description: + 'The app is free and ad-free. If it is useful to you, you can help cover its running costs.', + link: 'Learn more', + }, +} satisfies typeof fr; + +export const support = { fr, en }; diff --git a/lib/i18n/translations.ts b/lib/i18n/translations.ts index ffbddb5d..8fb5242d 100644 --- a/lib/i18n/translations.ts +++ b/lib/i18n/translations.ts @@ -18,6 +18,7 @@ import { danger } from './messages/danger'; import { metadata } from './messages/metadata'; import { onboarding } from './messages/onboarding'; import { oauth } from './messages/oauth'; +import { support } from './messages/support'; export const translations = { fr: { @@ -41,6 +42,7 @@ export const translations = { metadata: metadata.fr, onboarding: onboarding.fr, oauth: oauth.fr, + support: support.fr, }, en: { common: common.en, @@ -63,6 +65,7 @@ export const translations = { metadata: metadata.en, onboarding: onboarding.en, oauth: oauth.en, + support: support.en, }, }; diff --git a/lib/support.ts b/lib/support.ts new file mode 100644 index 00000000..169a006b --- /dev/null +++ b/lib/support.ts @@ -0,0 +1,2 @@ +/** Outbound donation destination — single source of truth so the payment provider can change without touching the UI. */ +export const DONATION_URL = 'https://revolut.me/sawkit17'; diff --git a/tests/e2e/support.spec.ts b/tests/e2e/support.spec.ts new file mode 100644 index 00000000..5347b8dc --- /dev/null +++ b/tests/e2e/support.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test'; + +const DONATION_URL = 'https://revolut.me/sawkit17'; + +test.describe('Support page', () => { + test('exposes the donation link as an external button', async ({ + page, + }) => { + await page.goto('/en/support'); + await expect( + page.getByRole('heading', { level: 1, name: /support reelmark/i }) + ).toBeVisible(); + + const donate = page.getByRole('link', { name: /donate/i }).first(); + await expect(donate).toBeVisible(); + await expect(donate).toHaveAttribute('href', DONATION_URL); + await expect(donate).toHaveAttribute('target', '_blank'); + await expect(donate).toHaveAttribute('rel', /noopener/); + }); + + test('is reachable from the footer in both languages', async ({ page }) => { + for (const [lang, name] of [ + ['en', /support/i], + ['fr', /soutenir/i], + ] as const) { + await page.goto(`/${lang}`); + const link = page + .getByRole('contentinfo') + .getByRole('link', { name }); + await expect(link).toHaveAttribute('href', `/${lang}/support`); + } + }); +}); From 17f0859368e88f1e1a8b8d694f9e5fb7ce22b960 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 19:19:16 +0200 Subject: [PATCH 11/98] fix(ux): Wrap flex + Replace support link by badge --- app/[lang]/layout.tsx | 41 +++++++++++++++-------------- components/support/SupportBadge.tsx | 27 +++++++++++++++++++ 2 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 components/support/SupportBadge.tsx diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx index d0e4a233..268673fa 100644 --- a/app/[lang]/layout.tsx +++ b/app/[lang]/layout.tsx @@ -15,6 +15,7 @@ import { BASE_URL, DEFAULT_OG_IMAGE } from '@/lib/metadata'; import { ScrollToTop } from '@/components/layout/ScrollToTop'; import { PreventImageContextMenu } from '@/components/shared/PreventImageContextMenu'; import { PWAInstallPrompt } from '@/components/shared/PWAInstallPrompt'; +import { SupportBadge } from '@/components/support/SupportBadge'; import NextTopLoader from 'nextjs-toploader'; import Link from 'next/link'; import { localizedHref } from '@/lib/i18n/utils'; @@ -379,30 +380,30 @@ export default async function RootLayout({ {children}
-
+

© {COPYRIGHT_YEAR} ReelMark

diff --git a/components/support/SupportBadge.tsx b/components/support/SupportBadge.tsx new file mode 100644 index 00000000..286f487b --- /dev/null +++ b/components/support/SupportBadge.tsx @@ -0,0 +1,27 @@ +import Link from 'next/link'; +import { Heart } from 'lucide-react'; +import { localizedHref } from '@/lib/i18n/utils'; +import type { Language } from '@/lib/i18n/translations'; +import { cn } from '@/lib/utils'; + +interface SupportBadgeProps { + lang: Language; + label: string; + className?: string; +} + +/** Pill link to the donation page — stands out from the plain text links it sits next to. */ +export function SupportBadge({ lang, label, className }: SupportBadgeProps) { + return ( + + + {label} + + ); +} From 5f2cfe8fb340a1e628102feb538ae1195d4652f5 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 20:40:37 +0200 Subject: [PATCH 12/98] =?UTF-8?q?fix(ux):=20contr=C3=B4les=20au=20survol?= =?UTF-8?q?=20inertes=20sur=20tactile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MediaCard et SeasonCard : pointer-events-none sur les overlays masqués, réactivés au survol — opacity-0 ne coupe pas les événements pointeur et Tailwind v4 borne hover: à @media (hover:hover) - corrige deux mutations involontaires en PWA : un tap au bas d'une card ajoutait le titre à la watchlist, un tap sur une card de saison marquait la saison entière comme vue - tests : helper pageButton — watchlist/list-controls/resilience ciblaient un bouton de card au lieu de la page détail - tests : garde anti-régression touch-ghost-controls (aucun contrôle invisible ne doit être tapable) --- components/media/card/MediaCard.tsx | 6 +- components/media/tv/SeasonCard.tsx | 4 +- tests/e2e/media-card-tap.spec.ts | 36 +++++++++ tests/e2e/protected/list-controls.spec.ts | 6 +- tests/e2e/protected/resilience.spec.ts | 6 +- .../protected/touch-ghost-controls.spec.ts | 30 ++++++++ tests/e2e/protected/watchlist.spec.ts | 77 +++++-------------- tests/helpers/controls.ts | 9 +++ tests/helpers/ghost-controls.ts | 59 ++++++++++++++ 9 files changed, 164 insertions(+), 69 deletions(-) create mode 100644 tests/e2e/media-card-tap.spec.ts create mode 100644 tests/e2e/protected/touch-ghost-controls.spec.ts create mode 100644 tests/helpers/controls.ts create mode 100644 tests/helpers/ghost-controls.ts diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index 98f98b02..be759326 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -179,7 +179,11 @@ export function MediaCard({
diff --git a/components/media/tv/SeasonCard.tsx b/components/media/tv/SeasonCard.tsx index 97e3a46f..e304c013 100644 --- a/components/media/tv/SeasonCard.tsx +++ b/components/media/tv/SeasonCard.tsx @@ -127,7 +127,9 @@ export function SeasonCard({ 'absolute top-2 right-2 z-10 transition-all duration-(--duration-base)', watched > 0 ? 'opacity-100' - : 'opacity-0 group-hover:opacity-100' + : // Masqué sans hover : sans `pointer-events-none`, un tap sur ce coin + // marquait toute la saison comme vue sur mobile, où l'icône est invisible. + 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto' )} > { + test('tapping the bottom of a card navigates instead of firing a watchlist action', async ({ + page, + }) => { + const serverActions: string[] = []; + page.on('request', (request) => { + if (request.method() === 'POST') serverActions.push(request.url()); + }); + + await page.goto('/en/movie/550'); + const card = page.locator('a:has(button)').first(); + await card.scrollIntoViewIfNeeded(); + await expect(card).toBeVisible(); + + const href = await card.getAttribute('href'); + const box = await card.boundingBox(); + if (!href || !box) throw new Error('no media card found on the page'); + + await page.touchscreen.tap( + box.x + box.width / 2, + box.y + box.height * 0.92 + ); + await page.waitForTimeout(2000); + + expect(serverActions).toEqual([]); + expect(new URL(page.url()).pathname).toBe(href); + }); +}); diff --git a/tests/e2e/protected/list-controls.spec.ts b/tests/e2e/protected/list-controls.spec.ts index 89f11eb0..4ed6d17f 100644 --- a/tests/e2e/protected/list-controls.spec.ts +++ b/tests/e2e/protected/list-controls.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from '@playwright/test'; import { hasValidAuth } from '../../helpers/auth'; +import { pageButton } from '../../helpers/controls'; const SEED_MOVIE_ID = 603; @@ -13,10 +14,7 @@ test.beforeEach(() => { async function ensureLibraryHasItem(page: import('@playwright/test').Page) { await page.goto(`/en/movie/${SEED_MOVIE_ID}`); await page.getByRole('heading', { level: 1 }).waitFor({ timeout: 10000 }); - const addBtn = page - .locator('button:not([disabled])') - .filter({ hasText: /ajouter à la liste|add to list/i }) - .first(); + const addBtn = pageButton(page, /ajouter à la liste|add to list/i); if (await addBtn.isVisible().catch(() => false)) { const response = page.waitForResponse( (resp) => diff --git a/tests/e2e/protected/resilience.spec.ts b/tests/e2e/protected/resilience.spec.ts index 78fd30c3..7bd03e87 100644 --- a/tests/e2e/protected/resilience.spec.ts +++ b/tests/e2e/protected/resilience.spec.ts @@ -1,4 +1,5 @@ import { test, expect, type Page } from '@playwright/test'; +import { pageButton } from '../../helpers/controls'; /** Le bouton de la frontière d'erreur (`ErrorCard`) — présent seulement si la page a planté. */ function errorBoundary(page: Page) { @@ -59,10 +60,7 @@ test.describe('Résilience de rendu', () => { await page.goto('/en/movie/550', { waitUntil: 'domcontentloaded' }); for (let i = 0; i < 3; i++) { - const button = page - .locator('button:not([disabled])') - .filter({ hasText: /watch|vu|ajout|added|list/i }) - .first(); + const button = pageButton(page, /watch|vu|ajout|added|list/i); if (!(await button.isVisible().catch(() => false))) break; await button.click({ timeout: 5000 }).catch(() => {}); await page.waitForTimeout(250); diff --git a/tests/e2e/protected/touch-ghost-controls.spec.ts b/tests/e2e/protected/touch-ghost-controls.spec.ts new file mode 100644 index 00000000..11eb0947 --- /dev/null +++ b/tests/e2e/protected/touch-ghost-controls.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { hasValidAuth } from '../../helpers/auth'; +import { findGhostControls } from '../../helpers/ghost-controls'; + +const PAGES = ['/fr/dashboard', '/fr/library', '/fr/movie/550', '/fr/tv/1399']; + +test.use({ + viewport: { width: 390, height: 844 }, + hasTouch: true, + isMobile: true, +}); + +test.beforeEach(() => { + test.skip( + !hasValidAuth(), + 'No valid auth session — skipping authenticated tests' + ); +}); + +test.describe('Hover-only controls on touch devices', () => { + test('no invisible control is tappable', async ({ page }) => { + test.setTimeout(180000); + + for (const path of PAGES) { + await page.goto(path, { waitUntil: 'domcontentloaded' }); + await page.waitForTimeout(2500); + expect(await findGhostControls(page), path).toEqual([]); + } + }); +}); diff --git a/tests/e2e/protected/watchlist.spec.ts b/tests/e2e/protected/watchlist.spec.ts index 028360b1..c40ac186 100644 --- a/tests/e2e/protected/watchlist.spec.ts +++ b/tests/e2e/protected/watchlist.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from '@playwright/test'; import { hasValidAuth } from '../../helpers/auth'; +import { pageButton } from '../../helpers/controls'; test.describe.configure({ mode: 'serial' }); @@ -51,10 +52,7 @@ async function ensureMovieRemoved(page: import('@playwright/test').Page) { await page .getByRole('heading', { level: 1 }) .waitFor({ timeout: 10000 }); - const active = page - .locator('button:not([disabled])') - .filter({ hasText: /^vu$|^watched$|^ajouté$|^added$/i }) - .first(); + const active = pageButton(page, /^vu$|^watched$|^ajouté$|^added$/i); if (!(await active.isVisible().catch(() => false))) return; await clickAndWaitForAction(page, active).catch(() => {}); } @@ -73,39 +71,24 @@ test.describe('Watchlist', () => { ); // If movie is in "watched" state, clicking it falls back to "to_watch" (fallbackStatus="to_watch") - const watchedActiveBtn = page - .locator('button:not([disabled])') - .filter({ hasText: /^vu$|^watched$/i }) - .first(); + const watchedActiveBtn = pageButton(page, /^vu$|^watched$/i); if (await watchedActiveBtn.isVisible()) { await clickAndWaitForAction(page, watchedActiveBtn); - await expect( - page - .locator('button:not([disabled])') - .filter({ hasText: /^ajouté$|^added$/i }) - .first() - ).toBeVisible({ timeout: 5000 }); + await expect(pageButton(page, /^ajouté$|^added$/i)).toBeVisible({ + timeout: 5000, + }); } // If movie is in "to_watch" state, remove it entirely - const addedBtn = page - .locator('button:not([disabled])') - .filter({ hasText: /^ajouté$|^added$/i }) - .first(); + const addedBtn = pageButton(page, /^ajouté$|^added$/i); if (await addedBtn.isVisible()) { await clickAndWaitForAction(page, addedBtn); await expect( - page - .locator('button:not([disabled])') - .filter({ hasText: /ajouter à la liste|add to list/i }) - .first() + pageButton(page, /ajouter à la liste|add to list/i) ).toBeVisible({ timeout: 5000 }); } - const addBtn = page - .locator('button:not([disabled])') - .filter({ hasText: /ajouter à la liste|add to list/i }) - .first(); + const addBtn = pageButton(page, /ajouter à la liste|add to list/i); await clickAndWaitForAction(page, addBtn); await page.goto('/en/library'); @@ -116,10 +99,7 @@ test.describe('Watchlist', () => { await page.goto(`/en/movie/${MOVIE_ID}`); await clickAndWaitForAction( page, - page - .locator('button:not([disabled])') - .filter({ hasText: /^ajouté$|^added$/i }) - .first() + pageButton(page, /^ajouté$|^added$/i) ); await page.goto('/en/library'); @@ -133,48 +113,27 @@ test.describe('Watchlist', () => { { timeout: 10000 } ); - const watchedBtn = page - .locator('button:not([disabled])') - .filter({ hasText: /^vu$|^watched$/i }) - .first(); + const watchedBtn = pageButton(page, /^vu$|^watched$/i); if (await watchedBtn.isVisible()) { await clickAndWaitForAction(page, watchedBtn); await expect( - page - .locator('button:not([disabled])') - .filter({ hasText: /marquer comme vu|mark as watched/i }) - .first() + pageButton(page, /marquer comme vu|mark as watched/i) ).toBeVisible({ timeout: 5000 }); } await clickAndWaitForAction( page, - page - .locator('button:not([disabled])') - .filter({ hasText: /marquer comme vu|mark as watched/i }) - .first() + pageButton(page, /marquer comme vu|mark as watched/i) ); await expectInWatchedTab(page); await page.goto(`/en/movie/${MOVIE_ID}`); + await expect(pageButton(page, /^vu$|^watched$/i)).toBeVisible({ + timeout: 5000, + }); + await clickAndWaitForAction(page, pageButton(page, /^vu$|^watched$/i)); await expect( - page - .locator('button:not([disabled])') - .filter({ hasText: /^vu$|^watched$/i }) - .first() - ).toBeVisible({ timeout: 5000 }); - await clickAndWaitForAction( - page, - page - .locator('button:not([disabled])') - .filter({ hasText: /^vu$|^watched$/i }) - .first() - ); - await expect( - page - .locator('button:not([disabled])') - .filter({ hasText: /marquer comme vu|mark as watched/i }) - .first() + pageButton(page, /marquer comme vu|mark as watched/i) ).toBeVisible({ timeout: 10000 }); }); }); diff --git a/tests/helpers/controls.ts b/tests/helpers/controls.ts new file mode 100644 index 00000000..ee38e901 --- /dev/null +++ b/tests/helpers/controls.ts @@ -0,0 +1,9 @@ +import type { Locator, Page } from '@playwright/test'; + +/** First enabled page-level action button matching `text`, excluding the hover-only buttons nested inside media cards. */ +export function pageButton(page: Page, text: RegExp): Locator { + return page + .locator('xpath=//button[not(@disabled) and not(ancestor::a)]') + .filter({ hasText: text }) + .first(); +} diff --git a/tests/helpers/ghost-controls.ts b/tests/helpers/ghost-controls.ts new file mode 100644 index 00000000..8174c156 --- /dev/null +++ b/tests/helpers/ghost-controls.ts @@ -0,0 +1,59 @@ +import type { Page } from '@playwright/test'; + +function collect(): string[] { + const SELECTOR = + 'button, a, input, select, textarea, [role="button"], [tabindex]'; + const effectiveOpacity = (el: Element): number => { + let opacity = 1; + for (let node: Element | null = el; node; node = node.parentElement) { + opacity *= parseFloat(getComputedStyle(node).opacity || '1'); + } + return opacity; + }; + const describe = (el: Element): string => + `${el.tagName.toLowerCase()} "${( + el.getAttribute('aria-label') || + el.textContent?.trim() || + el.getAttribute('href') || + '' + ) + .replace(/\s+/g, ' ') + .slice(0, 40)}"`; + + const ghosts: string[] = []; + for (const el of document.querySelectorAll(SELECTOR)) { + const style = getComputedStyle(el); + if (style.pointerEvents === 'none' || style.visibility === 'hidden') + continue; + const rect = el.getBoundingClientRect(); + if (rect.width * rect.height < 400) continue; + if (effectiveOpacity(el) > 0.05) continue; + const hit = document.elementFromPoint( + Math.min( + window.innerWidth - 1, + Math.max(1, rect.x + rect.width / 2) + ), + Math.min( + window.innerHeight - 1, + Math.max(1, rect.y + rect.height / 2) + ) + ); + if (!hit || (hit !== el && !el.contains(hit))) continue; + ghosts.push(describe(el)); + } + return ghosts; +} + +/** Lists interactive controls that are invisible yet still the tap target — on touch devices they fire actions the user cannot see. */ +export async function findGhostControls(page: Page): Promise { + const found = new Set(); + for (const ratio of [0, 0.35, 0.7, 1]) { + await page.evaluate( + (r) => window.scrollTo(0, document.body.scrollHeight * r), + ratio + ); + await page.waitForTimeout(600); + for (const ghost of await page.evaluate(collect)) found.add(ghost); + } + return [...found]; +} From f335709e114188e096355621f2626f89025a1cdc Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 22:06:13 +0200 Subject: [PATCH 13/98] =?UTF-8?q?fix(dashboard):=20le=20hero=20Reprendre?= =?UTF-8?q?=20suit=20la=20derni=C3=A8re=20s=C3=A9rie=20r=C3=A9ellement=20r?= =?UTF-8?q?eprenable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lang]/(protected)/dashboard/page.tsx | 27 ++-- .../dashboard/ContinueWatchingSection.tsx | 15 ++- lib/continue-watching.ts | 31 +++++ .../actions => lib/data}/continue-watching.ts | 117 ++++++++++-------- tests/unit/continue-watching-order.test.ts | 83 +++++++++++++ types/database.ts | 7 ++ 6 files changed, 218 insertions(+), 62 deletions(-) create mode 100644 lib/continue-watching.ts rename {app/actions => lib/data}/continue-watching.ts (66%) create mode 100644 tests/unit/continue-watching-order.test.ts diff --git a/app/[lang]/(protected)/dashboard/page.tsx b/app/[lang]/(protected)/dashboard/page.tsx index 042297fa..1fafd02f 100644 --- a/app/[lang]/(protected)/dashboard/page.tsx +++ b/app/[lang]/(protected)/dashboard/page.tsx @@ -35,7 +35,10 @@ import { BentoStatsSkeleton, TrendingMarqueeSkeleton, } from '@/components/dashboard/DashboardSkeletons'; -import { getContinueWatching } from '@/app/actions/continue-watching'; +import { + getContinueWatching, + type ContinueWatchingItem, +} from '@/lib/data/continue-watching'; import { getWatchlistWithProgress, mergeWithWatchlist, @@ -62,13 +65,15 @@ type Props = { async function buildHero( watchlist: Awaited>, tvProgress: Record, + resumable: ContinueWatchingItem | null, lang: Language ): Promise { const candidates = watchlist.filter((e) => e.status !== 'abandoned'); const featured = - candidates.find( - (e) => e.media_type === 'tv' && (tvProgress[e.media_id] ?? 0) > 0 - ) ?? + (resumable && + candidates.find( + (e) => e.media_type === 'tv' && e.media_id === resumable.tvId + )) ?? candidates.find((e) => e.status === 'to_watch') ?? candidates[0]; if (!featured) return null; @@ -89,7 +94,7 @@ async function buildHero( watched > 0 && d.number_of_episodes > 0 ? { watched, total: d.number_of_episodes } : null, - resume: watched > 0, + resume: featured.media_id === resumable?.tvId, }; } const d = await getMovieDetails(featured.media_id, lang); @@ -111,8 +116,16 @@ async function buildHero( } async function HeroSection({ t, lang }: { t: Translations; lang: Language }) { - const { watchlist, tvProgress } = await getWatchlistWithProgress(); - const hero = await buildHero(watchlist, tvProgress, lang); + const [{ watchlist, tvProgress }, resumable] = await Promise.all([ + getWatchlistWithProgress(), + getContinueWatching(), + ]); + const hero = await buildHero( + watchlist, + tvProgress, + resumable[0] ?? null, + lang + ); if (!hero) return null; return ( diff --git a/components/dashboard/ContinueWatchingSection.tsx b/components/dashboard/ContinueWatchingSection.tsx index 6151aa60..5e3d7b51 100644 --- a/components/dashboard/ContinueWatchingSection.tsx +++ b/components/dashboard/ContinueWatchingSection.tsx @@ -18,7 +18,7 @@ import { HorizontalScroll } from '@/components/shared/HorizontalScroll'; import { SectionHeading } from '@/components/ui/SectionHeading'; import { ProgressBar } from '@/components/shared/ProgressBar'; import { StaggeredItem } from '@/components/ui/StaggeredItem'; -import type { ContinueWatchingItem } from '@/app/actions/continue-watching'; +import type { ContinueWatchingItem } from '@/lib/data/continue-watching'; const CARD_ANIMATION_DELAY_MS = 50; @@ -98,19 +98,23 @@ function ContinueWatchingCard({ const previous = episodeWatchStore.get(item.tvId, episode.seasonNumber); await run({ - apply: () => + apply: () => { episodeWatchStore.setEpisode( item.tvId, episode.seasonNumber, episode.episodeNumber, true - ), - rollback: () => + ); + setQueueIndex((index) => index + 1); + }, + rollback: () => { episodeWatchStore.restore( item.tvId, episode.seasonNumber, previous - ), + ); + setQueueIndex((index) => Math.max(0, index - 1)); + }, action: () => setEpisodeWatched( item.tvId, @@ -120,7 +124,6 @@ function ContinueWatchingCard({ ), onSuccess: (result) => { mediaWatchStore.set('tv', item.tvId, result.tvStatus); - setQueueIndex((index) => index + 1); }, }); } diff --git a/lib/continue-watching.ts b/lib/continue-watching.ts new file mode 100644 index 00000000..66129d68 --- /dev/null +++ b/lib/continue-watching.ts @@ -0,0 +1,31 @@ +export interface ResumeCandidate { + media_id: number; + created_at: string | null; +} + +/** + * Orders shows for the "continue watching" row: the show whose last episode was watched most + * recently comes first, then the one furthest along, then the most recently added. + * + * @param candidates - Watchlist entries eligible for the row. + * @param lastWatchedAt - ISO timestamp of the latest watched episode, keyed by show id. + * @param progress - Watched episode count keyed by show id. + */ +export function orderByWatchRecency( + candidates: readonly T[], + lastWatchedAt: ReadonlyMap, + progress: Readonly> +): T[] { + const watchedAt = (entry: T) => lastWatchedAt.get(entry.media_id) ?? ''; + + return [...candidates].sort((a, b) => { + const byRecency = watchedAt(b).localeCompare(watchedAt(a)); + if (byRecency !== 0) return byRecency; + + const byProgress = + (progress[b.media_id] ?? 0) - (progress[a.media_id] ?? 0); + if (byProgress !== 0) return byProgress; + + return (b.created_at ?? '').localeCompare(a.created_at ?? ''); + }); +} diff --git a/app/actions/continue-watching.ts b/lib/data/continue-watching.ts similarity index 66% rename from app/actions/continue-watching.ts rename to lib/data/continue-watching.ts index e5ea06c1..9fe3787f 100644 --- a/app/actions/continue-watching.ts +++ b/lib/data/continue-watching.ts @@ -1,13 +1,17 @@ -'use server'; - +import { cache } from 'react'; import { getOptionalUser } from '@/lib/supabase/auth-helpers'; import { getWatchlistWithProgress } from '@/lib/data/watchlist'; import { fetchAllRows } from '@/lib/supabase/pagination'; import { getSeasonDetails } from '@/lib/tmdb'; import { getCachedTvShowDetails } from '@/lib/tmdb/cached'; import { findNextEpisode, type SeasonEpisodeCount } from '@/lib/next-episode'; +import { orderByWatchRecency } from '@/lib/continue-watching'; import type { WatchlistEntry } from '@/types/tmdb'; +type SupabaseServerClient = Awaited< + ReturnType +>['supabase']; + export interface ContinueWatchingEpisode { seasonNumber: number; episodeNumber: number; @@ -28,7 +32,8 @@ export interface ContinueWatchingItem { const SHOW_LIMIT = 10; const QUEUE_LIMIT = 10; -const RECENT_WATCHES_LIMIT = 300; +const WAVE_SIZE = 10; +const SCAN_LIMIT = 40; type WatchedBySeason = Map>; @@ -95,50 +100,11 @@ async function buildItem( }; } -/** - * TV shows the user can pick up right now, each resolved to its next unwatched episode - * plus the aired episodes that follow it, so the dashboard card can advance without a refetch. - * - * @returns At most 10 shows, most recently watched first; empty when unauthenticated. - */ -export async function getContinueWatching(): Promise { - const { supabase, userId } = await getOptionalUser(); - if (!userId) return []; - - const { watchlist, tvProgress } = await getWatchlistWithProgress(); - const candidates = watchlist.filter( - (entry) => entry.media_type === 'tv' && entry.status === 'to_watch' - ); - if (candidates.length === 0) return []; - - const candidateIds = candidates.map((entry) => entry.media_id); - const { data: recentWatches } = await supabase - .from('episode_watches') - .select('tv_id, watched_at') - .eq('user_id', userId) - .in('tv_id', candidateIds) - .order('watched_at', { ascending: false, nullsFirst: false }) - .limit(RECENT_WATCHES_LIMIT); - - const recencyRank = new Map(); - for (const [index, watch] of (recentWatches ?? []).entries()) { - if (!recencyRank.has(watch.tv_id)) recencyRank.set(watch.tv_id, index); - } - - const rankOf = (entry: WatchlistEntry) => - recencyRank.get(entry.media_id) ?? Number.MAX_SAFE_INTEGER; - - const selected = [...candidates] - .sort((a, b) => { - const byRecency = rankOf(a) - rankOf(b); - if (byRecency !== 0) return byRecency; - const byProgress = - (tvProgress[b.media_id] ?? 0) - (tvProgress[a.media_id] ?? 0); - if (byProgress !== 0) return byProgress; - return b.created_at.localeCompare(a.created_at); - }) - .slice(0, SHOW_LIMIT); - +async function buildWave( + supabase: SupabaseServerClient, + userId: string, + entries: WatchlistEntry[] +): Promise { const watches = await fetchAllRows((from, to) => supabase .from('episode_watches') @@ -146,7 +112,7 @@ export async function getContinueWatching(): Promise { .eq('user_id', userId) .in( 'tv_id', - selected.map((entry) => entry.media_id) + entries.map((entry) => entry.media_id) ) .order('tv_id') .order('season_number') @@ -164,10 +130,63 @@ export async function getContinueWatching(): Promise { } const items = await Promise.all( - selected.map((entry) => + entries.map((entry) => buildItem(entry, watchedByShow.get(entry.media_id) ?? new Map()) ) ); return items.filter((item) => item !== null); } + +/** + * TV shows the user can pick up right now, each resolved to its next unwatched episode + * plus the aired episodes that follow it, so the dashboard card can advance without a refetch. + * + * Shows are ranked by the timestamp of their last watched episode; those with nothing left to + * watch (finished, or next episode unaired) drop out and the next-most-recent show takes the + * slot, which is why candidates are resolved in waves instead of being truncated upfront. + * + * Request-cached: the dashboard hero and the row below it share one resolution. + * + * @returns At most 10 shows, most recently watched first; empty when unauthenticated. + */ +export const getContinueWatching = cache( + async (): Promise => { + const { supabase, userId } = await getOptionalUser(); + if (!userId) return []; + + const { watchlist, tvProgress } = await getWatchlistWithProgress(); + const candidates = watchlist.filter( + (entry) => entry.media_type === 'tv' && entry.status === 'to_watch' + ); + if (candidates.length === 0) return []; + + const { data: lastWatches } = await supabase.rpc( + 'episode_last_watches' + ); + const lastWatchedAt = new Map(); + for (const row of lastWatches ?? []) { + if (row.last_watched_at) + lastWatchedAt.set(row.tv_id, row.last_watched_at); + } + + const ordered = orderByWatchRecency( + candidates, + lastWatchedAt, + tvProgress + ); + const scannable = Math.min(ordered.length, SCAN_LIMIT); + const items: ContinueWatchingItem[] = []; + + for ( + let start = 0; + start < scannable && items.length < SHOW_LIMIT; + start += WAVE_SIZE + ) { + const wave = ordered.slice(start, start + WAVE_SIZE); + items.push(...(await buildWave(supabase, userId, wave))); + } + + return items.slice(0, SHOW_LIMIT); + } +); diff --git a/tests/unit/continue-watching-order.test.ts b/tests/unit/continue-watching-order.test.ts new file mode 100644 index 00000000..f2227338 --- /dev/null +++ b/tests/unit/continue-watching-order.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest'; +import { orderByWatchRecency } from '@/lib/continue-watching'; + +const show = (media_id: number, created_at = '2026-01-01T00:00:00Z') => ({ + media_id, + created_at, +}); + +const lastWatched = (entries: [number, string][]) => new Map(entries); + +describe('orderByWatchRecency', () => { + it('puts the show whose last episode was watched most recently first', () => { + const shows = [show(1), show(2), show(3)]; + const ordered = orderByWatchRecency( + shows, + lastWatched([ + [1, '2026-07-12T20:08:38Z'], + [2, '2026-07-25T01:49:46Z'], + [3, '2026-07-14T18:24:45Z'], + ]), + {} + ); + expect(ordered.map((s) => s.media_id)).toEqual([2, 3, 1]); + }); + + it('ranks shows sharing a bulk-import timestamp by progress', () => { + const sameInstant = '2026-07-12T20:08:38Z'; + const ordered = orderByWatchRecency( + [show(1), show(2), show(3)], + lastWatched([ + [1, sameInstant], + [2, sameInstant], + [3, sameInstant], + ]), + { 1: 5, 2: 51, 3: 18 } + ); + expect(ordered.map((s) => s.media_id)).toEqual([2, 3, 1]); + }); + + it('falls back to the most recently added show when nothing else separates them', () => { + const ordered = orderByWatchRecency( + [ + show(1, '2026-01-01T00:00:00Z'), + show(2, '2026-06-01T00:00:00Z'), + show(3, '2026-03-01T00:00:00Z'), + ], + lastWatched([]), + {} + ); + expect(ordered.map((s) => s.media_id)).toEqual([2, 3, 1]); + }); + + it('places never-watched shows after every started one', () => { + const ordered = orderByWatchRecency( + [show(1), show(2), show(3)], + lastWatched([[2, '2026-02-01T00:00:00Z']]), + { 1: 0, 2: 1, 3: 0 } + ); + expect(ordered[0].media_id).toBe(2); + }); + + it('tolerates a missing created_at', () => { + const ordered = orderByWatchRecency( + [ + { media_id: 1, created_at: null }, + { media_id: 2, created_at: '2026-05-01T00:00:00Z' }, + ], + lastWatched([]), + {} + ); + expect(ordered.map((s) => s.media_id)).toEqual([2, 1]); + }); + + it('leaves the input array untouched', () => { + const shows = [show(1), show(2)]; + orderByWatchRecency( + shows, + lastWatched([[2, '2026-07-01T00:00:00Z']]), + {} + ); + expect(shows.map((s) => s.media_id)).toEqual([1, 2]); + }); +}); diff --git a/types/database.ts b/types/database.ts index b5d2f4bc..3f41fd2c 100644 --- a/types/database.ts +++ b/types/database.ts @@ -465,6 +465,13 @@ export type Database = { [_ in never]: never; }; Functions: { + episode_last_watches: { + Args: never; + Returns: { + last_watched_at: string; + tv_id: number; + }[]; + }; episode_watch_counts: { Args: never; Returns: { From 7c895b78fbcb84f4696b7f793a7d63b90d947d97 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 22:46:04 +0200 Subject: [PATCH 14/98] =?UTF-8?q?feat(ux):=20dashboard=20r=C3=A9actif=20sa?= =?UTF-8?q?ns=20SSR=20(hero,=20compteurs,=20barres).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lang]/(protected)/dashboard/page.tsx | 21 +-- components/dashboard/BentoStats.tsx | 31 +++- .../dashboard/ContinueWatchingSection.tsx | 1 + components/dashboard/DashboardHero.tsx | 36 ++++- components/media/card/MediaCard.tsx | 12 +- lib/dashboard-hero.ts | 23 +++ lib/episode-watch-store.ts | 112 +++++++++++---- lib/media-watch-store.ts | 52 ++++++- tests/unit/dashboard-hero.test.ts | 48 +++++++ tests/unit/media-watch-store.test.ts | 4 + tests/unit/watch-deltas.test.ts | 135 ++++++++++++++++++ 11 files changed, 424 insertions(+), 51 deletions(-) create mode 100644 lib/dashboard-hero.ts create mode 100644 tests/unit/dashboard-hero.test.ts create mode 100644 tests/unit/watch-deltas.test.ts diff --git a/app/[lang]/(protected)/dashboard/page.tsx b/app/[lang]/(protected)/dashboard/page.tsx index 1fafd02f..b75fca6a 100644 --- a/app/[lang]/(protected)/dashboard/page.tsx +++ b/app/[lang]/(protected)/dashboard/page.tsx @@ -62,6 +62,8 @@ type Props = { params: Promise<{ lang: Language }>; }; +const HERO_CANDIDATES = 3; + async function buildHero( watchlist: Awaited>, tvProgress: Record, @@ -120,17 +122,20 @@ async function HeroSection({ t, lang }: { t: Translations; lang: Language }) { getWatchlistWithProgress(), getContinueWatching(), ]); - const hero = await buildHero( - watchlist, - tvProgress, - resumable[0] ?? null, - lang - ); - if (!hero) return null; + + const upNext = resumable.slice(0, HERO_CANDIDATES); + const heroes = ( + await Promise.all( + (upNext.length > 0 ? upNext : [null]).map((item) => + buildHero(watchlist, tvProgress, item, lang) + ) + ) + ).filter((hero) => hero !== null); + if (heroes.length === 0) return null; return ( diff --git a/components/dashboard/BentoStats.tsx b/components/dashboard/BentoStats.tsx index 6f548f82..ab76fcce 100644 --- a/components/dashboard/BentoStats.tsx +++ b/components/dashboard/BentoStats.tsx @@ -1,6 +1,10 @@ +'use client'; + import { Film, Tv, Bookmark, type LucideIcon } from 'lucide-react'; import { Aurora } from '@/components/effects/Aurora'; import { SectionHeading } from '@/components/ui/SectionHeading'; +import { useEpisodeWatchDelta } from '@/lib/episode-watch-store'; +import { useWatchStatusDelta } from '@/lib/media-watch-store'; interface BentoStatsProps { title: string; @@ -10,7 +14,10 @@ interface BentoStatsProps { labels: { movies: string; episodes: string; toWatch: string }; } -/** Compact stat grid summarizing the user's watch activity (data-backed counts). */ +/** + * Compact stat grid summarizing the user's watch activity. Server counts are the baseline; + * this session's mutations are added on top so the numbers move as the user ticks episodes. + */ export function BentoStats({ title, moviesWatched, @@ -18,10 +25,26 @@ export function BentoStats({ toWatch, labels, }: BentoStatsProps) { + const episodesDelta = useEpisodeWatchDelta(); + const moviesDelta = useWatchStatusDelta('watched', 'movie'); + const toWatchDelta = useWatchStatusDelta('to_watch'); + const cells: { icon: LucideIcon; value: number; label: string }[] = [ - { icon: Film, value: moviesWatched, label: labels.movies }, - { icon: Tv, value: episodesWatched, label: labels.episodes }, - { icon: Bookmark, value: toWatch, label: labels.toWatch }, + { + icon: Film, + value: Math.max(0, moviesWatched + moviesDelta), + label: labels.movies, + }, + { + icon: Tv, + value: Math.max(0, episodesWatched + episodesDelta), + label: labels.episodes, + }, + { + icon: Bookmark, + value: Math.max(0, toWatch + toWatchDelta), + label: labels.toWatch, + }, ]; return ( diff --git a/components/dashboard/ContinueWatchingSection.tsx b/components/dashboard/ContinueWatchingSection.tsx index 5e3d7b51..462bdc6f 100644 --- a/components/dashboard/ContinueWatchingSection.tsx +++ b/components/dashboard/ContinueWatchingSection.tsx @@ -87,6 +87,7 @@ function ContinueWatchingCard({ item.seasonWatchedEpisodes.length, item.seasonWatchedEpisodes ); + mediaWatchStore.seed('tv', item.tvId, 'to_watch'); }, [item.tvId, item.seasonNumber, item.seasonWatchedEpisodes]); const watchedTotal = useTvWatchTotal(item.tvId, item.seasonWatched); diff --git a/components/dashboard/DashboardHero.tsx b/components/dashboard/DashboardHero.tsx index 3206f695..5dbcb4ef 100644 --- a/components/dashboard/DashboardHero.tsx +++ b/components/dashboard/DashboardHero.tsx @@ -1,10 +1,17 @@ +'use client'; + import Link from 'next/link'; import Image from 'next/image'; import { Play, Star } from 'lucide-react'; import { getImageUrl } from '@/lib/tmdb/images'; import { getMediaHref } from '@/lib/media'; -import { getServerLanguage } from '@/lib/i18n/server'; +import { useTranslation } from '@/lib/i18n/context'; import { localizedHref } from '@/lib/i18n/utils'; +import { + showWatchedTotal, + useEpisodeWatchVersion, +} from '@/lib/episode-watch-store'; +import { pickResumableHero } from '@/lib/dashboard-hero'; import { TiltCard } from '@/components/effects/TiltCard'; import { Aurora } from '@/components/effects/Aurora'; import { Spotlight } from '@/components/effects/Spotlight'; @@ -27,19 +34,32 @@ export interface FeaturedHero { } interface DashboardHeroProps { - item: FeaturedHero; + items: FeaturedHero[]; resumeLabel: string; discoverLabel: string; } -/** Cinematic "resume / discover" hero featuring the user's next title to watch. */ -export async function DashboardHero({ - item, +/** + * Cinematic "resume / discover" hero featuring the show the user can pick up right now. + * + * Receives the next few resumable shows so that finishing one from the row below swaps the + * hero to the following one instantly, instead of waiting for the next server render. + */ +export function DashboardHero({ + items, resumeLabel, discoverLabel, }: DashboardHeroProps) { + const { lang } = useTranslation(); + useEpisodeWatchVersion(); + + const item = pickResumableHero(items, showWatchedTotal); + if (!item) return null; + + const watched = item.progress + ? showWatchedTotal(item.id, item.progress.watched) + : 0; const cta = item.resume ? resumeLabel : discoverLabel; - const lang = await getServerLanguage(); return (
@@ -108,8 +128,10 @@ export async function DashboardHero({ {item.progress && item.progress.total > 0 && (
)} diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index be759326..925cae16 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -7,6 +7,7 @@ import { getImageUrl } from '@/lib/tmdb/images'; import { Star, Eye, Clock, Ban } from 'lucide-react'; import { cn } from '@/lib/utils'; import { WatchButton } from '@/components/media/detail/WatchButton'; +import { useShowWatchedTotal } from '@/lib/episode-watch-store'; import { useTranslation } from '@/lib/i18n/context'; import { getLocale, localizedHref } from '@/lib/i18n/utils'; import { formatShortDate } from '@/lib/format'; @@ -52,6 +53,11 @@ export function MediaCard({ }: Props) { const { t, lang } = useTranslation(); const locale = getLocale(lang); + const watchedNow = useShowWatchedTotal(media.id, tvProgress?.watched ?? 0); + const liveProgress = tvProgress && { + ...tvProgress, + watched: watchedNow, + }; // Une note absente (upcoming, titre obscur) affichait un « N/A » doré à étoile, qui // suggère une note inexistante. On masque le badge : décidé côté serveur, donc affiché @@ -136,18 +142,18 @@ export function MediaCard({
)} - {tvProgress && tvProgress.total > 0 && ( + {liveProgress && liveProgress.total > 0 && (
= liveProgress.total ? 'bg-success' : 'bg-linear-to-r from-primary to-gold' )} style={{ - width: `${Math.min(100, Math.round((tvProgress.watched / tvProgress.total) * 100))}%`, + width: `${Math.min(100, Math.round((liveProgress.watched / liveProgress.total) * 100))}%`, }} />
diff --git a/lib/dashboard-hero.ts b/lib/dashboard-hero.ts new file mode 100644 index 00000000..eb843b42 --- /dev/null +++ b/lib/dashboard-hero.ts @@ -0,0 +1,23 @@ +export interface HeroCandidate { + id: number; + progress: { watched: number; total: number } | null; +} + +/** + * Picks the show the hero should feature: the first candidate that still has episodes left, + * so finishing one from the row below promotes the next without a server render. + * + * @param candidates - Resumable shows, most recently watched first. + * @param watchedOf - Live watched count for a show, given its server-rendered count. + */ +export function pickResumableHero( + candidates: readonly T[], + watchedOf: (id: number, serverWatched: number) => number +): T | undefined { + const stillResumable = (candidate: T) => + !candidate.progress || + watchedOf(candidate.id, candidate.progress.watched) < + candidate.progress.total; + + return candidates.find(stillResumable) ?? candidates[0]; +} diff --git a/lib/episode-watch-store.ts b/lib/episode-watch-store.ts index 234844d1..adf27fd2 100644 --- a/lib/episode-watch-store.ts +++ b/lib/episode-watch-store.ts @@ -7,17 +7,27 @@ export interface SeasonWatchState { count: number; episodes: ReadonlySet | null; dirty: boolean; + baseCount: number; } const { entries: seasons, notify, subscribe, + version, } = createKeyedStore(); const seasonKey = (tvId: number, seasonNumber: number) => `${tvId}:${seasonNumber}`; +type SeasonWrite = Omit; + +/** Keeps the server-rendered count alive across mutations so views can derive a session delta. */ +function write(key: string, next: SeasonWrite, countBefore: number) { + const prev = seasons.get(key); + seasons.set(key, { ...next, baseCount: prev?.baseCount ?? countBefore }); +} + /** * Client-side source of truth for per-season watch progress, shared between * episode buttons, season buttons and progress bars so mutations render @@ -37,7 +47,7 @@ export const episodeWatchStore = { ? new Set(episodeNumbers) : (prev?.episodes ?? null); if (prev && prev.count === count && prev.episodes === episodes) return; - seasons.set(key, { count, episodes, dirty: false }); + seasons.set(key, { count, episodes, dirty: false, baseCount: count }); notify(); }, @@ -53,14 +63,22 @@ export const episodeWatchStore = { const episodes = new Set(prev.episodes); if (watched) episodes.add(episodeNumber); else episodes.delete(episodeNumber); - seasons.set(key, { count: episodes.size, episodes, dirty: true }); + write( + key, + { count: episodes.size, episodes, dirty: true }, + prev.count + ); } else { const base = prev?.count ?? (watched ? 0 : 1); - seasons.set(key, { - count: Math.max(0, base + (watched ? 1 : -1)), - episodes: null, - dirty: true, - }); + write( + key, + { + count: Math.max(0, base + (watched ? 1 : -1)), + episodes: null, + dirty: true, + }, + base + ); } notify(); }, @@ -70,12 +88,13 @@ export const episodeWatchStore = { seasonNumber: number, episodeNumbers: number[] ) { + const key = seasonKey(tvId, seasonNumber); const episodes = new Set(episodeNumbers); - seasons.set(seasonKey(tvId, seasonNumber), { - count: episodes.size, - episodes, - dirty: true, - }); + write( + key, + { count: episodes.size, episodes, dirty: true }, + seasons.get(key)?.count ?? episodes.size + ); notify(); }, @@ -84,7 +103,11 @@ export const episodeWatchStore = { const prev = seasons.get(key); const episodes = new Set(prev?.episodes ?? []); for (let i = 1; i <= upToEpisode; i++) episodes.add(i); - seasons.set(key, { count: episodes.size, episodes, dirty: true }); + write( + key, + { count: episodes.size, episodes, dirty: true }, + prev?.count ?? 0 + ); notify(); }, @@ -98,11 +121,11 @@ export const episodeWatchStore = { const episodes = watched ? new Set(Array.from({ length: totalEpisodes }, (_, i) => i + 1)) : new Set(); - seasons.set(key, { - count: watched ? totalEpisodes : 0, - episodes, - dirty: true, - }); + write( + key, + { count: watched ? totalEpisodes : 0, episodes, dirty: true }, + seasons.get(key)?.count ?? (watched ? 0 : totalEpisodes) + ); notify(); }, @@ -125,7 +148,7 @@ export const episodeWatchStore = { const episodes = new Set(prev.episodes); if (watched) episodes.add(episodeNumber); else episodes.delete(episodeNumber); - seasons.set(key, { count: episodes.size, episodes, dirty: true }); + write(key, { count: episodes.size, episodes, dirty: true }, prev.count); notify(); }, @@ -136,13 +159,13 @@ export const episodeWatchStore = { clearShow(tvId: number) { const prefix = `${tvId}:`; let changed = false; - for (const key of seasons.keys()) { + for (const [key, state] of seasons) { if (!key.startsWith(prefix)) continue; - seasons.set(key, { - count: 0, - episodes: new Set(), - dirty: true, - }); + write( + key, + { count: 0, episodes: new Set(), dirty: true }, + state.count + ); changed = true; } if (changed) notify(); @@ -160,6 +183,45 @@ export const episodeWatchStore = { }, }; +/** + * Episodes ticked (or unticked) in this session versus what the server rendered. + * + * @param tvId - Restricts the delta to one show; omit for the whole session. + */ +export function episodeWatchDelta(tvId?: number): number { + const prefix = tvId === undefined ? undefined : `${tvId}:`; + let delta = 0; + for (const [key, state] of seasons) { + if (prefix && !key.startsWith(prefix)) continue; + delta += state.count - state.baseCount; + } + return delta; +} + +/** Reactive `episodeWatchDelta`, so a server-rendered total stays accurate without re-fetching. */ +export function useEpisodeWatchDelta(tvId?: number): number { + return useSyncExternalStore( + subscribe, + () => episodeWatchDelta(tvId), + () => 0 + ); +} + +/** Server-rendered watched count for a show, kept live from this session's episode changes. */ +export function useShowWatchedTotal(tvId: number, serverTotal: number): number { + return serverTotal + useEpisodeWatchDelta(tvId); +} + +/** Same as `useShowWatchedTotal` without subscribing — for views comparing several shows in one pass. */ +export function showWatchedTotal(tvId: number, serverTotal: number): number { + return serverTotal + episodeWatchDelta(tvId); +} + +/** Bumps on every episode change; lets a view re-derive totals for several shows at once. */ +export function useEpisodeWatchVersion(): number { + return useSyncExternalStore(subscribe, version, () => 0); +} + /** Reactive per-season watch state; undefined until seeded or mutated. */ export function useSeasonWatch( tvId: number, diff --git a/lib/media-watch-store.ts b/lib/media-watch-store.ts index 9a92f257..6bceb9d8 100644 --- a/lib/media-watch-store.ts +++ b/lib/media-watch-store.ts @@ -9,6 +9,7 @@ export type MediaWatchStatus = WatchStatus | 'none'; interface MediaWatchState { status: MediaWatchStatus; dirty: boolean; + baseStatus: MediaWatchStatus; } const { @@ -31,12 +32,18 @@ export const mediaWatchStore = { const key = mediaKey(mediaType, mediaId); const prev = media.get(key); if (prev?.dirty || prev?.status === status) return; - media.set(key, { status, dirty: false }); + media.set(key, { status, dirty: false, baseStatus: status }); notify(); }, set(mediaType: MediaType, mediaId: number, status: MediaWatchStatus) { - media.set(mediaKey(mediaType, mediaId), { status, dirty: true }); + const key = mediaKey(mediaType, mediaId); + const prev = media.get(key); + media.set(key, { + status, + dirty: true, + baseStatus: prev?.baseStatus ?? prev?.status ?? 'none', + }); notify(); }, @@ -46,8 +53,13 @@ export const mediaWatchStore = { status: MediaWatchStatus ) { const key = mediaKey(mediaType, mediaId); - if (media.get(key)?.status === status) return; - media.set(key, { status, dirty: true }); + const prev = media.get(key); + if (prev?.status === status) return; + media.set(key, { + status, + dirty: true, + baseStatus: prev?.baseStatus ?? prev?.status ?? 'none', + }); notify(); }, @@ -67,6 +79,38 @@ export const mediaWatchStore = { }, }; +/** + * Net change in how many entries hold `target` since the server rendered. + * + * @param target - Watchlist status being counted. + * @param mediaType - Restricts the count to movies or shows; omit for both. + */ +export function watchStatusDelta( + target: MediaWatchStatus, + mediaType?: MediaType +): number { + let delta = 0; + for (const [key, state] of media) { + if (mediaType && !key.startsWith(`${mediaType}:`)) continue; + if (state.baseStatus === state.status) continue; + if (state.status === target) delta += 1; + else if (state.baseStatus === target) delta -= 1; + } + return delta; +} + +/** Reactive `watchStatusDelta`, so counters stay accurate after a mutation without re-fetching. */ +export function useWatchStatusDelta( + status: MediaWatchStatus, + mediaType?: MediaType +): number { + return useSyncExternalStore( + subscribe, + () => watchStatusDelta(status, mediaType), + () => 0 + ); +} + /** Bumps on every store change; lets list views re-derive buckets from mutated statuses. */ export function useMediaWatchVersion(): number { return useSyncExternalStore(subscribe, version, () => 0); diff --git a/tests/unit/dashboard-hero.test.ts b/tests/unit/dashboard-hero.test.ts new file mode 100644 index 00000000..e53aca4e --- /dev/null +++ b/tests/unit/dashboard-hero.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { pickResumableHero } from '@/lib/dashboard-hero'; + +const show = (id: number, watched: number, total: number) => ({ + id, + progress: { watched, total }, +}); + +const serverCounts = (id: number, serverWatched: number) => serverWatched; + +describe('pickResumableHero', () => { + it('features the first show that still has episodes left', () => { + const picked = pickResumableHero( + [show(1, 10, 10), show(2, 3, 20), show(3, 0, 8)], + serverCounts + ); + expect(picked?.id).toBe(2); + }); + + it('promotes the next show once the current one is finished in session', () => { + const items = [show(1, 9, 10), show(2, 3, 20)]; + const watchedOf = (id: number, serverWatched: number) => + id === 1 ? serverWatched + 1 : serverWatched; + + expect(pickResumableHero(items, serverCounts)?.id).toBe(1); + expect(pickResumableHero(items, watchedOf)?.id).toBe(2); + }); + + it('keeps a show without progress data eligible', () => { + const picked = pickResumableHero( + [{ id: 7, progress: null }, show(2, 3, 20)], + serverCounts + ); + expect(picked?.id).toBe(7); + }); + + it('falls back to the first candidate when every show is finished', () => { + const picked = pickResumableHero( + [show(1, 10, 10), show(2, 20, 20)], + serverCounts + ); + expect(picked?.id).toBe(1); + }); + + it('returns undefined without candidates', () => { + expect(pickResumableHero([], serverCounts)).toBeUndefined(); + }); +}); diff --git a/tests/unit/media-watch-store.test.ts b/tests/unit/media-watch-store.test.ts index 7efc1d2e..31566753 100644 --- a/tests/unit/media-watch-store.test.ts +++ b/tests/unit/media-watch-store.test.ts @@ -14,6 +14,7 @@ describe('mediaWatchStore.seed', () => { expect(mediaWatchStore.get('movie', MOVIE_ID)).toEqual({ status: 'to_watch', dirty: false, + baseStatus: 'to_watch', }); }); @@ -25,6 +26,7 @@ describe('mediaWatchStore.seed', () => { expect(mediaWatchStore.get('movie', MOVIE_ID)).toEqual({ status: 'watched', dirty: true, + baseStatus: 'none', }); }); @@ -48,6 +50,7 @@ describe('mediaWatchStore.set / restore', () => { expect(mediaWatchStore.get('movie', MOVIE_ID)).toEqual({ status: 'none', dirty: true, + baseStatus: 'none', }); }); @@ -61,6 +64,7 @@ describe('mediaWatchStore.set / restore', () => { expect(mediaWatchStore.get('movie', MOVIE_ID)).toEqual({ status: 'to_watch', dirty: false, + baseStatus: 'to_watch', }); }); diff --git a/tests/unit/watch-deltas.test.ts b/tests/unit/watch-deltas.test.ts new file mode 100644 index 00000000..6ac5a8d2 --- /dev/null +++ b/tests/unit/watch-deltas.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { + episodeWatchStore, + episodeWatchDelta, + showWatchedTotal, +} from '@/lib/episode-watch-store'; +import { mediaWatchStore, watchStatusDelta } from '@/lib/media-watch-store'; + +const SHOW = 1399; +const OTHER_SHOW = 615; +const MOVIE = 550; + +describe('episodeWatchDelta', () => { + beforeEach(() => { + for (const season of [1, 2]) { + episodeWatchStore.restore(SHOW, season, undefined); + episodeWatchStore.restore(OTHER_SHOW, season, undefined); + } + }); + + it('is zero for a freshly seeded season', () => { + episodeWatchStore.seed(SHOW, 1, 3, [1, 2, 3]); + + expect(episodeWatchDelta(SHOW)).toBe(0); + }); + + it('counts an episode ticked after the server render', () => { + episodeWatchStore.seed(SHOW, 1, 3, [1, 2, 3]); + + episodeWatchStore.setEpisode(SHOW, 1, 4, true); + + expect(episodeWatchDelta(SHOW)).toBe(1); + expect(showWatchedTotal(SHOW, 9)).toBe(10); + }); + + it('goes negative when an episode is unticked', () => { + episodeWatchStore.seed(SHOW, 1, 3, [1, 2, 3]); + + episodeWatchStore.setEpisode(SHOW, 1, 3, false); + + expect(episodeWatchDelta(SHOW)).toBe(-1); + }); + + it('returns to zero once a mutation is rolled back', () => { + episodeWatchStore.seed(SHOW, 1, 3, [1, 2, 3]); + const previous = episodeWatchStore.get(SHOW, 1); + + episodeWatchStore.setEpisode(SHOW, 1, 4, true); + episodeWatchStore.restore(SHOW, 1, previous); + + expect(episodeWatchDelta(SHOW)).toBe(0); + }); + + it('sums seasons of the same show and ignores the others', () => { + episodeWatchStore.seed(SHOW, 1, 2, [1, 2]); + episodeWatchStore.seed(SHOW, 2, 0, []); + episodeWatchStore.seed(OTHER_SHOW, 1, 5, [1, 2, 3, 4, 5]); + + episodeWatchStore.setEpisode(SHOW, 1, 3, true); + episodeWatchStore.setEpisode(SHOW, 2, 1, true); + episodeWatchStore.setEpisode(OTHER_SHOW, 1, 6, true); + + expect(episodeWatchDelta(SHOW)).toBe(2); + expect(episodeWatchDelta(OTHER_SHOW)).toBe(1); + expect(episodeWatchDelta()).toBe(3); + }); + + it('counts a whole season marked at once', () => { + episodeWatchStore.seed(SHOW, 2, 0, []); + + episodeWatchStore.setSeason(SHOW, 2, true, 10); + + expect(episodeWatchDelta(SHOW)).toBe(10); + }); + + it('drops the show back to its baseline when the show is cleared', () => { + episodeWatchStore.seed(SHOW, 1, 4, [1, 2, 3, 4]); + + episodeWatchStore.clearShow(SHOW); + + expect(episodeWatchDelta(SHOW)).toBe(-4); + }); +}); + +describe('watchStatusDelta', () => { + beforeEach(() => { + mediaWatchStore.restore('movie', MOVIE, undefined); + mediaWatchStore.restore('tv', SHOW, undefined); + }); + + it('is zero for a seeded entry', () => { + mediaWatchStore.seed('movie', MOVIE, 'to_watch'); + + expect(watchStatusDelta('to_watch')).toBe(0); + }); + + it('counts an entry entering the watchlist', () => { + mediaWatchStore.seed('movie', MOVIE, 'none'); + + mediaWatchStore.set('movie', MOVIE, 'to_watch'); + + expect(watchStatusDelta('to_watch')).toBe(1); + }); + + it('counts an entry leaving a status', () => { + mediaWatchStore.seed('movie', MOVIE, 'to_watch'); + + mediaWatchStore.set('movie', MOVIE, 'watched'); + + expect(watchStatusDelta('to_watch')).toBe(-1); + expect(watchStatusDelta('watched')).toBe(1); + }); + + it('filters by media type', () => { + mediaWatchStore.seed('movie', MOVIE, 'none'); + mediaWatchStore.seed('tv', SHOW, 'none'); + + mediaWatchStore.set('movie', MOVIE, 'watched'); + mediaWatchStore.set('tv', SHOW, 'watched'); + + expect(watchStatusDelta('watched', 'movie')).toBe(1); + expect(watchStatusDelta('watched', 'tv')).toBe(1); + expect(watchStatusDelta('watched')).toBe(2); + }); + + it('cancels out when the entry returns to its server status', () => { + mediaWatchStore.seed('movie', MOVIE, 'to_watch'); + + mediaWatchStore.set('movie', MOVIE, 'watched'); + mediaWatchStore.set('movie', MOVIE, 'to_watch'); + + expect(watchStatusDelta('to_watch')).toBe(0); + expect(watchStatusDelta('watched')).toBe(0); + }); +}); From 89053a0b0e4f1fea527ba473a673d4c9f694e778 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 25 Jul 2026 23:48:19 +0200 Subject: [PATCH 15/98] =?UTF-8?q?feat(reco):=20exclure=20les=20titres=20te?= =?UTF-8?q?rmin=C3=A9s=20des=20suggestions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lang]/(protected)/dashboard/page.tsx | 3 +- components/dashboard/DashboardHero.tsx | 14 ++- components/media/detail/WatchButton.tsx | 4 +- lib/dashboard-hero.ts | 20 ++++- lib/episode-watch-store.ts | 21 ++++- lib/recommendations/engine.ts | 33 +++++++ lib/recommendations/sections.ts | 47 +++++++--- lib/tmdb/movies.ts | 10 ++- lib/tmdb/tv.ts | 10 ++- tests/unit/consumed-keys.test.ts | 101 ++++++++++++++++++++++ tests/unit/dashboard-hero.test.ts | 18 ++++ 11 files changed, 246 insertions(+), 35 deletions(-) create mode 100644 tests/unit/consumed-keys.test.ts diff --git a/app/[lang]/(protected)/dashboard/page.tsx b/app/[lang]/(protected)/dashboard/page.tsx index b75fca6a..14771f9b 100644 --- a/app/[lang]/(protected)/dashboard/page.tsx +++ b/app/[lang]/(protected)/dashboard/page.tsx @@ -62,7 +62,8 @@ type Props = { params: Promise<{ lang: Language }>; }; -const HERO_CANDIDATES = 3; +/** Every show of the "continue watching" row, so ticking any of them can promote it to the hero. */ +const HERO_CANDIDATES = 10; async function buildHero( watchlist: Awaited>, diff --git a/components/dashboard/DashboardHero.tsx b/components/dashboard/DashboardHero.tsx index 5dbcb4ef..87442cea 100644 --- a/components/dashboard/DashboardHero.tsx +++ b/components/dashboard/DashboardHero.tsx @@ -8,15 +8,13 @@ import { getMediaHref } from '@/lib/media'; import { useTranslation } from '@/lib/i18n/context'; import { localizedHref } from '@/lib/i18n/utils'; import { + lastTouchedShowId, showWatchedTotal, useEpisodeWatchVersion, } from '@/lib/episode-watch-store'; import { pickResumableHero } from '@/lib/dashboard-hero'; import { TiltCard } from '@/components/effects/TiltCard'; -import { Aurora } from '@/components/effects/Aurora'; -import { Spotlight } from '@/components/effects/Spotlight'; import { Grain } from '@/components/effects/Grain'; -import { PauseWhenOffscreen } from '@/components/effects/PauseWhenOffscreen'; import { GlowBorder } from '@/components/effects/GlowBorder'; import { ProgressBar } from '@/components/shared/ProgressBar'; import { BlurredPosterBackdrop } from '@/components/shared/BlurredPosterBackdrop'; @@ -53,7 +51,11 @@ export function DashboardHero({ const { lang } = useTranslation(); useEpisodeWatchVersion(); - const item = pickResumableHero(items, showWatchedTotal); + const item = pickResumableHero( + items, + showWatchedTotal, + lastTouchedShowId() + ); if (!item) return null; const watched = item.progress @@ -84,10 +86,6 @@ export function DashboardHero({ /> )} - - - -
diff --git a/components/media/detail/WatchButton.tsx b/components/media/detail/WatchButton.tsx index 2bd1cab2..b45eff75 100644 --- a/components/media/detail/WatchButton.tsx +++ b/components/media/detail/WatchButton.tsx @@ -131,7 +131,7 @@ export function WatchButton({ 'border text-sm font-semibold', 'transition-all focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none', isActive - ? 'bg-primary/50 text-white border-white/10 shadow-control-lift' + ? 'bg-primary/50 text-white border-transparent shadow-card-sm' : 'bg-surface/70 text-text border-white/10 hover:bg-surface/85 hover:text-text shadow-card-sm' )} > @@ -157,7 +157,7 @@ export function WatchButton({ 'flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg text-sm font-semibold transition-all border focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none min-h-11 w-full shrink-0', blur && 'backdrop-blur-2xl', isActive - ? 'bg-primary/50 text-white border-white/10 shadow-control-lift' + ? 'bg-primary/50 text-white border-transparent shadow-card-sm' : onDark ? 'bg-black/50 text-white/90 border-white/10 hover:bg-black/65 hover:text-white shadow-card-sm' : 'bg-surface/70 text-text border-white/10 hover:bg-surface/85 hover:text-text shadow-card-sm' diff --git a/lib/dashboard-hero.ts b/lib/dashboard-hero.ts index eb843b42..f74717ef 100644 --- a/lib/dashboard-hero.ts +++ b/lib/dashboard-hero.ts @@ -4,20 +4,32 @@ export interface HeroCandidate { } /** - * Picks the show the hero should feature: the first candidate that still has episodes left, - * so finishing one from the row below promotes the next without a server render. + * Picks the show the hero should feature: the one the user just watched an episode of, else + * the first candidate with episodes left. Both cases resolve client-side, so ticking an + * episode from the row below moves the hero without waiting for a server render. * * @param candidates - Resumable shows, most recently watched first. * @param watchedOf - Live watched count for a show, given its server-rendered count. + * @param justWatchedId - Show ticked most recently in this session, if any. */ export function pickResumableHero( candidates: readonly T[], - watchedOf: (id: number, serverWatched: number) => number + watchedOf: (id: number, serverWatched: number) => number, + justWatchedId?: number | null ): T | undefined { const stillResumable = (candidate: T) => !candidate.progress || watchedOf(candidate.id, candidate.progress.watched) < candidate.progress.total; - return candidates.find(stillResumable) ?? candidates[0]; + const justWatched = + justWatchedId == null + ? undefined + : candidates.find( + (candidate) => + candidate.id === justWatchedId && + stillResumable(candidate) + ); + + return justWatched ?? candidates.find(stillResumable) ?? candidates[0]; } diff --git a/lib/episode-watch-store.ts b/lib/episode-watch-store.ts index adf27fd2..1f1c6612 100644 --- a/lib/episode-watch-store.ts +++ b/lib/episode-watch-store.ts @@ -22,10 +22,17 @@ const seasonKey = (tvId: number, seasonNumber: number) => type SeasonWrite = Omit; -/** Keeps the server-rendered count alive across mutations so views can derive a session delta. */ +const touchOrder = new Map(); +let touchCounter = 0; + +/** + * Keeps the server-rendered count alive across mutations so views can derive a session delta, + * and records which show was touched last so the dashboard hero can follow the user. + */ function write(key: string, next: SeasonWrite, countBefore: number) { const prev = seasons.get(key); seasons.set(key, { ...next, baseCount: prev?.baseCount ?? countBefore }); + touchOrder.set(Number(key.split(':')[0]), ++touchCounter); } /** @@ -222,6 +229,18 @@ export function useEpisodeWatchVersion(): number { return useSyncExternalStore(subscribe, version, () => 0); } +/** + * Show whose episodes were ticked most recently in this session — the one the user is + * actually watching right now, which outranks the order the server sent. + */ +export function lastTouchedShowId(): number | null { + let latest: { tvId: number; order: number } | null = null; + for (const [tvId, order] of touchOrder) { + if (!latest || order > latest.order) latest = { tvId, order }; + } + return latest?.tvId ?? null; +} + /** Reactive per-season watch state; undefined until seeded or mutated. */ export function useSeasonWatch( tvId: number, diff --git a/lib/recommendations/engine.ts b/lib/recommendations/engine.ts index 4336f629..29f1a055 100644 --- a/lib/recommendations/engine.ts +++ b/lib/recommendations/engine.ts @@ -114,6 +114,39 @@ export function genreAffinity( return { favorites, disliked }; } +/** + * Whether the user is done with a title: a watched movie, or a show either marked watched + * or with every episode ticked. A show left mid-way — or abandoned — is not done, so it can + * still be suggested. + * + * @param entry - Watchlist entry to judge. + * @param episodesWatched - Watched episode count per show id. + */ +export function isConsumed( + entry: WatchlistEntry, + episodesWatched: Readonly> +): boolean { + if (entry.media_type === 'movie') return entry.status === 'watched'; + if (entry.status === 'watched') return true; + const total = entry.total_episodes ?? 0; + return total > 0 && (episodesWatched[entry.media_id] ?? 0) >= total; +} + +/** Keys of every title the user is done with — what suggestions must never surface again. */ +export function consumedKeys( + entries: readonly WatchlistEntry[], + episodesWatched: Readonly> +): Set { + const keys = new Set(); + for (const entry of entries) { + if (!isConsumed(entry, episodesWatched)) continue; + keys.add( + getMediaKey({ media_type: entry.media_type, id: entry.media_id }) + ); + } + return keys; +} + export interface DismissedRecommendation { media_id: number; media_type: WatchlistEntry['media_type']; diff --git a/lib/recommendations/sections.ts b/lib/recommendations/sections.ts index b4630d65..dde9587b 100644 --- a/lib/recommendations/sections.ts +++ b/lib/recommendations/sections.ts @@ -23,6 +23,7 @@ import { } from '@/lib/data/watchlist'; import { applyDismissals, + consumedKeys, genreAffinity, isPersonSeedRating, pickFavoritePerson, @@ -44,6 +45,17 @@ type WatchlistWithProgress = Awaited< ReturnType >; +const MIN_ROW_ITEMS = 12; +const MAX_RECOMMENDATION_PAGES = 3; + +const toMediaItems = ( + results: Movie[] | TvShow[], + isMovie: boolean +): MediaItem[] => + isMovie + ? (results as Movie[]).map(movieToMediaItem) + : (results as TvShow[]).map(tvShowToMediaItem); + export interface DashboardSection { title: string; items: MediaItem[]; @@ -224,28 +236,41 @@ export async function buildLibrarySections( const seedCandidates = seeds.map(({ weight }, index) => ({ weight, - items: isMovie - ? (recommendationsResults[index] as Movie[]).map(movieToMediaItem) - : (recommendationsResults[index] as TvShow[]).map( - tvShowToMediaItem - ), + items: toMediaItems(recommendationsResults[index], isMovie), })); - const excludedKeys = new Set( - typeEntries.map((entry) => - getMediaKey({ media_type: entry.media_type, id: entry.media_id }) - ) - ); + const excludedKeys = consumedKeys(typeEntries, tvProgress); const affinity = genreAffinity(typeEntries, ratingByKey); applyDismissals( excludedKeys, affinity, dismissals.filter((dismissal) => dismissal.media_type === type) ); - const forYouItems = rankRecommendations( + + let forYouItems = rankRecommendations( seedCandidates, excludedKeys, affinity ); + for ( + let page = 2; + page <= MAX_RECOMMENDATION_PAGES && forYouItems.length < MIN_ROW_ITEMS; + page++ + ) { + const extraResults = await Promise.all( + seeds.map(({ entry }) => getRecs(entry.media_id, lang, page)) + ); + seedCandidates.push( + ...seeds.map(({ weight }, index) => ({ + weight, + items: toMediaItems(extraResults[index], isMovie), + })) + ); + forYouItems = rankRecommendations( + seedCandidates, + excludedKeys, + affinity + ); + } const onServicesItems = await buildOnServicesItems( type, diff --git a/lib/tmdb/movies.ts b/lib/tmdb/movies.ts index 140d4b2e..d46e38d4 100644 --- a/lib/tmdb/movies.ts +++ b/lib/tmdb/movies.ts @@ -216,12 +216,13 @@ export async function getMovieVideos( */ export async function getMovieRecommendations( id: number, - lang?: Language + lang?: Language, + page = 1 ): Promise { try { const { results } = await fetchTMDB<{ results: Movie[] }>( `/movie/${id}/recommendations`, - {}, + { page: String(page) }, { revalidate: 86400, lang } ); return results; @@ -237,12 +238,13 @@ export async function getMovieRecommendations( */ export async function getSimilarMovies( id: number, - lang?: Language + lang?: Language, + page = 1 ): Promise { try { const { results } = await fetchTMDB<{ results: Movie[] }>( `/movie/${id}/similar`, - {}, + { page: String(page) }, { revalidate: 86400, lang } ); return results; diff --git a/lib/tmdb/tv.ts b/lib/tmdb/tv.ts index d1163195..9e76e3a5 100644 --- a/lib/tmdb/tv.ts +++ b/lib/tmdb/tv.ts @@ -225,12 +225,13 @@ export async function getTvShowImages( */ export async function getTvShowRecommendations( id: number, - lang?: Language + lang?: Language, + page = 1 ): Promise { try { const { results } = await fetchTMDB<{ results: TvShow[] }>( `/tv/${id}/recommendations`, - {}, + { page: String(page) }, { revalidate: 86400, lang } ); return results; @@ -246,12 +247,13 @@ export async function getTvShowRecommendations( */ export async function getSimilarTvShows( id: number, - lang?: Language + lang?: Language, + page = 1 ): Promise { try { const { results } = await fetchTMDB<{ results: TvShow[] }>( `/tv/${id}/similar`, - {}, + { page: String(page) }, { revalidate: 86400, lang } ); return results; diff --git a/tests/unit/consumed-keys.test.ts b/tests/unit/consumed-keys.test.ts new file mode 100644 index 00000000..a7b48cb0 --- /dev/null +++ b/tests/unit/consumed-keys.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from 'vitest'; +import { isConsumed, consumedKeys } from '@/lib/recommendations/engine'; +import type { WatchlistEntry, WatchStatus, MediaType } from '@/types/tmdb'; + +const entry = ( + overrides: Partial & { + media_id: number; + media_type: MediaType; + status: WatchStatus; + } +): WatchlistEntry => ({ + id: `row-${overrides.media_id}`, + user_id: 'user', + media_title: 'Title', + poster_path: null, + created_at: '2026-01-01T00:00:00Z', + total_episodes: null, + release_date: null, + genre_ids: null, + ...overrides, +}); + +const movie = (status: WatchStatus) => + entry({ media_id: 1, media_type: 'movie', status }); + +const show = (status: WatchStatus, total: number | null) => + entry({ media_id: 2, media_type: 'tv', status, total_episodes: total }); + +describe('isConsumed', () => { + it('treats a watched movie as done', () => { + expect(isConsumed(movie('watched'), {})).toBe(true); + }); + + it('keeps a planned movie suggestable', () => { + expect(isConsumed(movie('to_watch'), {})).toBe(false); + }); + + it('treats a show marked watched as done, whatever the episode count', () => { + expect(isConsumed(show('watched', 73), { 2: 9 })).toBe(true); + }); + + it('treats a show with every episode ticked as done', () => { + expect(isConsumed(show('to_watch', 10), { 2: 10 })).toBe(true); + }); + + it('keeps a show in progress suggestable', () => { + expect(isConsumed(show('to_watch', 73), { 2: 9 })).toBe(false); + }); + + it('keeps an abandoned show suggestable', () => { + expect(isConsumed(show('abandoned', 73), { 2: 9 })).toBe(false); + }); + + it('treats an abandoned show that was fully watched as done', () => { + expect(isConsumed(show('abandoned', 10), { 2: 10 })).toBe(true); + }); + + it('keeps a show suggestable when its episode count is unknown', () => { + expect(isConsumed(show('to_watch', null), { 2: 40 })).toBe(false); + }); + + it('keeps a show with no episode watched suggestable', () => { + expect(isConsumed(show('to_watch', 10), {})).toBe(false); + }); +}); + +describe('consumedKeys', () => { + it('keys only the finished titles, by media type', () => { + const keys = consumedKeys( + [ + entry({ media_id: 10, media_type: 'movie', status: 'watched' }), + entry({ + media_id: 11, + media_type: 'movie', + status: 'to_watch', + }), + entry({ + media_id: 12, + media_type: 'tv', + status: 'to_watch', + total_episodes: 4, + }), + entry({ + media_id: 13, + media_type: 'tv', + status: 'to_watch', + total_episodes: 4, + }), + ], + { 12: 4, 13: 1 } + ); + + expect([...keys].sort()).toEqual(['movie-10', 'tv-12']); + }); + + it('is empty when nothing has been finished', () => { + expect( + consumedKeys([movie('to_watch'), show('abandoned', 10)], {}).size + ).toBe(0); + }); +}); diff --git a/tests/unit/dashboard-hero.test.ts b/tests/unit/dashboard-hero.test.ts index e53aca4e..c3724e94 100644 --- a/tests/unit/dashboard-hero.test.ts +++ b/tests/unit/dashboard-hero.test.ts @@ -45,4 +45,22 @@ describe('pickResumableHero', () => { it('returns undefined without candidates', () => { expect(pickResumableHero([], serverCounts)).toBeUndefined(); }); + + it('features the show just watched, even when it is not the first candidate', () => { + const items = [show(1, 3, 20), show(2, 5, 30), show(3, 0, 8)]; + + expect(pickResumableHero(items, serverCounts, 3)?.id).toBe(3); + }); + + it('ignores a just-watched show that has no episode left', () => { + const items = [show(1, 3, 20), show(2, 10, 10)]; + + expect(pickResumableHero(items, serverCounts, 2)?.id).toBe(1); + }); + + it('ignores a just-watched show that is not among the candidates', () => { + const items = [show(1, 3, 20), show(2, 5, 30)]; + + expect(pickResumableHero(items, serverCounts, 99)?.id).toBe(1); + }); }); From 5e81ac6911a3fb514876a258a6d75360b5b52a14 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 00:58:25 +0200 Subject: [PATCH 16/98] =?UTF-8?q?fix(ui):=20liser=C3=A9=20blanc=20sur=20le?= =?UTF-8?q?s=20boutons=20actifs=20en=20th=C3=A8me=20clair?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - shadow-control-lift porte un highlight inset à 0.7 en clair (0.18 en sombre) : sur un fond rouge translucide il dessine un contour blanc détaché - WatchButton, EpisodeWatchButton, SeasonWatchButton et la pastille « Vu » du dashboard passent à shadow-card-sm - le token reste inchangé pour son usage sur fond opaque (StreamingSettings) --- components/dashboard/ContinueWatchingSection.tsx | 2 +- components/media/tv/EpisodeWatchButton.tsx | 2 +- components/media/tv/SeasonWatchButton.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/dashboard/ContinueWatchingSection.tsx b/components/dashboard/ContinueWatchingSection.tsx index 462bdc6f..cb41d5b7 100644 --- a/components/dashboard/ContinueWatchingSection.tsx +++ b/components/dashboard/ContinueWatchingSection.tsx @@ -217,7 +217,7 @@ function ContinueWatchingCard({ : t.movie.markEpisodeWatched} ) : ( - + diff --git a/components/media/tv/EpisodeWatchButton.tsx b/components/media/tv/EpisodeWatchButton.tsx index 52c81a62..78266a71 100644 --- a/components/media/tv/EpisodeWatchButton.tsx +++ b/components/media/tv/EpisodeWatchButton.tsx @@ -110,7 +110,7 @@ export function EpisodeWatchButton({ className={cn( 'flex items-center justify-center gap-2 px-4 py-2 rounded-md text-xs font-medium transition-colors border focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none min-h-10 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer', watched - ? 'bg-primary/40 text-white border-border/10 border-t-border/20 shadow-control-lift' + ? 'bg-primary/40 text-white border-border/10 border-t-border/20 shadow-card-sm' : 'bg-surface/70 text-muted border-border/10 border-t-border/20 hover:text-text hover:bg-surface-2/70 hover:border-border shadow-card-sm' )} > diff --git a/components/media/tv/SeasonWatchButton.tsx b/components/media/tv/SeasonWatchButton.tsx index 611d421c..9531ad5d 100644 --- a/components/media/tv/SeasonWatchButton.tsx +++ b/components/media/tv/SeasonWatchButton.tsx @@ -49,7 +49,7 @@ export function SeasonWatchButton({ 'flex items-center justify-center gap-2 rounded-full text-sm font-semibold shrink-0 border', 'transition-all duration-(--duration-base) focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer', allWatched - ? 'bg-primary/40 text-white border-border/10 border-t-border/20 shadow-control-lift' + ? 'bg-primary/40 text-white border-border/10 border-t-border/20 shadow-card-sm' : 'bg-surface/70 text-rating-gold border-border/10 border-t-border/20 hover:bg-surface/85 hover:text-text' )} > @@ -71,7 +71,7 @@ export function SeasonWatchButton({ className={cn( 'flex items-center justify-center gap-2 px-5 py-2.5 rounded-full text-sm font-semibold transition-all duration-(--duration-base) border focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none min-h-12 w-full sm:w-auto shrink-0 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer shadow-card', allWatched - ? 'bg-primary/40 backdrop-blur-2xl text-white border-border/10 border-t-border/20 shadow-control-lift' + ? 'bg-primary/40 backdrop-blur-2xl text-white border-border/10 border-t-border/20 shadow-card-sm' : 'bg-surface/70 backdrop-blur-2xl text-rating-gold border-border/10 border-t-border/20 hover:bg-surface/85 hover:border-border hover:shadow-glow-gold hover:text-text shadow-card' )} > From e1368cefe63c280beed88f2440a229ed5bf4295f Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 01:48:54 +0200 Subject: [PATCH 17/98] feat(cta): Add CTA for notifications, settings and platforms. --- app/[lang]/(protected)/dashboard/page.tsx | 5 + app/[lang]/(protected)/settings/page.tsx | 26 ++- app/[lang]/layout.tsx | 6 +- app/actions/prompts.ts | 52 +++++ components/dashboard/StreamingPromptCard.tsx | 66 ++++++ .../dashboard/StreamingPromptSection.tsx | 27 +++ components/media/detail/WatchButton.tsx | 4 + .../notifications/NotificationToast.tsx | 66 ++++++ .../notifications/NotificationsProvider.tsx | 53 ++++- components/prompts/IosInstallHint.tsx | 24 +++ components/prompts/PromptBanner.tsx | 67 ++++++ components/prompts/PromptHost.tsx | 199 ++++++++++++++++++ components/prompts/PromptSlot.tsx | 38 ++++ components/settings/SettingsContent.tsx | 4 +- components/settings/SettingsNav.tsx | 27 ++- components/shared/PWAInstallPrompt.tsx | 94 --------- hooks/usePWAInstall.ts | 65 +++--- lib/i18n/messages/notifications.ts | 6 +- lib/i18n/messages/prompts.ts | 41 ++++ lib/i18n/messages/pwa.ts | 10 +- lib/i18n/translations.ts | 3 + lib/prompts/device.ts | 69 ++++++ lib/prompts/engine.ts | 44 ++++ lib/prompts/keys.ts | 16 ++ lib/prompts/store.ts | 108 ++++++++++ tests/e2e/pwa-install-prompt.spec.ts | 103 +++++++++ tests/unit/prompts-engine.test.ts | 81 +++++++ types/database.ts | 21 ++ 28 files changed, 1172 insertions(+), 153 deletions(-) create mode 100644 app/actions/prompts.ts create mode 100644 components/dashboard/StreamingPromptCard.tsx create mode 100644 components/dashboard/StreamingPromptSection.tsx create mode 100644 components/notifications/NotificationToast.tsx create mode 100644 components/prompts/IosInstallHint.tsx create mode 100644 components/prompts/PromptBanner.tsx create mode 100644 components/prompts/PromptHost.tsx create mode 100644 components/prompts/PromptSlot.tsx delete mode 100644 components/shared/PWAInstallPrompt.tsx create mode 100644 lib/i18n/messages/prompts.ts create mode 100644 lib/prompts/device.ts create mode 100644 lib/prompts/engine.ts create mode 100644 lib/prompts/keys.ts create mode 100644 lib/prompts/store.ts create mode 100644 tests/e2e/pwa-install-prompt.spec.ts create mode 100644 tests/unit/prompts-engine.test.ts diff --git a/app/[lang]/(protected)/dashboard/page.tsx b/app/[lang]/(protected)/dashboard/page.tsx index 14771f9b..91f7351a 100644 --- a/app/[lang]/(protected)/dashboard/page.tsx +++ b/app/[lang]/(protected)/dashboard/page.tsx @@ -29,6 +29,7 @@ import { import { BentoStats } from '@/components/dashboard/BentoStats'; import { TrendingMarquee } from '@/components/dashboard/TrendingMarquee'; import { ContinueWatchingSection } from '@/components/dashboard/ContinueWatchingSection'; +import { StreamingPromptSection } from '@/components/dashboard/StreamingPromptSection'; import { ContinueWatchingSkeleton } from '@/components/dashboard/ContinueWatchingSkeleton'; import { DashboardHeroSkeleton, @@ -327,6 +328,10 @@ export default async function DashboardPage({ params: paramsPromise }: Props) { + + + + }> diff --git a/app/[lang]/(protected)/settings/page.tsx b/app/[lang]/(protected)/settings/page.tsx index 59dc3371..4ba02087 100644 --- a/app/[lang]/(protected)/settings/page.tsx +++ b/app/[lang]/(protected)/settings/page.tsx @@ -3,6 +3,10 @@ import { requireAuth } from '@/lib/auth'; import { createClient } from '@/lib/supabase/server'; import { isOAuthOnly } from '@/lib/supabase/auth-helpers'; import { SettingsContent } from '@/components/settings/SettingsContent'; +import { + isSettingsTab, + type SettingsTab, +} from '@/components/settings/SettingsNav'; import { SettingsContentSkeleton } from '@/components/settings/SettingsContentSkeleton'; import { PageLayout, PageHeader } from '@/components/layout/PageLayout'; import { getTranslations } from '@/lib/i18n/server'; @@ -16,6 +20,7 @@ import type { User } from '@supabase/supabase-js'; type Props = { params: Promise<{ lang: Language }>; + searchParams: Promise<{ section?: string }>; }; export async function generateMetadata({ params }: Props) { @@ -32,7 +37,15 @@ export async function generateMetadata({ params }: Props) { }; } -async function SettingsSection({ user, lang }: { user: User; lang: Language }) { +async function SettingsSection({ + user, + lang, + initialTab, +}: { + user: User; + lang: Language; + initialTab: SettingsTab; +}) { const supabase = await createClient(); const [profileResult, privacyResult, region, selectedProviderIds] = @@ -65,12 +78,13 @@ async function SettingsSection({ user, lang }: { user: User; lang: Language }) { isOAuthOnly={isOAuthOnly(user)} streamingProviders={streamingProviders} selectedProviderIds={selectedProviderIds} + initialTab={initialTab} /> ); } -export default async function SettingsPage({ params }: Props) { - const { lang } = await params; +export default async function SettingsPage({ params, searchParams }: Props) { + const [{ lang }, { section }] = await Promise.all([params, searchParams]); const user = await requireAuth(); const t = await getTranslations(lang); @@ -81,7 +95,11 @@ export default async function SettingsPage({ params }: Props) { subtitle={t.settings.subtitle} /> }> - + ); diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx index 268673fa..77b72593 100644 --- a/app/[lang]/layout.tsx +++ b/app/[lang]/layout.tsx @@ -14,7 +14,7 @@ import { notFound } from 'next/navigation'; import { BASE_URL, DEFAULT_OG_IMAGE } from '@/lib/metadata'; import { ScrollToTop } from '@/components/layout/ScrollToTop'; import { PreventImageContextMenu } from '@/components/shared/PreventImageContextMenu'; -import { PWAInstallPrompt } from '@/components/shared/PWAInstallPrompt'; +import { PromptSlot } from '@/components/prompts/PromptSlot'; import { SupportBadge } from '@/components/support/SupportBadge'; import NextTopLoader from 'nextjs-toploader'; import Link from 'next/link'; @@ -407,7 +407,9 @@ export default async function RootLayout({
- + + + diff --git a/app/actions/prompts.ts b/app/actions/prompts.ts new file mode 100644 index 00000000..c520e79a --- /dev/null +++ b/app/actions/prompts.ts @@ -0,0 +1,52 @@ +'use server'; + +import { getAuthenticatedUser, getOptionalUser } from '@/lib/supabase/auth-helpers'; +import { + ACCOUNT_PROMPTS, + PROMPT_KEYS, + type PromptKey, + type PromptState, + type PromptStates, +} from '@/lib/prompts/keys'; + +const VALID_STATES = new Set(['done', 'dismissed']); + +/** Answers already given to account-scoped call-to-actions, so none is ever asked twice. */ +export async function getPromptStates(): Promise { + const { supabase, userId } = await getOptionalUser(); + if (!userId) return {}; + + const { data } = await supabase + .from('user_prompts') + .select('prompt_key, state') + .eq('user_id', userId); + + const states: PromptStates = {}; + for (const row of data ?? []) { + const key = row.prompt_key as PromptKey; + if (PROMPT_KEYS.includes(key)) states[key] = row.state as PromptState; + } + return states; +} + +/** Records the user's answer to a call-to-action; deliberately never revalidates. */ +export async function resolvePrompt( + key: PromptKey, + state: PromptState +): Promise { + if (!ACCOUNT_PROMPTS.has(key)) throw new Error('Invalid prompt_key'); + if (!VALID_STATES.has(state)) throw new Error('Invalid state'); + + const { supabase, userId } = await getAuthenticatedUser(); + + const { error } = await supabase.from('user_prompts').upsert( + { + user_id: userId, + prompt_key: key, + state, + updated_at: new Date().toISOString(), + }, + { onConflict: 'user_id,prompt_key' } + ); + if (error) throw new Error(error.message); +} diff --git a/components/dashboard/StreamingPromptCard.tsx b/components/dashboard/StreamingPromptCard.tsx new file mode 100644 index 00000000..337186f0 --- /dev/null +++ b/components/dashboard/StreamingPromptCard.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Tv } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { resolvePrompt } from '@/app/actions/prompts'; +import type { PromptState } from '@/lib/prompts/keys'; +import { useTranslation } from '@/lib/i18n/context'; +import { localizedHref } from '@/lib/i18n/utils'; +import { reportSwallowed } from '@/lib/report'; + +/** Stands in for the "on your services" row while the user has picked no platform. */ +export function StreamingPromptCard() { + const { t, lang } = useTranslation(); + const router = useRouter(); + const [hidden, setHidden] = useState(false); + + if (hidden) return null; + + function settle(state: PromptState) { + setHidden(true); + resolvePrompt('streaming', state).catch((error) => + reportSwallowed('prompts:resolve', error) + ); + } + + return ( +
+ + + + +
+

+ {t.prompts.streaming.title} +

+

+ {t.prompts.streaming.description} +

+
+ +
+ + +
+
+ ); +} diff --git a/components/dashboard/StreamingPromptSection.tsx b/components/dashboard/StreamingPromptSection.tsx new file mode 100644 index 00000000..a58da55c --- /dev/null +++ b/components/dashboard/StreamingPromptSection.tsx @@ -0,0 +1,27 @@ +import { getMyStreamingProviders } from '@/app/actions/recommendations'; +import { getPromptStates } from '@/app/actions/prompts'; +import { getUserContext } from '@/lib/supabase/auth-helpers'; +import { StreamingPromptCard } from '@/components/dashboard/StreamingPromptCard'; + +/** Enough of a library for "what's on my platforms" to return anything useful. */ +const MIN_WATCHLIST_ENTRIES = 5; + +export async function StreamingPromptSection() { + const { supabase, user } = await getUserContext(); + if (!user) return null; + + const [providerIds, states, watchlist] = await Promise.all([ + getMyStreamingProviders(), + getPromptStates(), + supabase + .from('watchlist') + .select('id', { count: 'exact', head: true }) + .eq('user_id', user.id), + ]); + + if (providerIds.length > 0) return null; + if (states.streaming !== undefined) return null; + if ((watchlist.count ?? 0) < MIN_WATCHLIST_ENTRIES) return null; + + return ; +} diff --git a/components/media/detail/WatchButton.tsx b/components/media/detail/WatchButton.tsx index b45eff75..77868854 100644 --- a/components/media/detail/WatchButton.tsx +++ b/components/media/detail/WatchButton.tsx @@ -13,6 +13,7 @@ import { useIsUnreleased } from '@/hooks/useIsUnreleased'; import { mediaWatchStore, useMediaWatch } from '@/lib/media-watch-store'; import { episodeWatchStore } from '@/lib/episode-watch-store'; import { mediaRatingStore } from '@/lib/media-rating-store'; +import { promptStore } from '@/lib/prompts/store'; import type { WatchButtonProps } from '@/types/components'; const ReviewDialog = dynamic( @@ -99,6 +100,9 @@ export function WatchButton({ if (mediaType === 'tv' && target === 'none') { episodeWatchStore.clearShow(mediaId); } + if (mediaType === 'tv' && target !== 'none') { + promptStore.requestPush(); + } if (changesMembership) router.refresh(); if (target === 'watched') setReviewOpen(true); }, diff --git a/components/notifications/NotificationToast.tsx b/components/notifications/NotificationToast.tsx new file mode 100644 index 00000000..eeef7e61 --- /dev/null +++ b/components/notifications/NotificationToast.tsx @@ -0,0 +1,66 @@ +'use client'; + +import Image from 'next/image'; +import { UserAvatar } from '@/components/shared/UserAvatar'; +import { cn } from '@/lib/utils'; +import type { AppNotification } from '@/types/notifications'; + +interface NotificationToastProps { + notification: AppNotification; + message: string; + openLabel: string; + onOpen?: () => void; +} + +/** Live notification popped by the realtime channel, with the sender avatar or the poster. */ +export function NotificationToast({ + notification, + message, + openLabel, + onOpen, +}: NotificationToastProps) { + const visual = notification.type.startsWith('friend') ? ( + + ) : notification.posterPath ? ( + + ) : null; + + const content = ( +
+ {visual} +

+ {message} +

+
+ ); + + const surface = + 'w-full rounded-xl border border-border-subtle bg-surface p-3 shadow-card'; + + if (!onOpen) return
{content}
; + + return ( + + ); +} diff --git a/components/notifications/NotificationsProvider.tsx b/components/notifications/NotificationsProvider.tsx index 8b8e8cf0..63668e99 100644 --- a/components/notifications/NotificationsProvider.tsx +++ b/components/notifications/NotificationsProvider.tsx @@ -8,14 +8,20 @@ import { useCallback, type ReactNode, } from 'react'; +import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { createClient } from '@/lib/supabase/client'; import { getUnreadCount, markAllNotificationsRead, + markNotificationRead, } from '@/app/actions/notifications'; +import { NotificationToast } from '@/components/notifications/NotificationToast'; import { rowToAppNotification, notificationMessage } from '@/lib/notifications'; +import { promptStore } from '@/lib/prompts/store'; +import { reportSwallowed } from '@/lib/report'; import { useTranslation } from '@/lib/i18n/context'; +import { localizedHref } from '@/lib/i18n/utils'; interface NotificationsContextValue { unreadCount: number; @@ -42,7 +48,8 @@ export function NotificationsProvider({ initialUnreadCount, children, }: ProviderProps) { - const { t } = useTranslation(); + const { t, lang } = useTranslation(); + const router = useRouter(); const [unreadCount, setUnreadCount] = useState(initialUnreadCount); const refresh = useCallback(async () => { @@ -77,7 +84,40 @@ export function NotificationsProvider({ typeof rowToAppNotification >[0] ); - toast(notificationMessage(n, t.notifications.templates)); + if (n.type === 'friend_request') promptStore.requestPush(); + + const url = n.url; + toast.custom((id) => ( + { + toast.dismiss(id); + setUnreadCount((c) => + Math.max(0, c - 1) + ); + void markNotificationRead( + n.id + ).catch((error) => + reportSwallowed( + 'notifications:markRead', + error + ) + ); + router.push( + localizedHref(lang, url) + ); + } + : undefined + } + /> + )); } ) .on( @@ -105,7 +145,14 @@ export function NotificationsProvider({ return () => { void supabase.removeChannel(channel); }; - }, [userId, refresh, t.notifications.templates]); + }, [ + userId, + refresh, + router, + lang, + t.notifications.templates, + t.notifications.open, + ]); return ( + {t.pwa.iosSafariOnly} + + + {t.pwa.iosShare} + + {t.pwa.iosThen} + + + {t.pwa.iosAdd} + + + ); +} diff --git a/components/prompts/PromptBanner.tsx b/components/prompts/PromptBanner.tsx new file mode 100644 index 00000000..c32aa43d --- /dev/null +++ b/components/prompts/PromptBanner.tsx @@ -0,0 +1,67 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +interface PromptBannerProps { + visible: boolean; + icon: ReactNode; + title: string; + description: ReactNode; + action?: ReactNode; + dismissLabel: string; + onDismiss: () => void; +} + +/** Single slot under the navbar where every call-to-action is rendered, one at a time. */ +export function PromptBanner({ + visible, + icon, + title, + description, + action, + dismissLabel, + onDismiss, +}: PromptBannerProps) { + return ( +
+
+ {icon} + +
+

+ {title} +

+
+ {description} +
+
+ + {action} + + +
+
+ ); +} diff --git a/components/prompts/PromptHost.tsx b/components/prompts/PromptHost.tsx new file mode 100644 index 00000000..b57e8cca --- /dev/null +++ b/components/prompts/PromptHost.tsx @@ -0,0 +1,199 @@ +'use client'; + +import { useEffect, useState, type ReactNode } from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import { Bell, FolderDown } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { PromptBanner } from '@/components/prompts/PromptBanner'; +import { IosInstallHint } from '@/components/prompts/IosInstallHint'; +import { resolvePrompt } from '@/app/actions/prompts'; +import { usePWAInstall, type InstallMode } from '@/hooks/usePWAInstall'; +import { usePushSubscription } from '@/hooks/usePushSubscription'; +import { countVisit } from '@/lib/prompts/device'; +import { + ACCOUNT_PROMPTS, + type PromptKey, + type PromptState, + type PromptStates, +} from '@/lib/prompts/keys'; +import { + promptStore, + usePromptSlot, + usePushRequested, +} from '@/lib/prompts/store'; +import { useTranslation } from '@/lib/i18n/context'; +import type { Translations } from '@/lib/i18n/translations'; +import { localizedHref } from '@/lib/i18n/utils'; +import { reportSwallowed } from '@/lib/report'; + +/** Offering an import only makes sense to someone who came back at least once. */ +const IMPORT_MIN_VISITS = 2; + +interface PromptCopy { + icon: ReactNode; + title: string; + description: ReactNode; + actionLabel: string | null; +} + +function installCopy(t: Translations, mode: InstallMode): PromptCopy { + const icon = ( + + ); + + if (mode === 'ios-safari') + return { + icon, + title: t.pwa.title, + description: , + actionLabel: null, + }; + + if (mode === 'ios-other') + return { + icon, + title: t.pwa.title, + description: t.pwa.iosWrongBrowser, + actionLabel: null, + }; + + return { + icon, + title: t.pwa.title, + description: t.pwa.description, + actionLabel: t.pwa.install, + }; +} + +function promptCopy( + key: PromptKey, + t: Translations, + mode: InstallMode +): PromptCopy { + if (key === 'install') return installCopy(t, mode); + + if (key === 'push') + return { + icon: , + title: t.prompts.push.title, + description: t.prompts.push.description, + actionLabel: t.prompts.push.action, + }; + + return { + icon: , + title: t.prompts.import.title, + description: t.prompts.import.description, + actionLabel: t.prompts.import.action, + }; +} + +interface PromptHostProps { + initialStates: PromptStates; + accountCreatedAt: number | null; + canImport: boolean; +} + +/** Renders the single call-to-action the prompt engine allows for this session, if any. */ +export function PromptHost({ + initialStates, + accountCreatedAt, + canImport, +}: PromptHostProps) { + const { t, lang } = useTranslation(); + const router = useRouter(); + const pwa = usePWAInstall(); + const push = usePushSubscription(); + const pushRequested = usePushRequested(); + const { active, open } = usePromptSlot(); + const [pending, setPending] = useState(false); + + useEffect(() => { + const eligible: PromptKey[] = []; + if (pwa.visible) eligible.push('install'); + if (pushRequested && push.status === 'off') eligible.push('push'); + if (canImport && countVisit() >= IMPORT_MIN_VISITS) + eligible.push('import'); + + promptStore.evaluate( + eligible, + initialStates, + accountCreatedAt, + Date.now() + ); + }, [ + pwa.visible, + pushRequested, + push.status, + canImport, + initialStates, + accountCreatedAt, + ]); + + if (active === null) return null; + + const copy = promptCopy(active, t, pwa.mode); + + async function settle(key: PromptKey, state: PromptState) { + promptStore.resolve(key, state); + if (!ACCOUNT_PROMPTS.has(key)) return; + + try { + await resolvePrompt(key, state); + } catch (error) { + reportSwallowed('prompts:resolve', error); + } + } + + async function handleAction() { + if (active === null) return; + setPending(true); + try { + if (active === 'install') { + await pwa.triggerInstall(); + await settle('install', 'done'); + return; + } + if (active === 'push') { + await push.enable(); + await settle('push', 'done'); + return; + } + router.push(localizedHref(lang, '/settings?section=data')); + await settle('import', 'done'); + } finally { + setPending(false); + } + } + + return ( + void settle(active, 'dismissed')} + action={ + copy.actionLabel && ( + + ) + } + /> + ); +} diff --git a/components/prompts/PromptSlot.tsx b/components/prompts/PromptSlot.tsx new file mode 100644 index 00000000..69f6c266 --- /dev/null +++ b/components/prompts/PromptSlot.tsx @@ -0,0 +1,38 @@ +import { getUserContext } from '@/lib/supabase/auth-helpers'; +import { getPromptStates } from '@/app/actions/prompts'; +import { PromptHost } from '@/components/prompts/PromptHost'; + +/** Below this many watchlist entries, importing an existing library is worth offering. */ +const IMPORT_THRESHOLD = 3; + +/** Feeds the call-to-action slot with what only the server knows; anonymous visitors still get the install banner. */ +export async function PromptSlot() { + const { supabase, user } = await getUserContext(); + + if (!user) + return ( + + ); + + const [states, watchlist] = await Promise.all([ + getPromptStates(), + supabase + .from('watchlist') + .select('id', { count: 'exact', head: true }) + .eq('user_id', user.id), + ]); + + const createdAt = Date.parse(user.created_at); + + return ( + + ); +} diff --git a/components/settings/SettingsContent.tsx b/components/settings/SettingsContent.tsx index d77cc6f6..f8570e6a 100644 --- a/components/settings/SettingsContent.tsx +++ b/components/settings/SettingsContent.tsx @@ -30,6 +30,7 @@ interface SettingsContentProps { isOAuthOnly: boolean; streamingProviders: WatchProvider[]; selectedProviderIds: number[]; + initialTab: SettingsTab; } export function SettingsContent({ @@ -39,8 +40,9 @@ export function SettingsContent({ isOAuthOnly, streamingProviders, selectedProviderIds, + initialTab, }: SettingsContentProps) { - const [activeTab, setActiveTab] = useState('profile'); + const [activeTab, setActiveTab] = useState(initialTab); return (
diff --git a/components/settings/SettingsNav.tsx b/components/settings/SettingsNav.tsx index d6149da0..6dbf9e9c 100644 --- a/components/settings/SettingsNav.tsx +++ b/components/settings/SettingsNav.tsx @@ -3,14 +3,25 @@ import { cn } from '@/lib/utils'; import { useTranslation } from '@/lib/i18n/context'; -export type SettingsTab = - | 'profile' - | 'security' - | 'notifications' - | 'appearance' - | 'services' - | 'data' - | 'privacy'; +export const SETTINGS_TABS = [ + 'profile', + 'security', + 'notifications', + 'appearance', + 'services', + 'data', + 'privacy', +] as const; + +export type SettingsTab = (typeof SETTINGS_TABS)[number]; + +/** Narrows the `?section=` query param used by the call-to-actions that deep-link here. */ +export function isSettingsTab(value: string | undefined): value is SettingsTab { + return ( + value !== undefined && + (SETTINGS_TABS as readonly string[]).includes(value) + ); +} interface SettingsNavProps { onTabChange: (tab: SettingsTab) => void; diff --git a/components/shared/PWAInstallPrompt.tsx b/components/shared/PWAInstallPrompt.tsx deleted file mode 100644 index f094d05c..00000000 --- a/components/shared/PWAInstallPrompt.tsx +++ /dev/null @@ -1,94 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import Image from 'next/image'; -import { Share, SquarePlus, X } from 'lucide-react'; -import { usePWAInstall } from '@/hooks/usePWAInstall'; -import { useTranslation } from '@/lib/i18n/context'; -import { Button } from '@/components/ui/button'; -import { cn } from '@/lib/utils'; - -export function PWAInstallPrompt() { - const { visible, isIOS, dismiss, triggerInstall } = usePWAInstall(); - const { t } = useTranslation(); - const [installing, setInstalling] = useState(false); - - async function handleInstall() { - setInstalling(true); - await triggerInstall(); - setInstalling(false); - } - - return ( -
-
- ReelMark - -
-

- ReelMark -

- {isIOS ? ( -
- - - {t.pwa.iosShare} - - - {t.pwa.iosThen} - - - - {t.pwa.iosAdd} - -
- ) : ( -

- {t.pwa.description} -

- )} -
- - {!isIOS && ( - - )} - - -
-
- ); -} diff --git a/hooks/usePWAInstall.ts b/hooks/usePWAInstall.ts index 8ac50a9d..b3928ab5 100644 --- a/hooks/usePWAInstall.ts +++ b/hooks/usePWAInstall.ts @@ -7,18 +7,10 @@ interface BeforeInstallPromptEvent extends Event { userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; } -const DISMISS_KEY = 'pwa-install-dismissed'; -const DISMISS_TTL_DAYS = 30; - -function isDismissed(): boolean { - try { - const raw = localStorage.getItem(DISMISS_KEY); - if (!raw) return false; - return Date.now() - Number(raw) < DISMISS_TTL_DAYS * 86_400_000; - } catch { - return false; - } -} +/** Let the page settle before asking anything. */ +const REVEAL_DELAY_MS = 3000; + +export type InstallMode = 'prompt' | 'ios-safari' | 'ios-other'; function isStandalone(): boolean { return ( @@ -35,6 +27,14 @@ function detectIOS(): boolean { return /iPad|iPhone|iPod/.test(ua) || isIPadOS; } +/** + * Every iOS browser runs WebKit, but only Safari itself exposes "Add to Home Screen", + * so anywhere else the user has to be sent back to Safari first. + */ +function isIOSSafari(): boolean { + return !/CriOS|FxiOS|EdgiOS|OPiOS|GSA|DuckDuckGo/.test(navigator.userAgent); +} + function isMobile(): boolean { return ( /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || @@ -42,20 +42,26 @@ function isMobile(): boolean { ); } +/** Whether this device can still install the app, and by which route. */ export function usePWAInstall() { - const [state, setState] = useState({ visible: false, isIOS: false }); + const [state, setState] = useState<{ visible: boolean; mode: InstallMode }>({ + visible: false, + mode: 'prompt', + }); const promptRef = useRef(null); useEffect(() => { - if (!isMobile() || isStandalone() || isDismissed()) return; + if (!isMobile() || isStandalone()) return; - const ios = detectIOS(); let timer: ReturnType; - if (ios) { + if (detectIOS()) { + const mode: InstallMode = isIOSSafari() + ? 'ios-safari' + : 'ios-other'; timer = setTimeout( - () => setState({ visible: true, isIOS: true }), - 3000 + () => setState({ visible: true, mode }), + REVEAL_DELAY_MS ); return () => clearTimeout(timer); } @@ -64,8 +70,8 @@ export function usePWAInstall() { e.preventDefault(); promptRef.current = e as BeforeInstallPromptEvent; timer = setTimeout( - () => setState({ visible: true, isIOS: false }), - 3000 + () => setState({ visible: true, mode: 'prompt' }), + REVEAL_DELAY_MS ); }; @@ -76,27 +82,12 @@ export function usePWAInstall() { }; }, []); - function dismiss() { - try { - localStorage.setItem(DISMISS_KEY, String(Date.now())); - } catch {} - setState((s) => ({ ...s, visible: false })); - } - async function triggerInstall() { const prompt = promptRef.current; if (!prompt) return; await prompt.prompt(); - const { outcome } = await prompt.userChoice; - if (outcome === 'accepted') { - setState((s) => ({ ...s, visible: false })); - } + await prompt.userChoice; } - return { - visible: state.visible, - isIOS: state.isIOS, - dismiss, - triggerInstall, - }; + return { visible: state.visible, mode: state.mode, triggerInstall }; } diff --git a/lib/i18n/messages/notifications.ts b/lib/i18n/messages/notifications.ts index faf033f8..895bd090 100644 --- a/lib/i18n/messages/notifications.ts +++ b/lib/i18n/messages/notifications.ts @@ -5,9 +5,10 @@ const fr = { seeAll: 'Voir tout', new: 'Nouveau', delete: 'Supprimer', + open: 'Ouvrir la notification', templates: { - friend_request: '{user} vous a envoyé une demande d’ami', - friend_accepted: '{user} a accepté votre demande d’ami', + friend_request: '{user} t’a envoyé une demande d’ami', + friend_accepted: '{user} a accepté ta demande d’ami', new_episode: 'Nouvel épisode de {title} : S{season}E{episode}', suggestion: 'À voir : {title}', }, @@ -20,6 +21,7 @@ const en = { seeAll: 'See all', new: 'New', delete: 'Delete', + open: 'Open notification', templates: { friend_request: '{user} sent you a friend request', friend_accepted: '{user} accepted your friend request', diff --git a/lib/i18n/messages/prompts.ts b/lib/i18n/messages/prompts.ts new file mode 100644 index 00000000..12678e04 --- /dev/null +++ b/lib/i18n/messages/prompts.ts @@ -0,0 +1,41 @@ +const fr = { + dismiss: 'Plus tard', + push: { + title: 'Activer les notifications', + description: 'Sois prévenu dès qu’un nouvel épisode de tes séries sort.', + action: 'Activer', + }, + import: { + title: 'Importer ta bibliothèque', + description: 'Depuis Letterboxd, Trakt ou un export ReelMark.', + action: 'Importer', + }, + streaming: { + title: 'Tes plateformes de streaming', + description: + 'Indique tes abonnements pour voir en priorité ce qui est déjà inclus chez toi.', + action: 'Choisir', + }, +}; + +const en = { + dismiss: 'Later', + push: { + title: 'Turn on notifications', + description: 'Get told as soon as a new episode of your shows airs.', + action: 'Turn on', + }, + import: { + title: 'Import your library', + description: 'From Letterboxd, Trakt, or a ReelMark export.', + action: 'Import', + }, + streaming: { + title: 'Your streaming platforms', + description: + 'Tell us what you subscribe to and see what is already included first.', + action: 'Choose', + }, +} satisfies typeof fr; + +export const prompts = { fr, en }; diff --git a/lib/i18n/messages/pwa.ts b/lib/i18n/messages/pwa.ts index b92ad49c..c7781c59 100644 --- a/lib/i18n/messages/pwa.ts +++ b/lib/i18n/messages/pwa.ts @@ -1,11 +1,14 @@ const fr = { title: 'Installer ReelMark', - description: "Ajoutez l'application sur votre écran d'accueil.", + description: 'Ajoute l’application sur ton écran d’accueil.', install: 'Installer', dismiss: 'Plus tard', iosThen: 'puis', iosShare: 'Partager', - iosAdd: "Écran d'accueil", + iosAdd: 'Écran d’accueil', + iosSafariOnly: 'Dans Safari :', + iosWrongBrowser: + 'Ouvre ReelMark dans Safari : c’est le seul navigateur qui installe une app sur iPhone.', }; const en = { @@ -16,6 +19,9 @@ const en = { iosThen: 'then', iosShare: 'Share', iosAdd: 'Home Screen', + iosSafariOnly: 'In Safari:', + iosWrongBrowser: + 'Open ReelMark in Safari — it is the only browser that can install an app on iPhone.', } satisfies typeof fr; export const pwa = { fr, en }; diff --git a/lib/i18n/translations.ts b/lib/i18n/translations.ts index 8fb5242d..2ebde362 100644 --- a/lib/i18n/translations.ts +++ b/lib/i18n/translations.ts @@ -2,6 +2,7 @@ import { common } from './messages/common'; import { pwa } from './messages/pwa'; import { navbar } from './messages/navbar'; import { notifications } from './messages/notifications'; +import { prompts } from './messages/prompts'; import { hero } from './messages/hero'; import { features } from './messages/features'; import { explorer } from './messages/explorer'; @@ -26,6 +27,7 @@ export const translations = { pwa: pwa.fr, navbar: navbar.fr, notifications: notifications.fr, + prompts: prompts.fr, hero: hero.fr, features: features.fr, explorer: explorer.fr, @@ -49,6 +51,7 @@ export const translations = { pwa: pwa.en, navbar: navbar.en, notifications: notifications.en, + prompts: prompts.en, hero: hero.en, features: features.en, explorer: explorer.en, diff --git a/lib/prompts/device.ts b/lib/prompts/device.ts new file mode 100644 index 00000000..91245a77 --- /dev/null +++ b/lib/prompts/device.ts @@ -0,0 +1,69 @@ +import { + DEVICE_PROMPTS, + type PromptKey, + type PromptState, + type PromptStates, +} from './keys'; + +const STATE_PREFIX = 'reelmark:prompt:'; +const LAST_SHOWN_KEY = 'reelmark:prompt-last-shown'; +const VISIT_COUNT_KEY = 'reelmark:prompt-visits'; +const VISIT_FLAG_KEY = 'reelmark:prompt-visit-counted'; + +/** Answers to device-scoped prompts, stored per browser since a push permission is. */ +export function readDeviceStates(): PromptStates { + const states: PromptStates = {}; + try { + for (const key of DEVICE_PROMPTS) { + const raw = localStorage.getItem(`${STATE_PREFIX}${key}`); + if (raw === 'done' || raw === 'dismissed') states[key] = raw; + } + } catch { + return states; + } + return states; +} + +export function writeDeviceState(key: PromptKey, state: PromptState): void { + try { + localStorage.setItem(`${STATE_PREFIX}${key}`, state); + } catch { + return; + } +} + +export function readLastShownAt(): number | null { + try { + const raw = localStorage.getItem(LAST_SHOWN_KEY); + const parsed = raw === null ? NaN : Number(raw); + return Number.isFinite(parsed) ? parsed : null; + } catch { + return null; + } +} + +export function writeLastShownAt(timestamp: number): void { + try { + localStorage.setItem(LAST_SHOWN_KEY, String(timestamp)); + } catch { + return; + } +} + +/** + * How many visits this browser has made, current one included. Counted once per tab + * via sessionStorage, so a prompt can wait until the user actually comes back. + */ +export function countVisit(): number { + try { + const previous = Number(localStorage.getItem(VISIT_COUNT_KEY)) || 0; + if (sessionStorage.getItem(VISIT_FLAG_KEY)) return previous; + + const current = previous + 1; + sessionStorage.setItem(VISIT_FLAG_KEY, '1'); + localStorage.setItem(VISIT_COUNT_KEY, String(current)); + return current; + } catch { + return 1; + } +} diff --git a/lib/prompts/engine.ts b/lib/prompts/engine.ts new file mode 100644 index 00000000..6b134fd9 --- /dev/null +++ b/lib/prompts/engine.ts @@ -0,0 +1,44 @@ +import { BANNER_PRIORITY, type PromptKey, type PromptStates } from './keys'; + +/** Minimum quiet period between two different call-to-action banners. */ +export const PROMPT_COOLDOWN_MS = 3 * 86_400_000; + +/** Quiet period after signup, so a fresh account is never greeted by a banner. */ +export const ACCOUNT_GRACE_MS = 10 * 60_000; + +export interface PromptContext { + eligible: readonly PromptKey[]; + states: PromptStates; + lastShownAt: number | null; + accountCreatedAt: number | null; + shownThisSession: boolean; + now: number; +} + +/** + * The single call-to-action to display, or null when every anti-spam rule says stay quiet: + * one banner per session, never twice for the same prompt, and a cooldown between two of them. + */ +export function pickPrompt(context: PromptContext): PromptKey | null { + if (context.shownThisSession) return null; + + if ( + context.accountCreatedAt !== null && + context.now - context.accountCreatedAt < ACCOUNT_GRACE_MS + ) + return null; + + if ( + context.lastShownAt !== null && + context.now - context.lastShownAt < PROMPT_COOLDOWN_MS + ) + return null; + + const offered = new Set(context.eligible); + + return ( + BANNER_PRIORITY.find( + (key) => offered.has(key) && context.states[key] === undefined + ) ?? null + ); +} diff --git a/lib/prompts/keys.ts b/lib/prompts/keys.ts new file mode 100644 index 00000000..d764d48e --- /dev/null +++ b/lib/prompts/keys.ts @@ -0,0 +1,16 @@ +export const PROMPT_KEYS = ['install', 'push', 'import', 'streaming'] as const; + +export type PromptKey = (typeof PROMPT_KEYS)[number]; + +export type PromptState = 'done' | 'dismissed'; + +export type PromptStates = Partial>; + +/** Answered per account, so a refusal on desktop also holds on mobile. */ +export const ACCOUNT_PROMPTS = new Set(['import', 'streaming']); + +/** Answered per browser: neither a notification permission nor an install leaves its device. */ +export const DEVICE_PROMPTS = new Set(['install', 'push']); + +/** Order the banner slot resolves ties in: install first, since iOS push requires the PWA. */ +export const BANNER_PRIORITY = ['install', 'push', 'import'] as const; diff --git a/lib/prompts/store.ts b/lib/prompts/store.ts new file mode 100644 index 00000000..57b6b468 --- /dev/null +++ b/lib/prompts/store.ts @@ -0,0 +1,108 @@ +'use client'; + +import { useSyncExternalStore } from 'react'; +import { pickPrompt } from '@/lib/prompts/engine'; +import { + readDeviceStates, + readLastShownAt, + writeDeviceState, + writeLastShownAt, +} from '@/lib/prompts/device'; +import { + DEVICE_PROMPTS, + type PromptKey, + type PromptState, + type PromptStates, +} from '@/lib/prompts/keys'; + +interface PromptSlot { + active: PromptKey | null; + open: boolean; +} + +const CLOSED: PromptSlot = { active: null, open: false }; + +const listeners = new Set<() => void>(); + +let slot: PromptSlot = CLOSED; +let states: PromptStates = {}; +let hydrated = false; +let pushRequested = false; +let shownThisSession = false; + +function notify() { + listeners.forEach((listener) => listener()); +} + +function subscribe(listener: () => void) { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; +} + +/** + * Owns which single call-to-action the current tab may show. State lives outside React + * so any component can signal an opportunity without re-rendering the whole tree. + */ +export const promptStore = { + /** Signals that push notifications just became worth offering (a show added, a friend request). */ + requestPush() { + if (pushRequested) return; + pushRequested = true; + notify(); + }, + + /** Picks a prompt if every anti-spam rule allows it; safe to call on every render pass. */ + evaluate( + eligible: readonly PromptKey[], + accountStates: PromptStates, + accountCreatedAt: number | null, + now: number + ) { + if (!hydrated) { + hydrated = true; + states = { ...accountStates, ...readDeviceStates() }; + } + if (slot.active) return; + + const key = pickPrompt({ + eligible, + states, + lastShownAt: readLastShownAt(), + accountCreatedAt, + shownThisSession, + now, + }); + if (!key) return; + + shownThisSession = true; + writeLastShownAt(now); + slot = { active: key, open: true }; + notify(); + }, + + /** Closes the banner and remembers the answer so the prompt never comes back. */ + resolve(key: PromptKey, state: PromptState) { + states = { ...states, [key]: state }; + if (DEVICE_PROMPTS.has(key)) writeDeviceState(key, state); + slot = { ...slot, open: false }; + notify(); + }, +}; + +export function usePromptSlot(): PromptSlot { + return useSyncExternalStore( + subscribe, + () => slot, + () => CLOSED + ); +} + +export function usePushRequested(): boolean { + return useSyncExternalStore( + subscribe, + () => pushRequested, + () => false + ); +} diff --git a/tests/e2e/pwa-install-prompt.spec.ts b/tests/e2e/pwa-install-prompt.spec.ts new file mode 100644 index 00000000..69d32dc8 --- /dev/null +++ b/tests/e2e/pwa-install-prompt.spec.ts @@ -0,0 +1,103 @@ +import { test, expect, type Page } from '@playwright/test'; + +const IPHONE_SAFARI = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'; + +const IPHONE_CHROME = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.0.0 Mobile/15E148 Safari/604.1'; + +const ANDROID_CHROME = + 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'; + +const phone = { viewport: { width: 390, height: 844 }, isMobile: true }; + +const banner = (page: Page) => + page.getByRole('region', { name: 'Install ReelMark' }); + +/** + * Headless Chromium never fires `beforeinstallprompt`, so replay it the moment the app + * subscribes — dispatching on a timer would race the hydration that attaches the listener. + */ +async function replayInstallabilityEvent(page: Page) { + await page.addInitScript(() => { + const attach = window.addEventListener.bind(window); + window.addEventListener = ( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ) => { + attach(type, listener, options); + if (type === 'beforeinstallprompt') + setTimeout(() => window.dispatchEvent(new Event(type)), 0); + }; + }); +} + +test.describe('Install prompt — iPhone on Safari', () => { + test.use({ ...phone, userAgent: IPHONE_SAFARI }); + + test('spells out the Share then Home Screen gesture, with no install button', async ({ + page, + }) => { + await page.goto('/en'); + + await expect(banner(page)).toBeVisible({ timeout: 10_000 }); + await expect(banner(page)).toContainText('In Safari:'); + await expect(banner(page)).toContainText('Share'); + await expect(banner(page)).toContainText('Home Screen'); + await expect( + banner(page).getByRole('button', { name: 'Install' }) + ).toHaveCount(0); + }); +}); + +test.describe('Install prompt — iPhone on another browser', () => { + test.use({ ...phone, userAgent: IPHONE_CHROME }); + + test('sends the user back to Safari instead of a gesture it cannot perform', async ({ + page, + }) => { + await page.goto('/en'); + + await expect(banner(page)).toBeVisible({ timeout: 10_000 }); + await expect(banner(page)).toContainText('Open ReelMark in Safari'); + await expect(banner(page)).not.toContainText('Home Screen'); + }); +}); + +test.describe('Install prompt — Android', () => { + test.use({ ...phone, userAgent: ANDROID_CHROME }); + + test('offers the native install button once the browser announces it', async ({ + page, + }) => { + await replayInstallabilityEvent(page); + await page.goto('/en'); + + await expect(banner(page)).toBeVisible({ timeout: 10_000 }); + await expect( + banner(page).getByRole('button', { name: 'Install' }) + ).toBeVisible(); + await expect(banner(page)).not.toContainText('Safari'); + }); + + test('stays hidden until the browser announces the app is installable', async ({ + page, + }) => { + await page.goto('/en'); + await page.waitForTimeout(5000); + + await expect(banner(page)).toHaveCount(0); + }); +}); + +test.describe('Install prompt — desktop', () => { + test('never shows up, since there is no home screen to add to', async ({ + page, + }) => { + await page.goto('/en'); + await page.waitForTimeout(5000); + + await expect(banner(page)).toHaveCount(0); + }); +}); diff --git a/tests/unit/prompts-engine.test.ts b/tests/unit/prompts-engine.test.ts new file mode 100644 index 00000000..6bdf455f --- /dev/null +++ b/tests/unit/prompts-engine.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from 'vitest'; +import { + pickPrompt, + PROMPT_COOLDOWN_MS, + ACCOUNT_GRACE_MS, + type PromptContext, +} from '@/lib/prompts/engine'; + +const NOW = 1_800_000_000_000; + +const context = (overrides: Partial = {}): PromptContext => ({ + eligible: ['push'], + states: {}, + lastShownAt: null, + accountCreatedAt: null, + shownThisSession: false, + now: NOW, + ...overrides, +}); + +describe('pickPrompt', () => { + it('returns the only eligible prompt on a clean slate', () => { + expect(pickPrompt(context())).toBe('push'); + }); + + it('returns null when nothing is eligible', () => { + expect(pickPrompt(context({ eligible: [] }))).toBeNull(); + }); + + it('shows install before push, since iOS push needs the installed app', () => { + expect(pickPrompt(context({ eligible: ['push', 'install'] }))).toBe( + 'install' + ); + }); + + it('shows push before import', () => { + expect(pickPrompt(context({ eligible: ['import', 'push'] }))).toBe( + 'push' + ); + }); + + it('never asks twice, whatever the previous answer was', () => { + expect(pickPrompt(context({ states: { push: 'dismissed' } }))).toBeNull(); + expect(pickPrompt(context({ states: { push: 'done' } }))).toBeNull(); + }); + + it('falls through to the next prompt when the top one is already answered', () => { + const picked = pickPrompt( + context({ + eligible: ['install', 'push'], + states: { install: 'dismissed' }, + }) + ); + expect(picked).toBe('push'); + }); + + it('shows at most one banner per session', () => { + expect(pickPrompt(context({ shownThisSession: true }))).toBeNull(); + }); + + it('stays quiet during the cooldown that follows another banner', () => { + const justBefore = NOW - PROMPT_COOLDOWN_MS + 1; + expect(pickPrompt(context({ lastShownAt: justBefore }))).toBeNull(); + }); + + it('speaks again once the cooldown has elapsed', () => { + const elapsed = NOW - PROMPT_COOLDOWN_MS; + expect(pickPrompt(context({ lastShownAt: elapsed }))).toBe('push'); + }); + + it('leaves a freshly created account alone', () => { + const justSignedUp = NOW - ACCOUNT_GRACE_MS + 1; + expect( + pickPrompt(context({ accountCreatedAt: justSignedUp })) + ).toBeNull(); + }); + + it('ignores an eligible key that has no banner slot', () => { + expect(pickPrompt(context({ eligible: ['streaming'] }))).toBeNull(); + }); +}); diff --git a/types/database.ts b/types/database.ts index 3f41fd2c..329ccefd 100644 --- a/types/database.ts +++ b/types/database.ts @@ -400,6 +400,27 @@ export type Database = { }; Relationships: []; }; + user_prompts: { + Row: { + prompt_key: string; + state: string; + updated_at: string; + user_id: string; + }; + Insert: { + prompt_key: string; + state: string; + updated_at?: string; + user_id: string; + }; + Update: { + prompt_key?: string; + state?: string; + updated_at?: string; + user_id?: string; + }; + Relationships: []; + }; user_streaming_providers: { Row: { provider_ids: number[]; From 77425d19b33b7e36548e4c2072cf3db366b08a85 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 02:32:26 +0200 Subject: [PATCH 18/98] =?UTF-8?q?perf(cta):=20d=C3=A9duplique=20les=20lect?= =?UTF-8?q?ures=20serveur=20du=20slot=20de=20CTA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le slot est monté dans le layout, donc rendu sur chaque page : sur /dashboard il déclenchait cinq requêtes dont trois doublons (états des prompts et compte watchlist demandés deux fois, plateformes hors du cache déjà utilisé par buildLibrarySections). - lib/data/prompts.ts : getCachedPromptStates + getCachedWatchlistCount, même pattern React.cache que lib/data/watchlist.ts - StreamingPromptSection passe sur getCachedStreamingProviders - le compte reste un count exact plutôt que la liste complète, puisque le slot s'affiche aussi sur les pages qui n'ont pas besoin de la watchlist Cinq requêtes ramenées à deux par rendu du dashboard. --- .../dashboard/StreamingPromptSection.tsx | 22 +++++++++---------- components/prompts/PromptSlot.tsx | 18 +++++++-------- lib/data/prompts.ts | 21 ++++++++++++++++++ 3 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 lib/data/prompts.ts diff --git a/components/dashboard/StreamingPromptSection.tsx b/components/dashboard/StreamingPromptSection.tsx index a58da55c..fb3e6e1c 100644 --- a/components/dashboard/StreamingPromptSection.tsx +++ b/components/dashboard/StreamingPromptSection.tsx @@ -1,5 +1,8 @@ -import { getMyStreamingProviders } from '@/app/actions/recommendations'; -import { getPromptStates } from '@/app/actions/prompts'; +import { getCachedStreamingProviders } from '@/lib/data/watchlist'; +import { + getCachedPromptStates, + getCachedWatchlistCount, +} from '@/lib/data/prompts'; import { getUserContext } from '@/lib/supabase/auth-helpers'; import { StreamingPromptCard } from '@/components/dashboard/StreamingPromptCard'; @@ -7,21 +10,18 @@ import { StreamingPromptCard } from '@/components/dashboard/StreamingPromptCard' const MIN_WATCHLIST_ENTRIES = 5; export async function StreamingPromptSection() { - const { supabase, user } = await getUserContext(); + const { user } = await getUserContext(); if (!user) return null; - const [providerIds, states, watchlist] = await Promise.all([ - getMyStreamingProviders(), - getPromptStates(), - supabase - .from('watchlist') - .select('id', { count: 'exact', head: true }) - .eq('user_id', user.id), + const [providerIds, states, watchlistCount] = await Promise.all([ + getCachedStreamingProviders(), + getCachedPromptStates(), + getCachedWatchlistCount(), ]); if (providerIds.length > 0) return null; if (states.streaming !== undefined) return null; - if ((watchlist.count ?? 0) < MIN_WATCHLIST_ENTRIES) return null; + if (watchlistCount < MIN_WATCHLIST_ENTRIES) return null; return ; } diff --git a/components/prompts/PromptSlot.tsx b/components/prompts/PromptSlot.tsx index 69f6c266..b83b69b8 100644 --- a/components/prompts/PromptSlot.tsx +++ b/components/prompts/PromptSlot.tsx @@ -1,5 +1,8 @@ import { getUserContext } from '@/lib/supabase/auth-helpers'; -import { getPromptStates } from '@/app/actions/prompts'; +import { + getCachedPromptStates, + getCachedWatchlistCount, +} from '@/lib/data/prompts'; import { PromptHost } from '@/components/prompts/PromptHost'; /** Below this many watchlist entries, importing an existing library is worth offering. */ @@ -7,7 +10,7 @@ const IMPORT_THRESHOLD = 3; /** Feeds the call-to-action slot with what only the server knows; anonymous visitors still get the install banner. */ export async function PromptSlot() { - const { supabase, user } = await getUserContext(); + const { user } = await getUserContext(); if (!user) return ( @@ -18,12 +21,9 @@ export async function PromptSlot() { /> ); - const [states, watchlist] = await Promise.all([ - getPromptStates(), - supabase - .from('watchlist') - .select('id', { count: 'exact', head: true }) - .eq('user_id', user.id), + const [states, watchlistCount] = await Promise.all([ + getCachedPromptStates(), + getCachedWatchlistCount(), ]); const createdAt = Date.parse(user.created_at); @@ -32,7 +32,7 @@ export async function PromptSlot() { ); } diff --git a/lib/data/prompts.ts b/lib/data/prompts.ts new file mode 100644 index 00000000..7d05345c --- /dev/null +++ b/lib/data/prompts.ts @@ -0,0 +1,21 @@ +import { cache } from 'react'; +import { getPromptStates } from '@/app/actions/prompts'; +import { getUserContext } from '@/lib/supabase/auth-helpers'; + +export const getCachedPromptStates = cache(getPromptStates); + +/** + * Watchlist size only, request-deduped: the call-to-action slot renders on every page, + * so it must never pull the whole list just to know whether the library is still empty. + */ +export const getCachedWatchlistCount = cache(async (): Promise => { + const { supabase, user } = await getUserContext(); + if (!user) return 0; + + const { count } = await supabase + .from('watchlist') + .select('id', { count: 'exact', head: true }) + .eq('user_id', user.id); + + return count ?? 0; +}); From a999fb58c9bbca994ebfae1ecded750ef39cabc5 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 02:32:35 +0200 Subject: [PATCH 19/98] =?UTF-8?q?fix(cta):=20comptage=20des=20visites,=20b?= =?UTF-8?q?andeau=20ferm=C3=A9=20et=20validation=20des=20cl=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - countVisit() était évalué derrière un court-circuit `canImport &&`, donc le compteur de visites n'avançait que pour les comptes à watchlist vide. Il est maintenant lu en tête d'effet, indépendamment de qui s'en sert. - le bandeau fermé restait annoncé par les lecteurs d'écran et focusable : translate-y ne le sort que visuellement, d'où aria-hidden + inert. - isPromptKey / isPromptState remplacent les `as` posés avant vérification sur les lignes user_prompts. - usePWAInstall annule son timer avant d'en reposer un, si le navigateur réémet beforeinstallprompt. --- app/actions/prompts.ts | 16 +++++++++------- components/prompts/PromptBanner.tsx | 2 ++ components/prompts/PromptHost.tsx | 5 +++-- hooks/usePWAInstall.ts | 11 +++++++---- lib/prompts/keys.ts | 8 ++++++++ tests/unit/prompts-engine.test.ts | 4 +++- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/app/actions/prompts.ts b/app/actions/prompts.ts index c520e79a..effc464e 100644 --- a/app/actions/prompts.ts +++ b/app/actions/prompts.ts @@ -1,16 +1,18 @@ 'use server'; -import { getAuthenticatedUser, getOptionalUser } from '@/lib/supabase/auth-helpers'; +import { + getAuthenticatedUser, + getOptionalUser, +} from '@/lib/supabase/auth-helpers'; import { ACCOUNT_PROMPTS, - PROMPT_KEYS, + isPromptKey, + isPromptState, type PromptKey, type PromptState, type PromptStates, } from '@/lib/prompts/keys'; -const VALID_STATES = new Set(['done', 'dismissed']); - /** Answers already given to account-scoped call-to-actions, so none is ever asked twice. */ export async function getPromptStates(): Promise { const { supabase, userId } = await getOptionalUser(); @@ -23,8 +25,8 @@ export async function getPromptStates(): Promise { const states: PromptStates = {}; for (const row of data ?? []) { - const key = row.prompt_key as PromptKey; - if (PROMPT_KEYS.includes(key)) states[key] = row.state as PromptState; + if (isPromptKey(row.prompt_key) && isPromptState(row.state)) + states[row.prompt_key] = row.state; } return states; } @@ -35,7 +37,7 @@ export async function resolvePrompt( state: PromptState ): Promise { if (!ACCOUNT_PROMPTS.has(key)) throw new Error('Invalid prompt_key'); - if (!VALID_STATES.has(state)) throw new Error('Invalid state'); + if (!isPromptState(state)) throw new Error('Invalid state'); const { supabase, userId } = await getAuthenticatedUser(); diff --git a/components/prompts/PromptBanner.tsx b/components/prompts/PromptBanner.tsx index c32aa43d..a4b2d1da 100644 --- a/components/prompts/PromptBanner.tsx +++ b/components/prompts/PromptBanner.tsx @@ -29,6 +29,8 @@ export function PromptBanner({
{ + const visits = countVisit(); + const eligible: PromptKey[] = []; if (pwa.visible) eligible.push('install'); if (pushRequested && push.status === 'off') eligible.push('push'); - if (canImport && countVisit() >= IMPORT_MIN_VISITS) - eligible.push('import'); + if (canImport && visits >= IMPORT_MIN_VISITS) eligible.push('import'); promptStore.evaluate( eligible, diff --git a/hooks/usePWAInstall.ts b/hooks/usePWAInstall.ts index b3928ab5..3ca8a9d1 100644 --- a/hooks/usePWAInstall.ts +++ b/hooks/usePWAInstall.ts @@ -44,10 +44,12 @@ function isMobile(): boolean { /** Whether this device can still install the app, and by which route. */ export function usePWAInstall() { - const [state, setState] = useState<{ visible: boolean; mode: InstallMode }>({ - visible: false, - mode: 'prompt', - }); + const [state, setState] = useState<{ visible: boolean; mode: InstallMode }>( + { + visible: false, + mode: 'prompt', + } + ); const promptRef = useRef(null); useEffect(() => { @@ -69,6 +71,7 @@ export function usePWAInstall() { const handler = (e: Event) => { e.preventDefault(); promptRef.current = e as BeforeInstallPromptEvent; + clearTimeout(timer); timer = setTimeout( () => setState({ visible: true, mode: 'prompt' }), REVEAL_DELAY_MS diff --git a/lib/prompts/keys.ts b/lib/prompts/keys.ts index d764d48e..2b366d4f 100644 --- a/lib/prompts/keys.ts +++ b/lib/prompts/keys.ts @@ -14,3 +14,11 @@ export const DEVICE_PROMPTS = new Set(['install', 'push']); /** Order the banner slot resolves ties in: install first, since iOS push requires the PWA. */ export const BANNER_PRIORITY = ['install', 'push', 'import'] as const; + +export function isPromptKey(value: string): value is PromptKey { + return (PROMPT_KEYS as readonly string[]).includes(value); +} + +export function isPromptState(value: string): value is PromptState { + return value === 'done' || value === 'dismissed'; +} diff --git a/tests/unit/prompts-engine.test.ts b/tests/unit/prompts-engine.test.ts index 6bdf455f..8c80451c 100644 --- a/tests/unit/prompts-engine.test.ts +++ b/tests/unit/prompts-engine.test.ts @@ -40,7 +40,9 @@ describe('pickPrompt', () => { }); it('never asks twice, whatever the previous answer was', () => { - expect(pickPrompt(context({ states: { push: 'dismissed' } }))).toBeNull(); + expect( + pickPrompt(context({ states: { push: 'dismissed' } })) + ).toBeNull(); expect(pickPrompt(context({ states: { push: 'done' } }))).toBeNull(); }); From 7e56dd759edcd7ed6d13f402fda4257953804027 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 02:32:45 +0200 Subject: [PATCH 20/98] chore(obs): un 404 TMDB ne remonte plus dans Sentry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les endpoints recommendations et similar répondent 404 pour les titres de niche, et un id disparaît quand TMDB fusionne ou supprime une fiche. Chaque dashboard poussait donc plusieurs warnings dans Bugsink pour une absence que tous les appelants gèrent déjà par un fallback. TMDBNotFoundError, sans dépendance pour rester importable côté client, permet à reportSwallowed de la traiter en console.debug. Toute autre erreur (réseau, 5xx, quota) garde son warning et sa remontée. --- lib/report.ts | 17 +++++++++++++---- lib/tmdb/client.ts | 2 ++ lib/tmdb/errors.ts | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 lib/tmdb/errors.ts diff --git a/lib/report.ts b/lib/report.ts index 97adf962..a4bb8944 100644 --- a/lib/report.ts +++ b/lib/report.ts @@ -1,6 +1,7 @@ import * as Sentry from '@sentry/nextjs'; import { unstable_rethrow } from 'next/navigation'; import { monotonicNowMs } from '@/lib/monotonic-now'; +import { isTMDBNotFound } from '@/lib/tmdb/errors'; const FORWARD_DEDUP_MS = 5 * 60_000; const lastForwardedAt = new Map(); @@ -20,13 +21,21 @@ function shouldForward(key: string): boolean { /** * Logs a swallowed fallback error with a stable tag and forwards it to Sentry as a - * warning, deduped per label+message for 5 min so routine failures (dead TMDB ids, - * quota cooldowns) surface without flooding. Next's own control-flow signals - * (`redirect`, `notFound`, and the halt that stops a Cache Components prerender) are - * rethrown instead — swallowing those would turn a redirect into a blank section. + * warning, deduped per label+message for 5 min so routine failures (quota cooldowns) + * surface without flooding. A TMDB 404 means the resource simply does not exist, which + * every caller already handles with a fallback, so it stays at debug level rather than + * filling the issue tracker. Next's own control-flow signals (`redirect`, `notFound`, + * and the halt that stops a Cache Components prerender) are rethrown instead — + * swallowing those would turn a redirect into a blank section. */ export function reportSwallowed(label: string, error: unknown): void { unstable_rethrow(error); + + if (isTMDBNotFound(error)) { + console.debug(`[${label}]`, error.message); + return; + } + console.warn(`[${label}]`, error); const message = error instanceof Error ? error.message : String(error); diff --git a/lib/tmdb/client.ts b/lib/tmdb/client.ts index 658a223b..b469e007 100644 --- a/lib/tmdb/client.ts +++ b/lib/tmdb/client.ts @@ -2,6 +2,7 @@ import { cache } from 'react'; import { cacheLife } from 'next/cache'; import { getServerLocale, getServerLanguage } from '@/lib/i18n/server'; import { createClient } from '@/lib/supabase/server'; +import { TMDBNotFoundError } from '@/lib/tmdb/errors'; import type { Language } from '@/lib/i18n/translations'; export interface FetchTMDBOptions { @@ -155,6 +156,7 @@ export async function fetchTMDB( const result = await fetchTMDBUrl(url, revalidate, retries); if (!result.ok) { + if (result.status === 404) throw new TMDBNotFoundError(safeEndpoint); throw new Error( `TMDB API Error: ${result.status} ${result.statusText}` ); diff --git a/lib/tmdb/errors.ts b/lib/tmdb/errors.ts new file mode 100644 index 00000000..47b9d2b6 --- /dev/null +++ b/lib/tmdb/errors.ts @@ -0,0 +1,15 @@ +/** + * TMDB has no such resource. Routine rather than exceptional: recommendations and + * similar-title endpoints answer 404 for niche entries, and ids disappear when a title + * is merged or deleted upstream. Kept dependency-free so the client bundle can import it. + */ +export class TMDBNotFoundError extends Error { + constructor(endpoint: string) { + super(`TMDB API Error: 404 Not Found (${endpoint})`); + this.name = 'TMDBNotFoundError'; + } +} + +export function isTMDBNotFound(error: unknown): error is TMDBNotFoundError { + return error instanceof TMDBNotFoundError; +} From 5ce934f3431fef5895986d9fb9bf8a1b67dcaaff Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 02:33:03 +0200 Subject: [PATCH 21/98] =?UTF-8?q?chore(i18n):=20tutoiement=20sur=20toute?= =?UTF-8?q?=20l'interface=20fran=C3=A7aise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le dashboard affichait « Tes statistiques » juste au-dessus de « Vos prochains visionnages » : settings tutoyait déjà, le reste vouvoyait. 93 chaînes alignées sur le tutoiement, y compris les impératifs pluriels (« Découvrez » → « Découvre ») que le premier passage sur vous/votre/vos avait laissés. app/manifest.ts duplique quatre descriptions de metadata.ts et suit le même changement. Concerne aussi /terms et /privacy : « Tes droits », « Ton compte ». --- app/manifest.ts | 8 ++-- lib/i18n/messages/auth.ts | 38 +++++++++---------- lib/i18n/messages/common.ts | 2 +- lib/i18n/messages/danger.ts | 10 ++--- lib/i18n/messages/features.ts | 13 +++---- lib/i18n/messages/hero.ts | 4 +- lib/i18n/messages/home.ts | 2 +- lib/i18n/messages/library.ts | 8 ++-- lib/i18n/messages/metadata.ts | 50 ++++++++++++------------- lib/i18n/messages/movie.ts | 2 +- lib/i18n/messages/offline.ts | 4 +- lib/i18n/messages/pages.ts | 70 +++++++++++++++++------------------ lib/i18n/messages/prompts.ts | 3 +- lib/i18n/messages/settings.ts | 33 ++++++++--------- 14 files changed, 122 insertions(+), 125 deletions(-) diff --git a/app/manifest.ts b/app/manifest.ts index 3556b700..5cdd50c0 100644 --- a/app/manifest.ts +++ b/app/manifest.ts @@ -32,7 +32,7 @@ export default function manifest(): MetadataRoute.Manifest { description: 'Track and organize movies & TV shows you watched.', description_localized: { fr: { - value: 'Suivez, organisez et découvrez tous les films et séries que vous avez regardés ou voulez regarder.', + value: 'Suis, organise et découvre tous les films et séries que tu as regardés ou veux regarder.', }, }, start_url: '/', @@ -54,7 +54,7 @@ export default function manifest(): MetadataRoute.Manifest { 'Your personal library of movies and TV shows to watch or already seen.', description_localized: { fr: { - value: 'Votre bibliothèque personnelle de films et séries à regarder ou déjà vus.', + value: 'Ta bibliothèque personnelle de films et séries à regarder ou déjà vus.', }, }, url: '/library', @@ -69,7 +69,7 @@ export default function manifest(): MetadataRoute.Manifest { 'Discover movies and TV shows to add to your collection.', description_localized: { fr: { - value: 'Découvrez des films et séries à ajouter à votre collection.', + value: 'Découvre des films et séries à ajouter à ta collection.', }, }, url: '/explorer', @@ -84,7 +84,7 @@ export default function manifest(): MetadataRoute.Manifest { 'Access your personal dashboard with recommendations and watch history.', description_localized: { fr: { - value: 'Accédez à votre tableau de bord personnel avec vos recommandations et votre historique de visionnage.', + value: 'Accède à ton tableau de bord personnel avec tes recommandations et ton historique de visionnage.', }, }, url: '/dashboard', diff --git a/lib/i18n/messages/auth.ts b/lib/i18n/messages/auth.ts index 12d36ede..ea939ac1 100644 --- a/lib/i18n/messages/auth.ts +++ b/lib/i18n/messages/auth.ts @@ -1,11 +1,11 @@ const fr = { signup: { - title: 'Rentrez en scène et créez votre compte', + title: 'Rentre en scène et crée ton compte', email: 'E-mail', password: 'Mot de passe', confirmPassword: 'Confirmer le mot de passe', button: 'Créer mon compte', - orEmail: 'Ou continuer avec votre email', + orEmail: 'Ou continuer avec ton email', alreadyHaveAccount: 'Déjà un compte ?', login: 'Se connecter', google: 'Continuer avec Google', @@ -16,16 +16,16 @@ const fr = { }, region: 'Pays / Région', regionDescription: - 'Ceci permet de synchroniser les sorties cinéma avec votre pays local.', + 'Ceci permet de synchroniser les sorties cinéma avec ton pays local.', }, login: { - title: 'Connectez-vous à votre compte', - description: 'Entrez vos identifiants pour accéder à votre compte', + title: 'Connecte-toi à ton compte', + description: 'Entre tes identifiants pour accéder à ton compte', email: 'E-mail', password: 'Mot de passe', button: 'Se connecter', - orEmail: 'Ou continuer avec votre email', - dontHaveAccount: "Vous n'avez pas de compte ?", + orEmail: 'Ou continuer avec ton email', + dontHaveAccount: "Tu n'as pas de compte ?", signup: "S'inscrire", google: 'Continuer avec Google', forgotPassword: 'Mot de passe oublié ?', @@ -42,16 +42,16 @@ const fr = { resetPassword: { metaTitle: 'Réinitialiser le mot de passe', metaDescription: - 'Recevez un lien pour réinitialiser votre mot de passe ReelMark.', - title: 'Réinitialiser votre mot de passe', + 'Reçois un lien pour réinitialiser ton mot de passe ReelMark.', + title: 'Réinitialiser ton mot de passe', description: - 'Entrez votre adresse e-mail. Nous vous enverrons un lien de réinitialisation.', + 'Entre ton adresse e-mail. Nous t’enverrons un lien de réinitialisation.', email: 'E-mail', button: 'Envoyer le lien', backToLogin: 'Retour à la connexion', successTitle: 'E-mail envoyé', successDescription: - 'Si un compte existe pour cette adresse, vous recevrez un e-mail avec un lien.', + 'Si un compte existe pour cette adresse, tu recevras un e-mail avec un lien.', placeholders: { email: 'chuck@example.com', }, @@ -59,16 +59,16 @@ const fr = { updatePassword: { metaTitle: 'Choisir un nouveau mot de passe', metaDescription: - 'Définissez un nouveau mot de passe pour votre compte ReelMark.', + 'Définis un nouveau mot de passe pour ton compte ReelMark.', title: 'Choisir un nouveau mot de passe', description: - 'Entrez votre nouveau mot de passe pour finaliser la réinitialisation.', + 'Entre ton nouveau mot de passe pour finaliser la réinitialisation.', password: 'Nouveau mot de passe', confirmPassword: 'Confirmer le mot de passe', button: 'Mettre à jour', - note: 'Vous serez redirigé vers votre tableau de bord après la mise à jour.', + note: 'Tu seras redirigé vers ton tableau de bord après la mise à jour.', footerHint: - 'Si le lien a expiré, demandez un nouveau lien depuis la page de connexion.', + 'Si le lien a expiré, demande un nouveau lien depuis la page de connexion.', placeholders: { password: '••••••••', confirmPassword: '••••••••', @@ -81,13 +81,11 @@ const fr = { retry: 'Réessayer', backHome: "Retour à l'accueil", invalidCredentials: 'Email ou mot de passe incorrect', - emailNotConfirmed: - 'Veuillez confirmer votre adresse email avant de vous connecter', + emailNotConfirmed: 'Confirme ton adresse email avant de te connecter', emailAlreadyUsed: 'Un compte existe déjà avec cette adresse email', - rateLimitExceeded: - 'Trop de tentatives, réessayez dans quelques minutes', + rateLimitExceeded: 'Trop de tentatives, réessaie dans quelques minutes', }, - terms: 'En continuant, vous acceptez nos', + terms: 'En continuant, tu acceptes nos', termsLink: "Conditions d'utilisation", privacyLink: 'Politique de confidentialité', notAuthenticated: 'Non authentifié', diff --git a/lib/i18n/messages/common.ts b/lib/i18n/messages/common.ts index 7bbc9d65..282d1653 100644 --- a/lib/i18n/messages/common.ts +++ b/lib/i18n/messages/common.ts @@ -36,7 +36,7 @@ const fr = { footerNav: 'Liens de pied de page', notFoundTitle: 'Page introuvable', notFoundDescription: - "La page que vous cherchez n'existe pas ou a été déplacée.", + "La page que tu cherches n'existe pas ou a été déplacée.", }; const en = { diff --git a/lib/i18n/messages/danger.ts b/lib/i18n/messages/danger.ts index 1e3886c0..1ba7f6f6 100644 --- a/lib/i18n/messages/danger.ts +++ b/lib/i18n/messages/danger.ts @@ -1,13 +1,13 @@ const fr = { warning: '⚠️ Avertissement important', - allDataWillBeDeleted: 'Toutes vos données seront supprimées définitivement', - accountCannotBeRecovered: 'Votre compte ne pourra pas être récupéré', + allDataWillBeDeleted: 'Toutes tes données seront supprimées définitivement', + accountCannotBeRecovered: 'Ton compte ne pourra pas être récupéré', actionCannotBeUndone: 'Cette action ne peut pas être annulée', - willBeLoggedOut: 'Vous serez déconnecté immédiatement', - typeToConfirm: 'Tapez "SUPPRIMER" pour confirmer', + willBeLoggedOut: 'Tu seras déconnecté immédiatement', + typeToConfirm: 'Tape "SUPPRIMER" pour confirmer', confirmPlaceholder: 'SUPPRIMER', additionalWarning: - "Cette action est irréversible. Assurez-vous d'avoir sauvegardé vos données.", + "Cette action est irréversible. Assure-toi d'avoir sauvegardé tes données.", deleting: 'Suppression...', }; diff --git a/lib/i18n/messages/features.ts b/lib/i18n/messages/features.ts index b124cc50..91501e3f 100644 --- a/lib/i18n/messages/features.ts +++ b/lib/i18n/messages/features.ts @@ -1,21 +1,20 @@ const fr = { title: 'Fonctionnalités', feature1Title: 'Suivi personnel', - feature1Desc: - 'Gardez une trace complète de votre parcours cinématographique', + feature1Desc: 'Garde une trace complète de ton parcours cinématographique', feature2Title: 'Découverte', - feature2Desc: 'Explorez de nouveaux films basés sur vos préférences', + feature2Desc: 'Explore de nouveaux films basés sur tes préférences', feature3Title: 'Organisation', - feature3Desc: 'Créez des listes et des catégories personnalisées', + feature3Desc: 'Crée des listes et des catégories personnalisées', feature4Title: 'Notes et avis', feature4Desc: - 'Ajoutez vos propres notes et évaluations à chaque contenu pour vous souvenir de ce que vous avez pensé.', + 'Ajoute tes propres notes et évaluations à chaque contenu pour te souvenir de ce que tu en as pensé.', feature5Title: 'Installable partout', feature5Desc: - 'Installez ReelMark sur iPhone, Android, Mac, Windows ou Linux — sans passer par un store, comme une app native.', + 'Installe ReelMark sur iPhone, Android, Mac, Windows ou Linux — sans passer par un store, comme une app native.', feature6Title: 'Amis & communauté', feature6Desc: - "Suivez les critiques et listes de vos amis, envoyez des demandes d'amis et découvrez ce que regardent les autres.", + "Suis les critiques et listes de tes amis, envoie des demandes d'amis et découvre ce que regardent les autres.", }; const en = { diff --git a/lib/i18n/messages/hero.ts b/lib/i18n/messages/hero.ts index 8bb65f4a..e8bdc4d0 100644 --- a/lib/i18n/messages/hero.ts +++ b/lib/i18n/messages/hero.ts @@ -1,7 +1,7 @@ const fr = { title: 'ReelMark', - subtitle: 'Votre compagnon personnel pour suivre et organiser', - description: 'tous les films, séries et contenus que vous avez déjà vus', + subtitle: 'Ton compagnon personnel pour suivre et organiser', + description: 'tous les films, séries et contenus que tu as déjà vus', cta: 'Créer un compte', login: 'Se connecter', }; diff --git a/lib/i18n/messages/home.ts b/lib/i18n/messages/home.ts index d25eefd2..ae8427ac 100644 --- a/lib/i18n/messages/home.ts +++ b/lib/i18n/messages/home.ts @@ -5,7 +5,7 @@ const fr = { cta: { title: 'Prêt à commencer ?', subtitle: - 'Rejoignez la communauté de cinéphiles et commencez à tracker vos films favoris dès maintenant.', + 'Rejoins la communauté de cinéphiles et commence à tracker tes films favoris dès maintenant.', button: 'Créer un compte gratuitement', alreadyHave: "J'ai déjà un compte", }, diff --git a/lib/i18n/messages/library.ts b/lib/i18n/messages/library.ts index 5a612649..016ba8aa 100644 --- a/lib/i18n/messages/library.ts +++ b/lib/i18n/messages/library.ts @@ -3,15 +3,15 @@ const fr = { toWatch: 'À regarder', watched: 'Regardés', abandoned: 'Abandonnées', - noMovies: 'Ajoutez du contenu à voir depuis Explorer', - noMoviesDesc: 'Parcourez et ajoutez des films à votre liste', + noMovies: 'Ajoute du contenu à voir depuis Explorer', + noMoviesDesc: 'Parcours et ajoute des films à ta liste', noWatched: 'Aucun contenu regardé pour le moment', - noWatchedDesc: 'Les titres que vous regardez apparaîtront ici', + noWatchedDesc: 'Les titres que tu regardes apparaîtront ici', noAbandoned: 'Aucune série abandonnée', noAbandonedDesc: 'Les séries que tu abandonnes atterrissent ici, prêtes à être reprises', watchlist: 'Watchlist', - inLibrary: 'dans votre collection', + inLibrary: 'dans ta collection', filmsCount: 'film', filmsCountPlural: 'films', tvCount: 'série', diff --git a/lib/i18n/messages/metadata.ts b/lib/i18n/messages/metadata.ts index 0193906c..78fd2ca5 100644 --- a/lib/i18n/messages/metadata.ts +++ b/lib/i18n/messages/metadata.ts @@ -1,55 +1,55 @@ const fr = { - defaultMovieDescription: 'Suivez et découvrez des films sur ReelMark', - defaultTvDescription: 'Suivez et découvrez des séries sur ReelMark', + defaultMovieDescription: 'Suis et découvre des films sur ReelMark', + defaultTvDescription: 'Suis et découvre des séries sur ReelMark', defaultCrewDescription: - 'Explorez les artistes et leur filmographie sur ReelMark', - watchMovieOn: 'Regardez ${title} sur ReelMark', - watchShowOn: 'Regardez ${title} sur ReelMark', - exploreCrewOn: 'Explorez la filmographie de ${name} sur ReelMark', + 'Explore les artistes et leur filmographie sur ReelMark', + watchMovieOn: 'Regarde ${title} sur ReelMark', + watchShowOn: 'Regarde ${title} sur ReelMark', + exploreCrewOn: 'Explore la filmographie de ${name} sur ReelMark', dashboardTitle: 'Tableau de bord', dashboardDescription: - 'Accédez à votre tableau de bord personnel avec vos recommandations et votre historique de visionnage.', + 'Accède à ton tableau de bord personnel avec tes recommandations et ton historique de visionnage.', explorerTitle: 'Explorateur', explorerDescription: - 'Découvrez des films et séries à ajouter à votre collection.', + 'Découvre des films et séries à ajouter à ta collection.', libraryDescription: - 'Votre bibliothèque personnelle de films et séries à regarder ou déjà vus.', + 'Ta bibliothèque personnelle de films et séries à regarder ou déjà vus.', loginTitle: 'Connexion', loginDescription: - 'Connectez-vous à votre compte ReelMark pour suivre vos films et séries.', + 'Connecte-toi à ton compte ReelMark pour suivre tes films et séries.', signupTitle: 'Inscription', signupDescription: - 'Créez votre compte ReelMark et commencez à suivre vos films et séries.', - landingTitle: 'ReelMark — Votre suivi de films et séries', + 'Crée ton compte ReelMark et commence à suivre tes films et séries.', + landingTitle: 'ReelMark — Ton suivi de films et séries', landingDescription: - 'Suivez, organisez et découvrez tous les films et séries que vous avez regardés ou voulez regarder.', + 'Suis, organise et découvre tous les films et séries que tu as regardés ou veux regarder.', profileDescription: - 'Découvrez le profil de @${username} sur ReelMark — watchlist, avis et plus.', + 'Découvre le profil de @${username} sur ReelMark — watchlist, avis et plus.', playlistDescription: 'Playlist de @${owner} sur ReelMark', authErrorTitle: "Erreur d'authentification", authErrorDescription: - 'Une erreur est survenue lors de la connexion. Retournez à la page de connexion pour réessayer.', + 'Une erreur est survenue lors de la connexion. Retourne à la page de connexion pour réessayer.', categories: { popularMovies: - 'Découvrez les films et séries TV les plus populaires du moment sur ReelMark.', + 'Découvre les films et séries TV les plus populaires du moment sur ReelMark.', topRatedMovies: - 'Explorez les films les mieux notés de tous les temps sur ReelMark.', + 'Explore les films les mieux notés de tous les temps sur ReelMark.', upcomingMovies: - 'Découvrez les films à venir et attendus prochainement sur ReelMark.', + 'Découvre les films à venir et attendus prochainement sur ReelMark.', nowPlayingMovies: - 'Regardez les films actuellement en salles sur ReelMark.', + 'Regarde les films actuellement en salles sur ReelMark.', trendingMovies: - 'Découvrez les films qui font tendance cette semaine sur ReelMark.', + 'Découvre les films qui font tendance cette semaine sur ReelMark.', tvPopular: - 'Découvrez les séries TV les plus populaires du moment sur ReelMark.', + 'Découvre les séries TV les plus populaires du moment sur ReelMark.', tvTopRated: - 'Explorez les séries TV les mieux notées de tous les temps sur ReelMark.', + 'Explore les séries TV les mieux notées de tous les temps sur ReelMark.', tvTrending: - 'Découvrez les séries TV qui font tendance cette semaine sur ReelMark.', + 'Découvre les séries TV qui font tendance cette semaine sur ReelMark.', tvAiringToday: - "Regardez les séries TV diffusées aujourd'hui sur ReelMark.", + "Regarde les séries TV diffusées aujourd'hui sur ReelMark.", tvOnTheAir: - 'Découvrez les séries TV actuellement en diffusion sur ReelMark.', + 'Découvre les séries TV actuellement en diffusion sur ReelMark.', }, }; diff --git a/lib/i18n/messages/movie.ts b/lib/i18n/messages/movie.ts index de790de9..0e7c2c59 100644 --- a/lib/i18n/messages/movie.ts +++ b/lib/i18n/messages/movie.ts @@ -100,7 +100,7 @@ const fr = { }, }, runtime: '${hours}h ${mins}min', - scrollEnd: 'Vous avez tout vu !', + scrollEnd: 'Tu as tout vu !', scrollEndCount: 'titres affichés', episodes: 'épisodes', episodesCapitalized: 'Épisodes', diff --git a/lib/i18n/messages/offline.ts b/lib/i18n/messages/offline.ts index 0b1148dd..2f4a562f 100644 --- a/lib/i18n/messages/offline.ts +++ b/lib/i18n/messages/offline.ts @@ -1,6 +1,6 @@ const fr = { - title: 'Vous êtes hors ligne', - description: 'Vérifiez votre connexion et réessayez.', + title: 'Tu es hors ligne', + description: 'Vérifie ta connexion et réessaie.', retry: 'Réessayer', }; diff --git a/lib/i18n/messages/pages.ts b/lib/i18n/messages/pages.ts index 82e31f40..e5368287 100644 --- a/lib/i18n/messages/pages.ts +++ b/lib/i18n/messages/pages.ts @@ -3,63 +3,63 @@ const fr = { privacy: { title: 'Politique de confidentialité', updated: 'Dernière mise à jour : 12 juillet 2026', - intro: 'ReelMark respecte votre vie privée. Cette déclaration explique quelles données personnelles nous traitons, pourquoi, et quels sont vos droits, conformément au Règlement général sur la protection des données (RGPD, UE 2016/679) et à la loi belge du 30 juillet 2018.', + intro: 'ReelMark respecte ta vie privée. Cette déclaration explique quelles données personnelles nous traitons, pourquoi, et quels sont tes droits, conformément au Règlement général sur la protection des données (RGPD, UE 2016/679) et à la loi belge du 30 juillet 2018.', sections: [ { title: '1. Responsable du traitement', body: [ - "ReelMark est édité par Silexio (Belgique). Pour toute question relative à vos données personnelles, contactez-nous à l'adresse : contact@silexio.be.", + "ReelMark est édité par Silexio (Belgique). Pour toute question relative à tes données personnelles, écris-nous à l'adresse : contact@silexio.be.", ], }, { title: '2. Données que nous collectons', body: [ - "Données de compte : adresse e-mail, mot de passe (haché, jamais stocké en clair), nom d'utilisateur, nom affiché, avatar, langue et région préférées. Si vous vous connectez avec Google, nous recevons votre e-mail et les informations de base de votre profil Google.", - "Contenu que vous créez : votre watchlist, les épisodes marqués comme vus, vos notes et critiques, vos playlists et vos relations d'amis sur la plateforme.", - "Données techniques : cookies strictement nécessaires (session de connexion, préférence de langue, répartition de charge), préférences stockées localement dans votre navigateur (thème, affichage) et journaux d'erreurs techniques.", + "Données de compte : adresse e-mail, mot de passe (haché, jamais stocké en clair), nom d'utilisateur, nom affiché, avatar, langue et région préférées. Si tu te connectes avec Google, nous recevons ton e-mail et les informations de base de ton profil Google.", + "Contenu que tu crées : ta watchlist, les épisodes marqués comme vus, tes notes et critiques, tes playlists et tes relations d'amis sur la plateforme.", + "Données techniques : cookies strictement nécessaires (session de connexion, préférence de langue, répartition de charge), préférences stockées localement dans ton navigateur (thème, affichage) et journaux d'erreurs techniques.", 'Nous ne collectons aucune donnée publicitaire et ne réalisons aucun profilage commercial.', ], }, { title: '3. Finalités et bases légales', body: [ - 'Fournir le service (création de compte, sauvegarde de votre bibliothèque, fonctionnalités sociales) : exécution du contrat qui nous lie lorsque vous créez un compte.', + 'Fournir le service (création de compte, sauvegarde de ta bibliothèque, fonctionnalités sociales) : exécution du contrat qui nous lie lorsque tu crées un compte.', "Assurer la sécurité de la plateforme (prévention des abus, journaux d'erreurs) : intérêt légitime.", - 'Mémoriser vos préférences (langue, thème) : consentement, exprimé par votre usage de ces réglages.', + 'Mémoriser tes préférences (langue, thème) : consentement, exprimé par ton usage de ces réglages.', ], }, { title: '4. Destinataires et sous-traitants', body: [ - 'Vos données sont traitées par nos sous-traitants techniques : Supabase (base de données et authentification), Infomaniak (hébergement en Suisse, pays reconnu adéquat par la Commission européenne) et Cloudflare (sécurité et diffusion du contenu, encadré par des clauses contractuelles types).', + 'Tes données sont traitées par nos sous-traitants techniques : Supabase (base de données et authentification), Infomaniak (hébergement en Suisse, pays reconnu adéquat par la Commission européenne) et Cloudflare (sécurité et diffusion du contenu, encadré par des clauses contractuelles types).', 'Les informations sur les films et séries proviennent de TMDB ; aucune donnée personnelle ne lui est transmise.', - 'Nous ne vendons ni ne louons vos données. Elles ne sont communiquées à aucun autre tiers, sauf obligation légale.', + 'Nous ne vendons ni ne louons tes données. Elles ne sont communiquées à aucun autre tiers, sauf obligation légale.', ], }, { title: '5. Durée de conservation', body: [ - 'Vos données sont conservées tant que votre compte est actif. La suppression de votre compte (Réglages → Zone de danger) entraîne la suppression de vos données personnelles. Les journaux techniques sont conservés pour une durée limitée.', + 'Tes données sont conservées tant que ton compte est actif. La suppression de ton compte (Réglages → Zone de danger) entraîne la suppression de tes données personnelles. Les journaux techniques sont conservés pour une durée limitée.', ], }, { - title: '6. Vos droits', + title: '6. Tes droits', body: [ - "Vous disposez des droits d'accès, de rectification, d'effacement, de portabilité, de limitation et d'opposition, ainsi que du droit de retirer votre consentement à tout moment.", - "Vous pouvez exporter vos données à tout moment (Réglages → Données), modifier votre profil ou supprimer votre compte directement dans l'application, ou nous écrire à contact@silexio.be.", - "Vous pouvez introduire une réclamation auprès de l'Autorité de protection des données belge (APD) : www.autoriteprotectiondonnees.be.", + "Tu disposes des droits d'accès, de rectification, d'effacement, de portabilité, de limitation et d'opposition, ainsi que du droit de retirer ton consentement à tout moment.", + "Tu peux exporter tes données à tout moment (Réglages → Données), modifier ton profil ou supprimer ton compte directement dans l'application, ou nous écrire à contact@silexio.be.", + "Tu peux introduire une réclamation auprès de l'Autorité de protection des données belge (APD) : www.autoriteprotectiondonnees.be.", ], }, { title: '7. Sécurité', body: [ - "Vos données sont chiffrées en transit (TLS), les mots de passe sont hachés, et l'accès aux données est restreint par des règles de sécurité au niveau de la base de données.", + "Tes données sont chiffrées en transit (TLS), les mots de passe sont hachés, et l'accès aux données est restreint par des règles de sécurité au niveau de la base de données.", ], }, { title: '8. Cookies et stockage local', body: [ - "Nous n'utilisons que des cookies strictement nécessaires au fonctionnement du service : session de connexion, préférence de langue et répartition de charge. Vos préférences d'affichage (thème) sont stockées localement dans votre navigateur. Aucun cookie publicitaire ni traceur tiers n'est utilisé.", + "Nous n'utilisons que des cookies strictement nécessaires au fonctionnement du service : session de connexion, préférence de langue et répartition de charge. Tes préférences d'affichage (thème) sont stockées localement dans ton navigateur. Aucun cookie publicitaire ni traceur tiers n'est utilisé.", ], }, { @@ -79,18 +79,18 @@ const fr = { terms: { title: "Conditions d'utilisation", updated: 'Dernière mise à jour : 12 juillet 2026', - intro: "Les présentes conditions régissent l'utilisation de ReelMark, une plateforme personnelle de suivi de films et de séries. En créant un compte ou en utilisant le service, vous les acceptez.", + intro: "Les présentes conditions régissent l'utilisation de ReelMark, une plateforme personnelle de suivi de films et de séries. En créant un compte ou en utilisant le service, tu les acceptes.", sections: [ { title: '1. Le service', body: [ - 'ReelMark vous permet de suivre les films et séries que vous avez vus ou souhaitez voir, de noter et critiquer des œuvres, de créer des playlists et de partager votre activité avec des amis.', + 'ReelMark te permet de suivre les films et séries que tu as vus ou souhaites voir, de noter et critiquer des œuvres, de créer des playlists et de partager ton activité avec des amis.', ], }, { - title: '2. Votre compte', + title: '2. Ton compte', body: [ - "Vous devez avoir au moins 13 ans pour créer un compte. Vous êtes responsable de la confidentialité de vos identifiants et de l'activité réalisée depuis votre compte. Les informations fournies doivent être exactes.", + "Tu dois avoir au moins 13 ans pour créer un compte. Tu es responsable de la confidentialité de tes identifiants et de l'activité réalisée depuis ton compte. Les informations fournies doivent être exactes.", ], }, { @@ -100,9 +100,9 @@ const fr = { ], }, { - title: '4. Votre contenu', + title: '4. Ton contenu', body: [ - 'Vous restez propriétaire des critiques, notes et playlists que vous créez. Vous nous accordez une licence limitée pour les afficher dans le service, selon les réglages de visibilité que vous choisissez. Nous pouvons retirer tout contenu contraire à ces conditions.', + 'Tu restes propriétaire des critiques, notes et playlists que tu crées. Tu nous accordes une licence limitée pour les afficher dans le service, selon les réglages de visibilité que tu choisis. Nous pouvons retirer tout contenu contraire à ces conditions.', ], }, { @@ -120,7 +120,7 @@ const fr = { { title: '7. Résiliation', body: [ - 'Vous pouvez supprimer votre compte à tout moment depuis les réglages. Nous pouvons suspendre ou supprimer un compte en cas de violation de ces conditions.', + 'Tu peux supprimer ton compte à tout moment depuis les réglages. Nous pouvons suspendre ou supprimer un compte en cas de violation de ces conditions.', ], }, { @@ -132,7 +132,7 @@ const fr = { { title: '9. Droit applicable', body: [ - 'Les présentes conditions sont régies par le droit belge. Tout litige relève des tribunaux belges compétents, sans préjudice des droits impératifs dont vous bénéficiez en tant que consommateur.', + 'Les présentes conditions sont régies par le droit belge. Tout litige relève des tribunaux belges compétents, sans préjudice des droits impératifs dont tu bénéficies en tant que consommateur.', ], }, { @@ -146,19 +146,19 @@ const fr = { }, dashboard: { title: 'Tableau de bord', - welcome: 'Bonjour, content de vous revoir !', + welcome: 'Bonjour, content de te revoir !', subtitle: - 'Voici quelques recommandations pour votre prochaine soirée ciné.', - nextWatchings: 'Vos prochains visionnages', + 'Voici quelques recommandations pour ta prochaine soirée ciné.', + nextWatchings: 'Tes prochains visionnages', emptyLibrary: - 'Votre bibliothèque est vide. Commencez à explorer pour obtenir des recommandations !', + 'Ta bibliothèque est vide. Commence à explorer pour obtenir des recommandations !', exploreButton: 'Explorer les films', recommendations: 'Recommandations', - forYou: 'Pour vous', - onYourServices: 'Sur vos services', - becauseYouLike: 'Parce que vous aimez ${name}', - upcomingForYou: 'Bientôt, pour vous', - onAirForYou: 'En ce moment, pour vous', + forYou: 'Pour toi', + onYourServices: 'Sur tes services', + becauseYouLike: 'Parce que tu aimes ${name}', + upcomingForYou: 'Bientôt, pour toi', + onAirForYou: 'En ce moment, pour toi', notInterested: 'Pas intéressé', similarTo: 'Similaire à ${movie.movie_title}', resume: 'Reprendre', @@ -169,12 +169,12 @@ const fr = { statsEpisodesWatched: 'Épisodes vus', statsToWatch: 'À voir', continueWatching: 'Continuer à regarder', - continueWatchingCaughtUp: 'Vous êtes à jour', + continueWatchingCaughtUp: 'Tu es à jour', episodeCode: 'S{season}E{episode}', }, explorer: { title: 'Explorateur', - subtitle: 'Découvrez des films et séries à ajouter à votre collection.', + subtitle: 'Découvre des films et séries à ajouter à ta collection.', trending: 'Tendances de la semaine', nowPlaying: 'Actuellement au cinéma', popular: 'Populaires', diff --git a/lib/i18n/messages/prompts.ts b/lib/i18n/messages/prompts.ts index 12678e04..6de09a21 100644 --- a/lib/i18n/messages/prompts.ts +++ b/lib/i18n/messages/prompts.ts @@ -2,7 +2,8 @@ const fr = { dismiss: 'Plus tard', push: { title: 'Activer les notifications', - description: 'Sois prévenu dès qu’un nouvel épisode de tes séries sort.', + description: + 'Sois prévenu dès qu’un nouvel épisode de tes séries sort.', action: 'Activer', }, import: { diff --git a/lib/i18n/messages/settings.ts b/lib/i18n/messages/settings.ts index 493234e2..a57577b5 100644 --- a/lib/i18n/messages/settings.ts +++ b/lib/i18n/messages/settings.ts @@ -18,23 +18,23 @@ const fr = { suggestions: 'Suggestions', }, title: 'Paramètres', - subtitle: 'Gérez votre compte et vos préférences', + subtitle: 'Gère ton compte et tes préférences', missingFields: 'Tous les champs sont obligatoires', usernameTaken: 'Ce pseudo est déjà utilisé', successUpdate: ' mis à jour avec succès', language: { title: 'Langue', - description: 'Choisissez votre langue préférée', + description: 'Choisis ta langue préférée', french: 'Français', english: 'Anglais', }, region: { title: 'Région Théâtrale', description: - 'Choisissez votre pays pour obtenir les dates de sorties au cinéma locales', + 'Choisis ton pays pour obtenir les dates de sorties au cinéma locales', label: 'Pays / Région', save: 'Enregistrer la région', - placeholder: 'Sélectionnez un pays', + placeholder: 'Sélectionne un pays', be: '🇧🇪 Belgique', fr: '🇫🇷 France', us: '🇺🇸 États-Unis', @@ -46,15 +46,15 @@ const fr = { streaming: { title: 'Services de streaming', description: - 'Sélectionnez vos plateformes pour voir les recommandations disponibles sur vos services', + 'Sélectionne tes plateformes pour voir les recommandations disponibles sur tes services', save: 'Enregistrer les services', saved: 'Services mis à jour', - empty: 'Aucun service disponible pour votre région', + empty: 'Aucun service disponible pour ta région', selectedCount: '${count} sélectionné(s)', }, theme: { title: 'Apparence', - description: "Personnalisez l'apparence de votre interface", + description: "Personnalise l'apparence de ton interface", light: '☀️ Clair', lightDesc: 'Thème pour les environnements lumineux', dark: '🌙 Sombre', @@ -64,24 +64,23 @@ const fr = { }, profile: { title: 'Profil', - description: 'Gérez les informations de votre profil', + description: 'Gère les informations de ton profil', fullName: 'Nom complet', username: 'Pseudo', email: 'Adresse e-mail', - warningEmail: 'Votre adresse e-mail ne peut pas être modifiée ici', + warningEmail: 'Ton adresse e-mail ne peut pas être modifiée ici', newEmail: "L'adresse e-mail est obligatoire", - confirmNewEmail: - 'Email mis à jour. Veuillez confirmer le nouveau email.', + confirmNewEmail: 'Email mis à jour. Confirme le nouveau email.', avatar: 'Photo de profil', invalidAvatarUrl: "URL d'avatar invalide", formatAvatar: 'PNG, JPG ou GIF. Taille maximale 5MB', updateAvatar: 'Mettre à jour la photo', updateProfile: 'Mettre à jour le profil', - placeholder: 'Votre ', + placeholder: 'Ton ', }, password: { title: 'Mot de passe', - description: 'Modifiez votre mot de passe pour sécuriser votre compte', + description: 'Modifie ton mot de passe pour sécuriser ton compte', currentPassword: 'Mot de passe actuel', newPassword: 'Nouveau mot de passe', confirmPassword: 'Confirmer le mot de passe', @@ -110,17 +109,17 @@ const fr = { description: 'Les actions suivantes ne peuvent pas être annulées', deleteAccount: 'Supprimer le compte', deleteAccountDesc: - 'Une fois supprimé, vous ne pourrez pas récupérer vos données', + 'Une fois supprimé, tu ne pourras pas récupérer tes données', confirmDelete: 'Je comprends que mon compte sera supprimé', incorrectConfirmation: - 'Texte de confirmation incorrect. Veuillez taper "SUPPRIMER".', - confirmPassword: 'Entrez votre mot de passe pour confirmer', + 'Texte de confirmation incorrect. Tape "SUPPRIMER".', + confirmPassword: 'Entre ton mot de passe pour confirmer', passwordRequired: 'Mot de passe requis pour confirmer la suppression', incorrectPassword: 'Mot de passe incorrect', }, social: { title: 'Réseaux sociaux', - description: 'Ajoutez vos liens pour les afficher sur votre profil', + description: 'Ajoute tes liens pour les afficher sur ton profil', bio: 'Bio', bioPlaceholder: 'Parle-moi de toi...', instagram: 'Instagram', From 7dec082f8f1e09bda2be8b33895826faf9fcd5d9 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 03:17:25 +0200 Subject: [PATCH 22/98] chore: Update dependencies --- package.json | 48 +- pnpm-lock.yaml | 1442 ++++++++++++++++++++++--------------------- pnpm-workspace.yaml | 5 + 3 files changed, 781 insertions(+), 714 deletions(-) diff --git a/package.json b/package.json index 10e4888b..1625ca8b 100644 --- a/package.json +++ b/package.json @@ -15,53 +15,53 @@ "test:e2e:ui": "playwright test --ui" }, "dependencies": { - "@radix-ui/react-dialog": "^1.1.18", - "@radix-ui/react-dropdown-menu": "^2.1.19", - "@radix-ui/react-label": "^2.1.11", - "@radix-ui/react-popover": "^1.1.18", - "@radix-ui/react-separator": "^1.1.11", - "@radix-ui/react-slot": "^1.3.0", - "@radix-ui/react-switch": "^1.3.2", - "@sentry/nextjs": "^10.63.0", - "@serwist/next": "^9.5.11", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-popover": "^1.1.23", + "@radix-ui/react-separator": "^1.1.15", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-switch": "^1.3.7", + "@sentry/nextjs": "^10.68.0", + "@serwist/next": "^9.5.12", "@supabase/ssr": "^0.10.3", - "@supabase/supabase-js": "^2.110.0", + "@supabase/supabase-js": "^2.110.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "fast-average-color": "^9.5.2", - "fuse.js": "^7.4.2", - "lucide-react": "^1.23.0", + "fuse.js": "^7.5.0", + "lucide-react": "^1.27.0", "next": "16.2.10", "nextjs-toploader": "^3.9.17", "react": "19.2.7", "react-dom": "19.2.7", - "serwist": "^9.5.11", - "simple-icons": "^16.24.1", + "serwist": "^9.5.12", + "simple-icons": "^16.27.1", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "virtua": "^0.49.3", "web-push": "^3.6.7" }, "devDependencies": { - "@playwright/test": "^1.61.1", - "@tailwindcss/postcss": "^4.3.2", - "@types/node": "^25.9.4", + "@playwright/test": "^1.62.0", + "@tailwindcss/postcss": "^4.3.3", + "@types/node": "^25.9.5", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/web-push": "^3.6.4", - "@vitest/coverage-v8": "^4.1.9", + "@vitest/coverage-v8": "^4.1.10", "dotenv": "^17.4.2", - "eslint": "^9.39.4", + "eslint": "^9.39.5", "eslint-config-next": "16.2.10", "eslint-config-prettier": "^10.1.8", - "lightningcss": "^1.32.0", - "prettier": "^3.9.4", - "tailwindcss": "^4.3.2", + "lightningcss": "^1.33.0", + "prettier": "^3.9.6", + "tailwindcss": "^4.3.3", "tw-animate-css": "^1.4.0", "typescript": "^6.0.3", - "vitest": "^4.1.9" + "vitest": "^4.1.10" }, - "packageManager": "pnpm@11.15.0+sha512.266f8957a30d2be6e9468e5e66bcdedd35a794175f71b067ba8504d686cce1d0c0f429b33c323c3c569ad4891e667574a49ff71d1b89a22cc66f13c65818c578", + "packageManager": "pnpm@11.17.0+sha512.cca3cea332ad254bb84145f966d19f4879615210346fc92c79a047f23a0d7b3cca3c3792f0076ba1f1831d277efbcf0a9119b31a9a60eca7fb3d6231f331ef72", "engines": { "node": ">=22.13" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a8b897b..e82b335b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,43 +6,45 @@ settings: overrides: postcss: '>=8.5.10' + simple-icons: '>=16.27.1' + lucide-react: '>=1.27.0' importers: .: dependencies: '@radix-ui/react-dialog': - specifier: ^1.1.18 - version: 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^1.1.23 + version: 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-dropdown-menu': - specifier: ^2.1.19 - version: 2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^2.1.24 + version: 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-label': - specifier: ^2.1.11 - version: 2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^2.1.15 + version: 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-popover': - specifier: ^1.1.18 - version: 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^1.1.23 + version: 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-separator': - specifier: ^1.1.11 - version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-slot': - specifier: ^1.3.0 - version: 1.3.0(@types/react@19.2.17)(react@19.2.7) + specifier: ^1.3.3 + version: 1.3.3(@types/react@19.2.17)(react@19.2.7) '@radix-ui/react-switch': - specifier: ^1.3.2 - version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^1.3.7 + version: 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@sentry/nextjs': - specifier: ^10.63.0 - version: 10.66.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@8.1.1)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19)) + specifier: ^10.68.0 + version: 10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) '@serwist/next': - specifier: ^9.5.11 - version: 9.5.11(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19)) + specifier: ^9.5.12 + version: 9.5.12(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) '@supabase/ssr': specifier: ^0.10.3 - version: 0.10.3(@supabase/supabase-js@2.110.7) + version: 0.10.3(@supabase/supabase-js@2.110.8) '@supabase/supabase-js': - specifier: ^2.110.0 - version: 2.110.7 + specifier: ^2.110.8 + version: 2.110.8 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -53,17 +55,17 @@ importers: specifier: ^9.5.2 version: 9.5.2 fuse.js: - specifier: ^7.4.2 + specifier: ^7.5.0 version: 7.5.0 lucide-react: - specifier: ^1.23.0 - version: 1.25.0(react@19.2.7) + specifier: ^1.27.0 + version: 1.27.0(react@19.2.7) next: specifier: 16.2.10 - version: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) nextjs-toploader: specifier: ^3.9.17 - version: 3.9.17(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.9.17(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: specifier: 19.2.7 version: 19.2.7 @@ -71,11 +73,11 @@ importers: specifier: 19.2.7 version: 19.2.7(react@19.2.7) serwist: - specifier: ^9.5.11 - version: 9.5.11(browserslist@4.28.2)(typescript@6.0.3) + specifier: ^9.5.12 + version: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) simple-icons: - specifier: ^16.24.1 - version: 16.26.0 + specifier: ^16.27.1 + version: 16.27.1 sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -90,13 +92,13 @@ importers: version: 3.6.7(supports-color@8.1.1) devDependencies: '@playwright/test': - specifier: ^1.61.1 - version: 1.61.1 + specifier: ^1.62.0 + version: 1.62.0 '@tailwindcss/postcss': - specifier: ^4.3.2 + specifier: ^4.3.3 version: 4.3.3 '@types/node': - specifier: ^25.9.4 + specifier: ^25.9.5 version: 25.9.5 '@types/react': specifier: ^19.2.17 @@ -108,28 +110,28 @@ importers: specifier: ^3.6.4 version: 3.6.4 '@vitest/coverage-v8': - specifier: ^4.1.9 + specifier: ^4.1.10 version: 4.1.10(vitest@4.1.10) dotenv: specifier: ^17.4.2 version: 17.4.2 eslint: - specifier: ^9.39.4 + specifier: ^9.39.5 version: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) eslint-config-next: specifier: 16.2.10 - version: 16.2.10(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + version: 16.2.10(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) lightningcss: - specifier: ^1.32.0 - version: 1.32.0 + specifier: ^1.33.0 + version: 1.33.0 prettier: - specifier: ^3.9.4 - version: 3.9.5 + specifier: ^3.9.6 + version: 3.9.6 tailwindcss: - specifier: ^4.3.2 + specifier: ^4.3.3 version: 4.3.3 tw-animate-css: specifier: ^1.4.0 @@ -138,7 +140,7 @@ importers: specifier: ^6.0.3 version: 6.0.3 vitest: - specifier: ^4.1.9 + specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0)) packages: @@ -149,17 +151,17 @@ packages: } engines: { node: '>=10' } - '@apm-js-collab/code-transformer-bundler-plugins@0.6.2': + '@apm-js-collab/code-transformer-bundler-plugins@0.7.1': resolution: { - integrity: sha512-5vBrtIEL+UVbO0YWWoyYG4QMgR+ZfnIL3xlteIkAmU7YaAPhc28k3md/NM14tnkfXKjKOn9yUzEA9AYUmNpvJg==, + integrity: sha512-Yidf5GOl60db80UxUtNdKK3pnY7obU/gs0xOfA0SCdnvVLMCvfYIer/egC3TqpPiT0Jg22eg3RlzcO+zKfPMcA==, } engines: { node: '>=18.0.0' } - '@apm-js-collab/code-transformer@0.18.0': + '@apm-js-collab/code-transformer@0.18.1': resolution: { - integrity: sha512-aN3Oq8r1J3gPJtCwErP664gM0+HhM1I1lujPr9TMTCcEl/joQQbpGpeMdts9B1+W2wHMsvioDMv5F4PvMWE6gw==, + integrity: sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==, } hasBin: true @@ -333,10 +335,10 @@ packages: integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==, } - '@eslint-community/eslint-utils@4.9.1': + '@eslint-community/eslint-utils@4.10.1': resolution: { - integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==, + integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==, } engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: @@ -864,10 +866,10 @@ packages: } engines: { node: '>=8.0.0' } - '@opentelemetry/core@2.9.0': + '@opentelemetry/core@2.10.0': resolution: { - integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==, + integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: @@ -882,28 +884,28 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/resources@2.9.0': + '@opentelemetry/resources@2.10.0': resolution: { - integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==, + integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.9.0': + '@opentelemetry/sdk-trace-base@2.10.0': resolution: { - integrity: sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==, + integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace@2.9.0': + '@opentelemetry/sdk-trace@2.10.0': resolution: { - integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==, + integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: @@ -922,24 +924,24 @@ packages: integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==, } - '@playwright/test@1.61.1': + '@playwright/test@1.62.0': resolution: { - integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==, + integrity: sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==, } - engines: { node: '>=18' } + engines: { node: '>=20' } hasBin: true - '@radix-ui/primitive@1.1.5': + '@radix-ui/primitive@1.1.7': resolution: { - integrity: sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==, + integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==, } - '@radix-ui/react-arrow@1.1.11': + '@radix-ui/react-arrow@1.1.15': resolution: { - integrity: sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==, + integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==, } peerDependencies: '@types/react': '*' @@ -952,10 +954,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.1.12': + '@radix-ui/react-collection@1.1.15': resolution: { - integrity: sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==, + integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==, } peerDependencies: '@types/react': '*' @@ -968,10 +970,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-compose-refs@1.1.3': + '@radix-ui/react-compose-refs@1.1.5': resolution: { - integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==, + integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==, } peerDependencies: '@types/react': '*' @@ -980,10 +982,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-context@1.2.0': + '@radix-ui/react-context@1.2.2': resolution: { - integrity: sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==, + integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==, } peerDependencies: '@types/react': '*' @@ -992,10 +994,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-dialog@1.1.19': + '@radix-ui/react-dialog@1.1.23': resolution: { - integrity: sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg==, + integrity: sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==, } peerDependencies: '@types/react': '*' @@ -1008,10 +1010,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-direction@1.1.2': + '@radix-ui/react-direction@1.1.4': resolution: { - integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==, + integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==, } peerDependencies: '@types/react': '*' @@ -1020,10 +1022,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.1.15': + '@radix-ui/react-dismissable-layer@1.1.19': resolution: { - integrity: sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA==, + integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==, } peerDependencies: '@types/react': '*' @@ -1036,10 +1038,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dropdown-menu@2.1.20': + '@radix-ui/react-dropdown-menu@2.1.24': resolution: { - integrity: sha512-slfm+rRaZRuQBvHq60lXvSVUPhid0IPtjSZzIuUlWZMUs01iYZNlGS3mJgRD3ChLQVBAYlKiL/tFyWGX+dz8Xw==, + integrity: sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==, } peerDependencies: '@types/react': '*' @@ -1052,10 +1054,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-focus-guards@1.1.4': + '@radix-ui/react-focus-guards@1.1.6': resolution: { - integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==, + integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==, } peerDependencies: '@types/react': '*' @@ -1064,10 +1066,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-focus-scope@1.1.12': + '@radix-ui/react-focus-scope@1.1.16': resolution: { - integrity: sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A==, + integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==, } peerDependencies: '@types/react': '*' @@ -1080,10 +1082,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-id@1.1.2': + '@radix-ui/react-id@1.1.4': resolution: { - integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==, + integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==, } peerDependencies: '@types/react': '*' @@ -1092,10 +1094,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-label@2.1.11': + '@radix-ui/react-label@2.1.15': resolution: { - integrity: sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ==, + integrity: sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==, } peerDependencies: '@types/react': '*' @@ -1108,10 +1110,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-menu@2.1.20': + '@radix-ui/react-menu@2.1.24': resolution: { - integrity: sha512-VsUrXxFe9d2ScbZF0fR/oPR1+qjyeLs5p0jzG8h90puMoA9bq4SirYlXbE+USRg9Q2qTeJSFNqjw2nts8jJe4w==, + integrity: sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==, } peerDependencies: '@types/react': '*' @@ -1124,10 +1126,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popover@1.1.19': + '@radix-ui/react-popover@1.1.23': resolution: { - integrity: sha512-jkrTdQVxnIB8fpn0NyyxW9CTB5aCXZZelVz5z+Xmii6g5WxMqS3fInNslZ63puP39+Puu4jYohUK31y3dT87gQ==, + integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==, } peerDependencies: '@types/react': '*' @@ -1140,10 +1142,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popper@1.3.3': + '@radix-ui/react-popper@1.3.7': resolution: { - integrity: sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g==, + integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==, } peerDependencies: '@types/react': '*' @@ -1156,10 +1158,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-portal@1.1.13': + '@radix-ui/react-portal@1.1.17': resolution: { - integrity: sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==, + integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==, } peerDependencies: '@types/react': '*' @@ -1172,10 +1174,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-presence@1.1.7': + '@radix-ui/react-presence@1.1.10': resolution: { - integrity: sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA==, + integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==, } peerDependencies: '@types/react': '*' @@ -1188,10 +1190,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.7': + '@radix-ui/react-primitive@2.1.10': resolution: { - integrity: sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==, + integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==, } peerDependencies: '@types/react': '*' @@ -1204,10 +1206,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-roving-focus@1.1.15': + '@radix-ui/react-roving-focus@1.1.19': resolution: { - integrity: sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg==, + integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==, } peerDependencies: '@types/react': '*' @@ -1220,10 +1222,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-separator@1.1.11': + '@radix-ui/react-separator@1.1.15': resolution: { - integrity: sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==, + integrity: sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==, } peerDependencies: '@types/react': '*' @@ -1236,10 +1238,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slot@1.3.0': + '@radix-ui/react-slot@1.3.3': resolution: { - integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==, + integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==, } peerDependencies: '@types/react': '*' @@ -1248,10 +1250,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-switch@1.3.3': + '@radix-ui/react-switch@1.3.7': resolution: { - integrity: sha512-1+mlB4/lxJfk5tgJ4g+R5mUCbRpPE1T9+UsEyeLYbGgMtwiMgmuTnfKz4Mw1nHALHjuwyxw4MLd4cSHn6pNSlQ==, + integrity: sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==, } peerDependencies: '@types/react': '*' @@ -1264,10 +1266,10 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-use-callback-ref@1.1.2': + '@radix-ui/react-use-callback-ref@1.1.4': resolution: { - integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==, + integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==, } peerDependencies: '@types/react': '*' @@ -1276,10 +1278,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-controllable-state@1.2.3': + '@radix-ui/react-use-controllable-state@1.2.6': resolution: { - integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==, + integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==, } peerDependencies: '@types/react': '*' @@ -1288,10 +1290,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-effect-event@0.0.3': + '@radix-ui/react-use-effect-event@0.0.5': resolution: { - integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==, + integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==, } peerDependencies: '@types/react': '*' @@ -1300,10 +1302,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-is-hydrated@0.1.1': + '@radix-ui/react-use-is-hydrated@0.1.3': resolution: { - integrity: sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==, + integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==, } peerDependencies: '@types/react': '*' @@ -1312,10 +1314,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-layout-effect@1.1.2': + '@radix-ui/react-use-layout-effect@1.1.4': resolution: { - integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==, + integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==, } peerDependencies: '@types/react': '*' @@ -1324,10 +1326,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-previous@1.1.2': + '@radix-ui/react-use-rect@1.1.4': resolution: { - integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==, + integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==, } peerDependencies: '@types/react': '*' @@ -1336,10 +1338,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-rect@1.1.2': + '@radix-ui/react-use-size@1.1.4': resolution: { - integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==, + integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==, } peerDependencies: '@types/react': '*' @@ -1348,22 +1350,10 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-size@1.1.2': + '@radix-ui/rect@1.1.3': resolution: { - integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/rect@1.1.2': - resolution: - { - integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==, + integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==, } '@rolldown/binding-android-arm64@1.1.5': @@ -1762,17 +1752,17 @@ packages: } engines: { node: '>= 18' } - '@sentry/browser-utils@10.66.0': + '@sentry/browser-utils@10.68.0': resolution: { - integrity: sha512-sHuALvJMMEilUz84F1ZOQuDoZ3MhjwUHWHkXcElcVMrDIlnTO7Ra0E6Kh0e8JyJaLBMk4Sy9srKSqH9zimQVTQ==, + integrity: sha512-be8VtdjCngKc77cstJeV+gO15iH+blyXpBDk8yOehmtX4BkFO33mfTMNCWVR2LA0oOxjIHWRAhf77fIUEhzxPg==, } engines: { node: '>=18' } - '@sentry/browser@10.66.0': + '@sentry/browser@10.68.0': resolution: { - integrity: sha512-MaPoBqKvI7O3UhexSJ/KO/o6T4Tr3A+vQWLZYTos0mxd59jVKMKbbJ6LxM/0uWQ5JAO2XYC/kCMRvk6VqQ5QZw==, + integrity: sha512-8xVgk7oG2lajXnbXF6a7H1xMZ/U6icqSldHGzQu1+bajfrK8Gan9ULG/Xsj1VM1LlNeK6/7znDJ3u1jgvIwznw==, } engines: { node: '>=18' } @@ -1783,10 +1773,10 @@ packages: } engines: { node: '>= 18' } - '@sentry/bundler-plugins@10.66.0': + '@sentry/bundler-plugins@10.68.0': resolution: { - integrity: sha512-u0Dzji4GouTR3zhNzCDal+mDxzOsnCVzA4mxOkTq89k2vKX6cuAVrrBWlSOcVYa8qbt43c0QNJxFnD0AqjhJtw==, + integrity: sha512-XWv7asJuTTUSlacROvqcIFKuNxAf3PYL/mdjMBhBwp3rJ4vMkA73jqNPjqCTm726cHs/Y4yadbbFA/OZLPWzeg==, } engines: { node: '>= 18' } peerDependencies: @@ -1884,33 +1874,33 @@ packages: } engines: { node: '>=14' } - '@sentry/core@10.66.0': + '@sentry/core@10.68.0': resolution: { - integrity: sha512-9UbgSvds7bMJsP561eWmeyMLcfOmnwxtnx2QuW3yLobzP2Ob7CyJCOzP4tGzlTAGDrzShkFEZhiyuBUKiEK2oQ==, + integrity: sha512-5Amhx8ltVz7vb1bRGyf3c4J69/iHW8R/H+SJxTRILHlsSOBrnVVc/IQEYDC6PTRdRdZ3x2u7RVjxZi2Mhe525g==, } engines: { node: '>=18' } - '@sentry/feedback@10.66.0': + '@sentry/feedback@10.68.0': resolution: { - integrity: sha512-fr69K76Gz7RRyfMerChdNjWIeQdFh+k21GJcmPCqk43dscUChOsLc3cYLS5cK7iZ/XiUmH9lVzf7EYL2kf2qsA==, + integrity: sha512-XbdcXiBnpC3vgw46eHOPeD/ZQ+XzluP75ubdUcaPDW02hCh2nsdXiwjZ2DBImbpvIpTbJgjHf/sIlHWvcZJ2Mg==, } engines: { node: '>=18' } - '@sentry/nextjs@10.66.0': + '@sentry/nextjs@10.68.0': resolution: { - integrity: sha512-t+FodSqOWII61kbkgeGkcT6Aii8eDnpQAEmky28w/rBfl715fPN3K/qeGhP2D1Q+DsjdvkJfHDRL1jZBVwFWZQ==, + integrity: sha512-oMDl9F8jkCgAsd8U9gLTAWr7u/j9/nUw0sBRHoAvK7HXfdrg9fPlsDd3wsHgYDC0U2tIDTXICXAwT2zJ7OHHWg==, } engines: { node: '>=18' } peerDependencies: next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0 - '@sentry/node-core@10.66.0': + '@sentry/node-core@10.68.0': resolution: { - integrity: sha512-SUnXHROqSdSetKgZC1goDEKCuMz3OmQ1h4rxzWeexLyqan+pensZXfLouP6jzZXlA8e/HP7uQg78LnfqYDcZlQ==, + integrity: sha512-VreORXnruy8A2SyprZKENAq3ArGwn35KPewLQSQ5dgDXSFtUj2scLuzZoVsZ/Uyt83td0WPvD6Lv0X7MvSYAMQ==, } engines: { node: '>=18' } peerDependencies: @@ -1931,17 +1921,17 @@ packages: '@opentelemetry/sdk-trace-base': optional: true - '@sentry/node@10.66.0': + '@sentry/node@10.68.0': resolution: { - integrity: sha512-5Ow7iQiRjaSaEOmqEIkYV368hFFzShIZCXPoj+wX3JtOmKFrsNu6lr8/VJlQs7cyjFS6tzE50PrLrD8iAPS/8w==, + integrity: sha512-bnvRzEehquG/894DD3BWNCBUbDWsLQfYPU+SNCMd6G4Ext75RthVkRs8R0sRaE6b8Tw9HSEgySHL834Tf8lVsA==, } engines: { node: '>=18' } - '@sentry/opentelemetry@10.66.0': + '@sentry/opentelemetry@10.68.0': resolution: { - integrity: sha512-K5Y9IettN9yIOnpqCs40HRLGqaGUoaQ50+ZsLqX2kPCk3TzJVpKZ9icKwaJ4Nxm72QgGVT5Ovfr/9FXbgd3b/Q==, + integrity: sha512-JDNH9dacX0MSi7FRvabPCJUAXRMTe16bE/Gajc/7Gfcw7Rwvo4DZ0nd162PCSAW9QoEjUT12upDehEHJuFmsXg==, } engines: { node: '>=18' } peerDependencies: @@ -1949,40 +1939,40 @@ packages: '@opentelemetry/core': ^1.30.1 || ^2.1.0 '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - '@sentry/react@10.66.0': + '@sentry/react@10.68.0': resolution: { - integrity: sha512-aodV9C2NnaZBqJvaBceIIaVyMVEisO38EWtH5xL7IMbD6QSmBYsuNU7yYzRcdOj/+99FxJYRPaqwzGnIWnNTHw==, + integrity: sha512-rIq4QR4ScMHHx9JJZv7Jgw31bMdUVJMx+ykHIJb7htjY6mj78sjKs+KpCsMDnvJxDhSmvftGM1KfKD4BggL7OQ==, } engines: { node: '>=18' } peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - '@sentry/replay-canvas@10.66.0': + '@sentry/replay-canvas@10.68.0': resolution: { - integrity: sha512-z7fPWEIfAJMJySKXFIqXAzFpkHjp13WGMAmB9+hlHhi9+gjAO8owD9R4XTd86lBswveGwMHKvIx5lFKNVqDcow==, + integrity: sha512-HusYcr+He+ohnUDHunYrc5St6vdDnBXpUAndnT5ReyUMVSCiWKfY3paXowU/0787HwYfxdcpZgwC5u79+XbEIg==, } engines: { node: '>=18' } - '@sentry/replay@10.66.0': + '@sentry/replay@10.68.0': resolution: { - integrity: sha512-Djb4FxQa9bF8z3PoCO00Nw1u+6hma2YuI8JkMoE+F14KWRfCZKQ28sScsvk+OokbXgjtuIsxDEZj/qFGY0w01w==, + integrity: sha512-ZoG2n16vbkx4GWSCnLIqUUN9xlUmccQFbQ2US2rhruQeHTUnHl/ukr8NHOQXZaEbwKyMkX9bEMwfmZHJm+wSTQ==, } engines: { node: '>=18' } - '@sentry/server-utils@10.66.0': + '@sentry/server-utils@10.68.0': resolution: { - integrity: sha512-h9EM9Wz9Mc6w2Vn7Fyh6ozjy4JC2UbUvWf9Ra1HYA4KMeYqjFA5/o0oB4pg4w/TF+rb+9OF/HNqxjuczxpudpA==, + integrity: sha512-lp1ZSs1auw7HrCESSYt/n4dOUaKPVUIAKyVYRk6xVr4bMIN3RPub/H5Wm7QPj9CpXVC5bQFvB6+dHZXz809oMg==, } engines: { node: '>=18' } - '@sentry/vercel-edge@10.66.0': + '@sentry/vercel-edge@10.68.0': resolution: { - integrity: sha512-uNctdCoSDFgbm7Cg+8tMOBIRT1R5Xt6oU3J0WyJkD1DDmJ/8SDlLNdAbckZwPr96KFFF4MavuP2Uf7zXxratfQ==, + integrity: sha512-6FukkfH1b3T7jMgPVE7todrKcC92zA0gwUA3kyTG645eOdfMTIQ9o5lr5UECixALmzTB0GAgrr2gH9xlR0R9bA==, } engines: { node: '>=18' } @@ -1995,10 +1985,10 @@ packages: peerDependencies: webpack: '>=5.0.0' - '@serwist/build@9.5.11': + '@serwist/build@9.5.12': resolution: { - integrity: sha512-PQfW+LhADYFOOp0PhEnjlgJCyKor6cYa06d3rID1OpiKzkmCApJV1WYfdTBB96jXaWv6OWcWSbSV4tqDLxvaVA==, + integrity: sha512-U2UkA9BjdpniZkXDIG6NQRBEbXccRvqewzL3hfNEQERvM2bL6ezKvsVF9359akUWP8BxV950Kkq9LKGFmOR8Uw==, } engines: { node: '>=18.0.0' } peerDependencies: @@ -2007,14 +1997,14 @@ packages: typescript: optional: true - '@serwist/next@9.5.11': + '@serwist/next@9.5.12': resolution: { - integrity: sha512-omT32H7U21ihCymSvOG9QeRJBuOEomJx4JdzKhUoqOW3DR10tH3m84VOHj3BvK0OcA7av3qj5FsyNFBB+f0n8A==, + integrity: sha512-aKSDZmxC2w6mvLC1aOcO4OnZqdKIl4zrW2VndlZT12+m+nIyOWih1xqqw4LsvUGfQCx0AswtKUUQ7woRYZ2thg==, } engines: { node: '>=18.0.0' } peerDependencies: - '@serwist/cli': ^9.5.11 + '@serwist/cli': ^9.5.12 next: '>=14.0.0' react: '>=18.0.0' typescript: '>=5.0.0' @@ -2024,10 +2014,10 @@ packages: typescript: optional: true - '@serwist/utils@9.5.11': + '@serwist/utils@9.5.12': resolution: { - integrity: sha512-zqxmwuHqWA3OwN82Wo8gFZ9QBemygJP3cap5JWAOG4UyJZgUZfmBXAXj+IMaD4eKZ/6pqrxHHDZ9uSWZmJ1mXA==, + integrity: sha512-BHDwiGL7H7JS7wFvVlAWTFHGt0ffHuPgKtdkaYP0lEWJFl6fKJRCnTDZhwhusFvBhicf37XP8Y6PouTZbcLmgg==, } peerDependencies: browserslist: '>=4' @@ -2035,10 +2025,10 @@ packages: browserslist: optional: true - '@serwist/webpack-plugin@9.5.11': + '@serwist/webpack-plugin@9.5.12': resolution: { - integrity: sha512-SlvO3A1UMcc1htCzMtLCtPQK6yISCO7B859ixLv7EiY/yayXjVxGm9vHqkJYpQ768PWyjEZXRY/X6EGRMA6wJQ==, + integrity: sha512-+a5Fap7wVR4oN5jg1GxVNbWW+xUv3LO7UWYfg2D8PkJZiTCcX8PwkcmdzMGVr39dyOtIMTSAANw0af1zMjVbOA==, } engines: { node: '>=18.0.0' } peerDependencies: @@ -2050,10 +2040,10 @@ packages: webpack: optional: true - '@serwist/window@9.5.11': + '@serwist/window@9.5.12': resolution: { - integrity: sha512-OrH9srhmifUvY36NuukHSZby24XTEk4pHh3pfY0GBQzA9ouU1fYh+ORWhKxH7/wkVHRr3sc4YAhjtpfL14PjjQ==, + integrity: sha512-+fjApJme34qfwdGE2kLT8sFx+xWO+a15477MN/B/b1v4HOWBmARuA9C2xGFBCT77XzCrzQ/2DxcDePZsqEYOlw==, } peerDependencies: typescript: '>=5.0.0' @@ -2067,17 +2057,17 @@ packages: integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, } - '@supabase/auth-js@2.110.7': + '@supabase/auth-js@2.110.8': resolution: { - integrity: sha512-M5Bpl4hCv6kHcOO/xM06Dyfg1mYLHljMkp1plhzG9IRZPc3czvyMsSN1XpL5+GKisOKM3lSN59zhpcm6sMVXfA==, + integrity: sha512-TQ5neTUDX2C2WmyYa03yGhLMkhdE/SkHXtK8/qxO/APUy3rsymsJCBP48p4jcN6iO2G0ow6RRexQd2mX+dSyJg==, } engines: { node: '>=22.0.0' } - '@supabase/functions-js@2.110.7': + '@supabase/functions-js@2.110.8': resolution: { - integrity: sha512-megYmexlYEoR/0qlsr4Snh9wtzAodO7MAri3NMevZrXzNvQRKlvmTcSBoKGLQEPDakgDZMqbMdf9DwoZz6qfoA==, + integrity: sha512-5yB9TLYzvv2oSQxwb0gamEvIAsuH66pVt7AM/pz03S7wN6ehD34GNgbShrccetqPedXQSz7e/1hAJ9NeEhoZVg==, } engines: { node: '>=22.0.0' } @@ -2087,17 +2077,17 @@ packages: integrity: sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==, } - '@supabase/postgrest-js@2.110.7': + '@supabase/postgrest-js@2.110.8': resolution: { - integrity: sha512-ban6YV0djhVaqVYezlOARKLIuOBSvLLhyQVZjA2nxPrtswhxHCl1+gI4giFgI9ATQAaMNbUZb4JXiuL5lEA/5g==, + integrity: sha512-QeRROxl1PpOZw5Jzi7BwdN9icsycMrLlCCvsjS0hYLW+nZoaT46zdagz/glJirj8jHF4jSd5Jyipuae2cBClCw==, } engines: { node: '>=22.0.0' } - '@supabase/realtime-js@2.110.7': + '@supabase/realtime-js@2.110.8': resolution: { - integrity: sha512-AMtZjyFA2gsmjuxopPNS/sRznLQHG0Ht5x+ytTPTOh3vAcOTUlVRLx7gW4/CONNnbb3PKOkE+HmM35HOSbmomQ==, + integrity: sha512-mwX7ituX6O31fLf+0g65rpLlNxqgnMaPltPsQwzox6jfmbfVl3tCxXrfr3HEsQcCRjpjuJG1+A0vFzP1yVjKHA==, } engines: { node: '>=22.0.0' } @@ -2109,17 +2099,17 @@ packages: peerDependencies: '@supabase/supabase-js': ^2.105.3 - '@supabase/storage-js@2.110.7': + '@supabase/storage-js@2.110.8': resolution: { - integrity: sha512-2tcDE8cjEDy1uKxKavBpKQod1JdMV1jDXQag48TCa+kycmJOltc0yVabC0BUlhOwAl6WykXU2aOsH3ELMtZrmQ==, + integrity: sha512-CcfhkZFBLxsthgUabZKxwfsoXdrikIGsL3LsGoV3FZTqCMx/s1y49taT4jT/oya5+1IuB0sFFHw6pF0o0iJniQ==, } engines: { node: '>=22.0.0' } - '@supabase/supabase-js@2.110.7': + '@supabase/supabase-js@2.110.8': resolution: { - integrity: sha512-AnfO3A230Shy6RMO7cya3Wl1OcXnABJrzH8vP+fY7/RFjhzcchB7DjKkkTIAntlwekD+GkSFzEvt2tC+D4Fp8w==, + integrity: sha512-E5qzoe74zhJRv4wRcbO9eMYzeQDb/+h6c603pL8shcxLGBjTKsIF7XXj05IcNj23TLDgJN1WkMw7mwAPyu5dZg==, } engines: { node: '>=22.0.0' } @@ -2334,92 +2324,92 @@ packages: integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==, } - '@typescript-eslint/eslint-plugin@8.64.0': + '@typescript-eslint/eslint-plugin@8.65.0': resolution: { - integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==, + integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: - '@typescript-eslint/parser': ^8.64.0 + '@typescript-eslint/parser': ^8.65.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.64.0': + '@typescript-eslint/parser@8.65.0': resolution: { - integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==, + integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.64.0': + '@typescript-eslint/project-service@8.65.0': resolution: { - integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==, + integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.64.0': + '@typescript-eslint/scope-manager@8.65.0': resolution: { - integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==, + integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@typescript-eslint/tsconfig-utils@8.64.0': + '@typescript-eslint/tsconfig-utils@8.65.0': resolution: { - integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==, + integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.64.0': + '@typescript-eslint/type-utils@8.65.0': resolution: { - integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==, + integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.64.0': + '@typescript-eslint/types@8.65.0': resolution: { - integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==, + integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@typescript-eslint/typescript-estree@8.64.0': + '@typescript-eslint/typescript-estree@8.65.0': resolution: { - integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==, + integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.64.0': + '@typescript-eslint/utils@8.65.0': resolution: { - integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==, + integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.64.0': + '@typescript-eslint/visitor-keys@8.65.0': resolution: { - integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==, + integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } @@ -2773,15 +2763,6 @@ packages: integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==, } - acorn-import-phases@1.0.4: - resolution: - { - integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==, - } - engines: { node: '>=10.13.0' } - peerDependencies: - acorn: ^8.14.0 - acorn-jsx@5.3.2: resolution: { @@ -2999,10 +2980,10 @@ packages: } engines: { node: 18 || 20 || >=22 } - baseline-browser-mapping@2.10.43: + baseline-browser-mapping@2.11.1: resolution: { - integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==, + integrity: sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==, } engines: { node: '>=6.0.0' } hasBin: true @@ -3019,12 +3000,12 @@ packages: integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==, } - brace-expansion@5.0.7: + brace-expansion@5.0.8: resolution: { - integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==, + integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==, } - engines: { node: 18 || 20 || >=22 } + engines: { node: 20 || >=22 } braces@3.0.3: resolution: @@ -3033,18 +3014,18 @@ packages: } engines: { node: '>=8' } - browserslist@4.28.2: + browserslist@4.28.6: resolution: { - integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==, + integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==, } engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } hasBin: true - browserslist@4.28.6: + browserslist@4.28.7: resolution: { - integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==, + integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==, } engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } hasBin: true @@ -3322,10 +3303,10 @@ packages: integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==, } - electron-to-chromium@1.5.393: + electron-to-chromium@1.5.396: resolution: { - integrity: sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==, + integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==, } emoji-regex@9.2.2: @@ -3334,10 +3315,10 @@ packages: integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, } - enhanced-resolve@5.24.2: + enhanced-resolve@5.24.3: resolution: { - integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==, + integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==, } engines: { node: '>=10.13.0' } @@ -3679,10 +3660,10 @@ packages: integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, } - fast-uri@3.1.3: + fast-uri@3.1.4: resolution: { - integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==, + integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==, } fastq@1.20.1: @@ -3731,10 +3712,10 @@ packages: } engines: { node: '>=16' } - flatted@3.4.2: + flatted@3.4.3: resolution: { - integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==, + integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==, } for-each@0.3.5: @@ -4010,10 +3991,10 @@ packages: } engines: { node: '>=6' } - import-in-the-middle@3.3.1: + import-in-the-middle@3.3.2: resolution: { - integrity: sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==, + integrity: sha512-jTd2FfOgOWOdgjkHuk/1Ms8VKFXkPs15ymYBETw1sAOrO/dY3XeGVRWir9qBbw7pXr0T2eTFwfCZ+N02HmiNGA==, } engines: { node: '>=18' } @@ -4411,6 +4392,15 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: + { + integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.32.0: resolution: { @@ -4420,6 +4410,15 @@ packages: cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: + { + integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: { @@ -4429,6 +4428,15 @@ packages: cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: + { + integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==, + } + engines: { node: '>= 12.0.0' } + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.32.0: resolution: { @@ -4438,6 +4446,15 @@ packages: cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: + { + integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==, + } + engines: { node: '>= 12.0.0' } + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: { @@ -4447,6 +4464,15 @@ packages: cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: + { + integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: resolution: { @@ -4457,6 +4483,16 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: + { + integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: resolution: { @@ -4467,6 +4503,16 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: + { + integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: resolution: { @@ -4477,6 +4523,16 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: + { + integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==, + } + engines: { node: '>= 12.0.0' } + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: resolution: { @@ -4487,6 +4543,16 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: + { + integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==, + } + engines: { node: '>= 12.0.0' } + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: resolution: { @@ -4496,6 +4562,15 @@ packages: cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: + { + integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: { @@ -4505,6 +4580,15 @@ packages: cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: + { + integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==, + } + engines: { node: '>= 12.0.0' } + cpu: [x64] + os: [win32] + lightningcss@1.32.0: resolution: { @@ -4512,12 +4596,12 @@ packages: } engines: { node: '>= 12.0.0' } - loader-runner@4.3.2: + lightningcss@1.33.0: resolution: { - integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==, + integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==, } - engines: { node: '>=6.11.5' } + engines: { node: '>= 12.0.0' } locate-path@6.0.0: resolution: @@ -4532,12 +4616,6 @@ packages: integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, } - lodash.sortby@4.7.0: - resolution: - { - integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==, - } - loose-envify@1.4.0: resolution: { @@ -4558,10 +4636,10 @@ packages: integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, } - lucide-react@1.25.0: + lucide-react@1.27.0: resolution: { - integrity: sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==, + integrity: sha512-rJicGl/3Fly/E0rOH1YmPZ6e49JCnKknh1ox1vpHnkfjujAkKA6sqUZvH3MTAaXXjgexyUwgNwTJzTtYuAFYJw==, } peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4880,10 +4958,10 @@ packages: } engines: { node: '>= 0.8.0' } - own-keys@1.0.1: + own-keys@1.0.2: resolution: { - integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==, + integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==, } engines: { node: '>= 0.4' } @@ -4961,20 +5039,20 @@ packages: } engines: { node: '>=12' } - playwright-core@1.61.1: + playwright-core@1.62.0: resolution: { - integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==, + integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==, } - engines: { node: '>=18' } + engines: { node: '>=20' } hasBin: true - playwright@1.61.1: + playwright@1.62.0: resolution: { - integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==, + integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==, } - engines: { node: '>=18' } + engines: { node: '>=20' } hasBin: true possible-typed-array-names@1.1.0: @@ -4984,10 +5062,10 @@ packages: } engines: { node: '>= 0.4' } - postcss@8.5.19: + postcss@8.5.23: resolution: { - integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==, + integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==, } engines: { node: ^10 || ^12 || >=14 } @@ -4998,10 +5076,10 @@ packages: } engines: { node: '>= 0.8.0' } - prettier@3.9.5: + prettier@3.9.6: resolution: { - integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==, + integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==, } engines: { node: '>=14' } hasBin: true @@ -5242,14 +5320,6 @@ packages: } hasBin: true - semver@7.7.4: - resolution: - { - integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==, - } - engines: { node: '>=10' } - hasBin: true - semver@7.8.5: resolution: { @@ -5258,10 +5328,10 @@ packages: engines: { node: '>=10' } hasBin: true - serwist@9.5.11: + serwist@9.5.12: resolution: { - integrity: sha512-Bq6uwJFd4ET60BWI77v3VbazKHv6k7lECOiiCFwKyBu/slaCn0GHJ5L5RfsuJUKrnbD9lYUCDo6sqaKRM5M2vA==, + integrity: sha512-PwREKJrSb3ja40XJyBntPOm7okcYZJCJPd++jUVz8tzxY1VYO9dyKSm0MimZ8LtUk0pIUTN0q2cfGe7R0wQsaQ==, } peerDependencies: typescript: '>=5.0.0' @@ -5345,10 +5415,10 @@ packages: integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, } - simple-icons@16.26.0: + simple-icons@16.27.1: resolution: { - integrity: sha512-T9rNJtyOshULM8heLlvrZY346g9zOgZILQ3vxP+FWcX13RhaOLez7YM/hNCEx3b5gJckSCNDoNsjZuDncSMYSQ==, + integrity: sha512-slZF8iKxkv7Lb9SF3L1AcIl5iYLNvDrKKm8yV69cG66XvxJ12SX+bYTgxRwCY4bXSmklGXoXaKEyVmEypOCeqw==, } engines: { node: '>=0.12.18' } @@ -5381,13 +5451,12 @@ packages: } engines: { node: '>=0.10.0' } - source-map@0.8.0-beta.0: + source-map@0.8.0: resolution: { - integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==, + integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==, } - engines: { node: '>= 8' } - deprecated: The work that was done in this beta branch won't be included in future versions + engines: { node: '>= 12' } stable-hash@0.0.5: resolution: @@ -5587,12 +5656,6 @@ packages: integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, } - tr46@1.0.1: - resolution: - { - integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==, - } - ts-api-utils@2.5.0: resolution: { @@ -5634,10 +5697,10 @@ packages: } engines: { node: '>=8' } - type-fest@5.6.0: + type-fest@5.8.0: resolution: { - integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==, + integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==, } engines: { node: '>=20' } @@ -5669,10 +5732,10 @@ packages: } engines: { node: '>= 0.4' } - typescript-eslint@8.64.0: + typescript-eslint@8.65.0: resolution: { - integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==, + integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: @@ -5881,12 +5944,6 @@ packages: integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, } - webidl-conversions@4.0.2: - resolution: - { - integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==, - } - webpack-sources@3.5.1: resolution: { @@ -5894,10 +5951,10 @@ packages: } engines: { node: '>=10.13.0' } - webpack@5.108.4: + webpack@5.109.0: resolution: { - integrity: sha512-yur8LyJoeiWh47dErD+Ok7vlbmDsJ3UbbRPAoxbGJ54WpE2y5yVo5G/inUzujnYgw3tPmBRdn+G7PoxXaYC33w==, + integrity: sha512-vomrngskVVXEZF9sMZfYAd4pXZUnfaWdJGlF+BTNF+gJBCKYCQBnOeVPlrh39Ewl7nlCsirDplMy6o5g9xJHBg==, } engines: { node: '>=10.13.0' } hasBin: true @@ -5913,12 +5970,6 @@ packages: integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, } - whatwg-url@7.1.0: - resolution: - { - integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==, - } - which-boxed-primitive@1.1.1: resolution: { @@ -5992,12 +6043,6 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@4.4.1: - resolution: - { - integrity: sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==, - } - zod@4.4.3: resolution: { @@ -6007,14 +6052,14 @@ packages: snapshots: '@alloc/quick-lru@5.2.0': {} - '@apm-js-collab/code-transformer-bundler-plugins@0.6.2': + '@apm-js-collab/code-transformer-bundler-plugins@0.7.1': dependencies: - '@apm-js-collab/code-transformer': 0.18.0 + '@apm-js-collab/code-transformer': 0.18.1 es-module-lexer: 2.3.1 magic-string: 0.30.21 module-details-from-path: 1.0.4 - '@apm-js-collab/code-transformer@0.18.0': + '@apm-js-collab/code-transformer@0.18.1': dependencies: '@types/estree': 1.0.9 astring: 1.9.0 @@ -6025,7 +6070,7 @@ snapshots: '@apm-js-collab/tracing-hooks@0.13.0(supports-color@8.1.1)': dependencies: - '@apm-js-collab/code-transformer': 0.18.0 + '@apm-js-collab/code-transformer': 0.18.1 debug: 4.4.3(supports-color@8.1.1) module-details-from-path: 1.0.4 transitivePeerDependencies: @@ -6071,7 +6116,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.6 + browserslist: 4.28.7 lru-cache: 5.1.1 semver: 6.3.1 @@ -6170,7 +6215,7 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))': + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))': dependencies: eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 @@ -6434,7 +6479,7 @@ snapshots: '@opentelemetry/api@1.9.1': {} - '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.43.0 @@ -6443,89 +6488,90 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api-logs': 0.220.0 - import-in-the-middle: 3.3.1 + import-in-the-middle: 3.3.2 require-in-the-middle: 8.0.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 '@opentelemetry/semantic-conventions@1.43.0': {} '@oxc-project/types@0.139.0': {} - '@playwright/test@1.61.1': + '@playwright/test@1.62.0': dependencies: - playwright: 1.61.1 + playwright: 1.62.0 - '@radix-ui/primitive@1.1.5': {} + '@radix-ui/primitive@1.1.7': {} - '@radix-ui/react-arrow@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-collection@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-context@1.2.0(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-context@1.2.2(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-dialog@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.5 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) aria-hidden: 1.2.6 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -6534,91 +6580,91 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-direction@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-dismissable-layer@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/primitive': 1.1.5 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-dropdown-menu@2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/primitive': 1.1.5 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-menu': 2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-focus-scope@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-id@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-label@2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-label@2.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-menu@2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.5 - '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-roving-focus': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) aria-hidden: 1.2.6 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -6627,21 +6673,21 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-popover@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.5 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) aria-hidden: 1.2.6 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -6650,156 +6696,150 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-popper@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@floating-ui/react-dom': 2.1.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-arrow': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/rect': 1.1.2 + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/rect': 1.1.3 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-portal@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-presence@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-primitive@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-roving-focus@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.5 - '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-separator@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-separator@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-slot@1.3.0(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-slot@1.3.3(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-switch@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-switch@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/primitive': 1.1.5 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-use-is-hydrated@0.1.1(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-use-previous@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: + '@radix-ui/rect': 1.1.3 react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-use-rect@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-size@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/rect': 1.1.2 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/rect@1.1.2': {} + '@radix-ui/rect@1.1.3': {} '@rolldown/binding-android-arm64@1.1.5': optional: true @@ -6951,19 +6991,19 @@ snapshots: '@sentry/babel-plugin-component-annotate@5.3.0': {} - '@sentry/browser-utils@10.66.0': + '@sentry/browser-utils@10.68.0': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/core': 10.66.0 + '@sentry/core': 10.68.0 - '@sentry/browser@10.66.0': + '@sentry/browser@10.68.0': dependencies: - '@sentry/browser-utils': 10.66.0 + '@sentry/browser-utils': 10.68.0 '@sentry/conventions': 0.16.0 - '@sentry/core': 10.66.0 - '@sentry/feedback': 10.66.0 - '@sentry/replay': 10.66.0 - '@sentry/replay-canvas': 10.66.0 + '@sentry/core': 10.68.0 + '@sentry/feedback': 10.68.0 + '@sentry/replay': 10.68.0 + '@sentry/replay-canvas': 10.68.0 '@sentry/bundler-plugin-core@5.3.0(supports-color@8.1.1)': dependencies: @@ -6978,18 +7018,18 @@ snapshots: - encoding - supports-color - '@sentry/bundler-plugins@10.66.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19))': + '@sentry/bundler-plugins@10.68.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) '@sentry/cli': 2.58.6(supports-color@8.1.1) - '@sentry/core': 10.66.0 + '@sentry/core': 10.68.0 dotenv: 17.4.2 find-up: 5.0.0 glob: 13.0.6 magic-string: 0.30.21 optionalDependencies: rollup: 4.62.2 - webpack: 5.108.4(lightningcss@1.32.0)(postcss@8.5.19) + webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) transitivePeerDependencies: - encoding - supports-color @@ -7040,29 +7080,29 @@ snapshots: '@sentry/conventions@0.16.0': {} - '@sentry/core@10.66.0': + '@sentry/core@10.68.0': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/feedback@10.66.0': + '@sentry/feedback@10.68.0': dependencies: - '@sentry/core': 10.66.0 + '@sentry/core': 10.68.0 - '@sentry/nextjs@10.66.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@8.1.1)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19))': + '@sentry/nextjs@10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': dependencies: '@opentelemetry/api': 1.9.1 '@rollup/plugin-commonjs': 28.0.1(rollup@4.62.2) - '@sentry/browser-utils': 10.66.0 + '@sentry/browser-utils': 10.68.0 '@sentry/bundler-plugin-core': 5.3.0(supports-color@8.1.1) '@sentry/conventions': 0.16.0 - '@sentry/core': 10.66.0 - '@sentry/node': 10.66.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(supports-color@8.1.1) - '@sentry/opentelemetry': 10.66.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)) - '@sentry/react': 10.66.0(react@19.2.7) - '@sentry/server-utils': 10.66.0(supports-color@8.1.1) - '@sentry/vercel-edge': 10.66.0 - '@sentry/webpack-plugin': 5.4.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19)) - next: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@sentry/core': 10.68.0 + '@sentry/node': 10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@8.1.1) + '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/react': 10.68.0(react@19.2.7) + '@sentry/server-utils': 10.68.0(supports-color@8.1.1) + '@sentry/vercel-edge': 10.68.0 + '@sentry/webpack-plugin': 5.4.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) + next: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) rollup: 4.62.2 stacktrace-parser: 0.1.11 transitivePeerDependencies: @@ -7074,136 +7114,136 @@ snapshots: - supports-color - webpack - '@sentry/node-core@10.66.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))': + '@sentry/node-core@10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/core': 10.66.0 - '@sentry/opentelemetry': 10.66.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)) - import-in-the-middle: 3.3.1 + '@sentry/core': 10.68.0 + '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + import-in-the-middle: 3.3.2 optionalDependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) - '@sentry/node@10.66.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(supports-color@8.1.1)': + '@sentry/node@10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) '@sentry/conventions': 0.16.0 - '@sentry/core': 10.66.0 - '@sentry/node-core': 10.66.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)) - '@sentry/opentelemetry': 10.66.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)) - '@sentry/server-utils': 10.66.0(supports-color@8.1.1) - import-in-the-middle: 3.3.1 + '@sentry/core': 10.68.0 + '@sentry/node-core': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/server-utils': 10.68.0(supports-color@8.1.1) + import-in-the-middle: 3.3.2 transitivePeerDependencies: - '@opentelemetry/core' - '@opentelemetry/exporter-trace-otlp-http' - supports-color - '@sentry/opentelemetry@10.66.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))': + '@sentry/opentelemetry@10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) '@sentry/conventions': 0.16.0 - '@sentry/core': 10.66.0 + '@sentry/core': 10.68.0 - '@sentry/react@10.66.0(react@19.2.7)': + '@sentry/react@10.68.0(react@19.2.7)': dependencies: - '@sentry/browser': 10.66.0 + '@sentry/browser': 10.68.0 '@sentry/conventions': 0.16.0 - '@sentry/core': 10.66.0 + '@sentry/core': 10.68.0 react: 19.2.7 - '@sentry/replay-canvas@10.66.0': + '@sentry/replay-canvas@10.68.0': dependencies: - '@sentry/core': 10.66.0 - '@sentry/replay': 10.66.0 + '@sentry/core': 10.68.0 + '@sentry/replay': 10.68.0 - '@sentry/replay@10.66.0': + '@sentry/replay@10.68.0': dependencies: - '@sentry/browser-utils': 10.66.0 - '@sentry/core': 10.66.0 + '@sentry/browser-utils': 10.68.0 + '@sentry/core': 10.68.0 - '@sentry/server-utils@10.66.0(supports-color@8.1.1)': + '@sentry/server-utils@10.68.0(supports-color@8.1.1)': dependencies: - '@apm-js-collab/code-transformer': 0.18.0 - '@apm-js-collab/code-transformer-bundler-plugins': 0.6.2 + '@apm-js-collab/code-transformer-bundler-plugins': 0.7.1 '@apm-js-collab/tracing-hooks': 0.13.0(supports-color@8.1.1) '@sentry/conventions': 0.16.0 - '@sentry/core': 10.66.0 + '@sentry/core': 10.68.0 + meriyah: 6.1.4 transitivePeerDependencies: - supports-color - '@sentry/vercel-edge@10.66.0': + '@sentry/vercel-edge@10.68.0': dependencies: '@opentelemetry/api': 1.9.1 - '@sentry/core': 10.66.0 + '@sentry/core': 10.68.0 - '@sentry/webpack-plugin@5.4.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19))': + '@sentry/webpack-plugin@5.4.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': dependencies: - '@sentry/bundler-plugins': 10.66.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19)) - webpack: 5.108.4(lightningcss@1.32.0)(postcss@8.5.19) + '@sentry/bundler-plugins': 10.68.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) + webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) transitivePeerDependencies: - encoding - rollup - supports-color - '@serwist/build@9.5.11(browserslist@4.28.2)(typescript@6.0.3)': + '@serwist/build@9.5.12(browserslist@4.28.6)(typescript@6.0.3)': dependencies: - '@serwist/utils': 9.5.11(browserslist@4.28.2) + '@serwist/utils': 9.5.12(browserslist@4.28.6) common-tags: 1.8.2 glob: 13.0.6 pretty-bytes: 6.1.1 - source-map: 0.8.0-beta.0 - type-fest: 5.6.0 - zod: 4.4.1 + source-map: 0.8.0 + type-fest: 5.8.0 + zod: 4.4.3 optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: - browserslist - '@serwist/next@9.5.11(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19))': + '@serwist/next@9.5.12(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': dependencies: - '@serwist/build': 9.5.11(browserslist@4.28.2)(typescript@6.0.3) - '@serwist/utils': 9.5.11(browserslist@4.28.2) - '@serwist/webpack-plugin': 9.5.11(browserslist@4.28.2)(typescript@6.0.3)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19)) - '@serwist/window': 9.5.11(browserslist@4.28.2)(typescript@6.0.3) - browserslist: 4.28.2 + '@serwist/build': 9.5.12(browserslist@4.28.6)(typescript@6.0.3) + '@serwist/utils': 9.5.12(browserslist@4.28.6) + '@serwist/webpack-plugin': 9.5.12(browserslist@4.28.6)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) + '@serwist/window': 9.5.12(browserslist@4.28.6)(typescript@6.0.3) + browserslist: 4.28.6 glob: 13.0.6 kolorist: 1.8.0 - next: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 - semver: 7.7.4 - serwist: 9.5.11(browserslist@4.28.2)(typescript@6.0.3) - zod: 4.4.1 + semver: 7.8.5 + serwist: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) + zod: 4.4.3 optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: - webpack - '@serwist/utils@9.5.11(browserslist@4.28.2)': + '@serwist/utils@9.5.12(browserslist@4.28.6)': optionalDependencies: - browserslist: 4.28.2 + browserslist: 4.28.6 - '@serwist/webpack-plugin@9.5.11(browserslist@4.28.2)(typescript@6.0.3)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19))': + '@serwist/webpack-plugin@9.5.12(browserslist@4.28.6)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': dependencies: - '@serwist/build': 9.5.11(browserslist@4.28.2)(typescript@6.0.3) - '@serwist/utils': 9.5.11(browserslist@4.28.2) + '@serwist/build': 9.5.12(browserslist@4.28.6)(typescript@6.0.3) + '@serwist/utils': 9.5.12(browserslist@4.28.6) pretty-bytes: 6.1.1 - zod: 4.4.1 + zod: 4.4.3 optionalDependencies: typescript: 6.0.3 - webpack: 5.108.4(lightningcss@1.32.0)(postcss@8.5.19) + webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) transitivePeerDependencies: - browserslist - '@serwist/window@9.5.11(browserslist@4.28.2)(typescript@6.0.3)': + '@serwist/window@9.5.12(browserslist@4.28.6)(typescript@6.0.3)': dependencies: '@types/trusted-types': 2.0.7 - serwist: 9.5.11(browserslist@4.28.2)(typescript@6.0.3) + serwist: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -7211,42 +7251,42 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@supabase/auth-js@2.110.7': + '@supabase/auth-js@2.110.8': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.110.7': + '@supabase/functions-js@2.110.8': dependencies: tslib: 2.8.1 '@supabase/phoenix@0.4.5': {} - '@supabase/postgrest-js@2.110.7': + '@supabase/postgrest-js@2.110.8': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.110.7': + '@supabase/realtime-js@2.110.8': dependencies: '@supabase/phoenix': 0.4.5 tslib: 2.8.1 - '@supabase/ssr@0.10.3(@supabase/supabase-js@2.110.7)': + '@supabase/ssr@0.10.3(@supabase/supabase-js@2.110.8)': dependencies: - '@supabase/supabase-js': 2.110.7 + '@supabase/supabase-js': 2.110.8 cookie: 1.1.1 - '@supabase/storage-js@2.110.7': + '@supabase/storage-js@2.110.8': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 - '@supabase/supabase-js@2.110.7': + '@supabase/supabase-js@2.110.8': dependencies: - '@supabase/auth-js': 2.110.7 - '@supabase/functions-js': 2.110.7 - '@supabase/postgrest-js': 2.110.7 - '@supabase/realtime-js': 2.110.7 - '@supabase/storage-js': 2.110.7 + '@supabase/auth-js': 2.110.8 + '@supabase/functions-js': 2.110.8 + '@supabase/postgrest-js': 2.110.8 + '@supabase/realtime-js': 2.110.8 + '@supabase/storage-js': 2.110.8 '@swc/helpers@0.5.15': dependencies: @@ -7255,7 +7295,7 @@ snapshots: '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.24.2 + enhanced-resolve: 5.24.3 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 @@ -7318,7 +7358,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 - postcss: 8.5.19 + postcss: 8.5.23 tailwindcss: 4.3.3 '@tybys/wasm-util@0.10.3': @@ -7357,14 +7397,14 @@ snapshots: dependencies: '@types/node': 25.9.5 - '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/type-utils': 8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.64.0 + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/type-utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.65.0 eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) ignore: 7.0.6 natural-compare: 1.4.0 @@ -7373,41 +7413,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.64.0 + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.65.0 debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.64.0(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/project-service@8.65.0(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3) - '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 debug: 4.4.3(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.64.0': + '@typescript-eslint/scope-manager@8.65.0': dependencies: - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/visitor-keys': 8.64.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 - '@typescript-eslint/tsconfig-utils@8.64.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) ts-api-utils: 2.5.0(typescript@6.0.3) @@ -7415,14 +7455,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.64.0': {} + '@typescript-eslint/types@8.65.0': {} - '@typescript-eslint/typescript-estree@8.64.0(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.65.0(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.64.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3) - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/visitor-keys': 8.64.0 + '@typescript-eslint/project-service': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.5 semver: 7.8.5 @@ -7432,20 +7472,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/utils@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@8.1.1)(typescript@6.0.3) + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.64.0': + '@typescript-eslint/visitor-keys@8.65.0': dependencies: - '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 '@unrs/resolver-binding-android-arm-eabi@1.12.2': @@ -7653,10 +7693,6 @@ snapshots: '@xtuc/long@4.2.2': {} - acorn-import-phases@1.0.4(acorn@8.17.0): - dependencies: - acorn: 8.17.0 - acorn-jsx@5.3.2(acorn@8.17.0): dependencies: acorn: 8.17.0 @@ -7690,7 +7726,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.3 + fast-uri: 3.1.4 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -7806,7 +7842,7 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.10.43: {} + baseline-browser-mapping@2.11.1: {} bn.js@4.12.5: {} @@ -7815,7 +7851,7 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.7: + brace-expansion@5.0.8: dependencies: balanced-match: 4.0.4 @@ -7823,21 +7859,21 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.2: + browserslist@4.28.6: dependencies: - baseline-browser-mapping: 2.10.43 + baseline-browser-mapping: 2.11.1 caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.393 + electron-to-chromium: 1.5.396 node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + update-browserslist-db: 1.2.3(browserslist@4.28.6) - browserslist@4.28.6: + browserslist@4.28.7: dependencies: - baseline-browser-mapping: 2.10.43 + baseline-browser-mapping: 2.11.1 caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.393 + electron-to-chromium: 1.5.396 node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.6) + update-browserslist-db: 1.2.3(browserslist@4.28.7) buffer-equal-constant-time@1.0.1: {} @@ -7977,11 +8013,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 - electron-to-chromium@1.5.393: {} + electron-to-chromium@1.5.396: {} emoji-regex@9.2.2: {} - enhanced-resolve@5.24.2: + enhanced-resolve@5.24.3: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -8033,7 +8069,7 @@ snapshots: object-inspect: 1.13.4 object-keys: 1.1.1 object.assign: 4.1.7 - own-keys: 1.0.1 + own-keys: 1.0.2 regexp.prototype.flags: 1.5.4 safe-array-concat: 1.1.4 safe-push-apply: 1.0.0 @@ -8103,18 +8139,18 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.2.10(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): + eslint-config-next@16.2.10(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): dependencies: '@next/eslint-plugin-next': 16.2.10 eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) globals: 16.4.0 - typescript-eslint: 8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + typescript-eslint: 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -8146,22 +8182,22 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -8172,7 +8208,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -8184,7 +8220,7 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -8260,7 +8296,7 @@ snapshots: eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2(supports-color@8.1.1) '@eslint/config-helpers': 0.4.2 @@ -8345,7 +8381,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.3: {} + fast-uri@3.1.4: {} fastq@1.20.1: dependencies: @@ -8370,10 +8406,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.3 keyv: 4.5.4 - flatted@3.4.2: {} + flatted@3.4.3: {} for-each@0.3.5: dependencies: @@ -8523,7 +8559,7 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-in-the-middle@3.3.1: + import-in-the-middle@3.3.2: dependencies: cjs-module-lexer: 2.2.0 es-module-lexer: 2.3.1 @@ -8755,36 +8791,69 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -8801,7 +8870,21 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - loader-runner@4.3.2: {} + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 locate-path@6.0.0: dependencies: @@ -8809,8 +8892,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash.sortby@4.7.0: {} - loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -8821,7 +8902,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@1.25.0(react@19.2.7): + lucide-react@1.27.0(react@19.2.7): dependencies: react: 19.2.7 @@ -8858,7 +8939,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 5.0.8 minimatch@3.1.5: dependencies: @@ -8866,16 +8947,16 @@ snapshots: minimist@1.2.8: {} - minimizer-webpack-plugin@5.6.1(lightningcss@1.32.0)(postcss@8.5.19)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19)): + minimizer-webpack-plugin@5.6.1(lightningcss@1.33.0)(postcss@8.5.23)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.49.0 - webpack: 5.108.4(lightningcss@1.32.0)(postcss@8.5.19) + webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) optionalDependencies: - lightningcss: 1.32.0 - postcss: 8.5.19 + lightningcss: 1.33.0 + postcss: 8.5.23 minipass@7.1.3: {} @@ -8891,13 +8972,13 @@ snapshots: neo-async@2.6.2: {} - next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@next/env': 16.2.10 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.43 + baseline-browser-mapping: 2.11.1 caniuse-lite: 1.0.30001806 - postcss: 8.5.19 + postcss: 8.5.23 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.7) @@ -8911,15 +8992,15 @@ snapshots: '@next/swc-win32-arm64-msvc': 16.2.10 '@next/swc-win32-x64-msvc': 16.2.10 '@opentelemetry/api': 1.9.1 - '@playwright/test': 1.61.1 + '@playwright/test': 1.62.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextjs-toploader@3.9.17(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + nextjs-toploader@3.9.17(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - next: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) nprogress: 0.2.0 prop-types: 15.8.1 react: 19.2.7 @@ -8993,8 +9074,9 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - own-keys@1.0.1: + own-keys@1.0.2: dependencies: + call-bound: 1.0.4 get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 @@ -9030,17 +9112,17 @@ snapshots: picomatch@4.0.5: {} - playwright-core@1.61.1: {} + playwright-core@1.62.0: {} - playwright@1.61.1: + playwright@1.62.0: dependencies: - playwright-core: 1.61.1 + playwright-core: 1.62.0 optionalDependencies: fsevents: 2.3.2 possible-typed-array-names@1.1.0: {} - postcss@8.5.19: + postcss@8.5.23: dependencies: nanoid: 3.3.16 picocolors: 1.1.1 @@ -9048,7 +9130,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier@3.9.5: {} + prettier@3.9.6: {} pretty-bytes@6.1.1: {} @@ -9238,13 +9320,11 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} - semver@7.8.5: {} - serwist@9.5.11(browserslist@4.28.2)(typescript@6.0.3): + serwist@9.5.12(browserslist@4.28.6)(typescript@6.0.3): dependencies: - '@serwist/utils': 9.5.11(browserslist@4.28.2) + '@serwist/utils': 9.5.12(browserslist@4.28.6) idb: 8.0.3 optionalDependencies: typescript: 6.0.3 @@ -9341,7 +9421,7 @@ snapshots: siginfo@2.0.0: {} - simple-icons@16.26.0: {} + simple-icons@16.27.1: {} sonner@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: @@ -9357,9 +9437,7 @@ snapshots: source-map@0.6.1: {} - source-map@0.8.0-beta.0: - dependencies: - whatwg-url: 7.1.0 + source-map@0.8.0: {} stable-hash@0.0.5: {} @@ -9480,10 +9558,6 @@ snapshots: tr46@0.0.3: {} - tr46@1.0.1: - dependencies: - punycode: 2.3.1 - ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 @@ -9505,7 +9579,7 @@ snapshots: type-fest@0.7.1: {} - type-fest@5.6.0: + type-fest@5.8.0: dependencies: tagged-tag: 1.0.0 @@ -9542,12 +9616,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): + typescript-eslint@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: @@ -9591,15 +9665,15 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - update-browserslist-db@1.2.3(browserslist@4.28.2): + update-browserslist-db@1.2.3(browserslist@4.28.6): dependencies: - browserslist: 4.28.2 + browserslist: 4.28.6 escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.2.3(browserslist@4.28.6): + update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: - browserslist: 4.28.6 + browserslist: 4.28.7 escalade: 3.2.0 picocolors: 1.1.1 @@ -9629,9 +9703,9 @@ snapshots: vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0): dependencies: - lightningcss: 1.32.0 + lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.19 + postcss: 8.5.23 rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: @@ -9685,11 +9759,9 @@ snapshots: webidl-conversions@3.0.1: {} - webidl-conversions@4.0.2: {} - webpack-sources@3.5.1: {} - webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19): + webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23): dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 @@ -9697,17 +9769,15 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.17.0 - acorn-import-phases: 1.0.4(acorn@8.17.0) - browserslist: 4.28.6 + browserslist: 4.28.7 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.2 + enhanced-resolve: 5.24.3 es-module-lexer: 2.3.1 eslint-scope: 5.1.1 events: 3.3.0 graceful-fs: 4.2.11 - loader-runner: 4.3.2 mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(lightningcss@1.32.0)(postcss@8.5.19)(webpack@5.108.4(lightningcss@1.32.0)(postcss@8.5.19)) + minimizer-webpack-plugin: 5.6.1(lightningcss@1.33.0)(postcss@8.5.23)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 @@ -9732,12 +9802,6 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 - whatwg-url@7.1.0: - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -9798,6 +9862,4 @@ snapshots: dependencies: zod: 4.4.3 - zod@4.4.1: {} - zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e5538a14..e9b6cdab 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,5 +9,10 @@ allowBuilds: ignoredBuiltDependencies: - sharp - unrs-resolver +minimumReleaseAgeExclude: + - lucide-react@1.27.0 + - simple-icons@16.27.1 overrides: postcss: '>=8.5.10' + simple-icons: '>=16.27.1' + lucide-react: '>=1.27.0' From 2c4e514361bd3554e5b55e19e123c2c78b605403 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 14:19:05 +0200 Subject: [PATCH 23/98] =?UTF-8?q?chore(i18n):=20r=C3=A9tablit=20le=20vouvo?= =?UTF-8?q?iement=20sur=20/terms=20et=20/privacy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le tutoiement généralisé avait emporté les pages légales, où le vouvoiement reste la convention. Seul le bloc legal français revient en arrière : le dashboard, l'explorer et le reste de pages.ts gardent le tutoiement. --- lib/i18n/messages/pages.ts | 48 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/i18n/messages/pages.ts b/lib/i18n/messages/pages.ts index e5368287..7b8e3f33 100644 --- a/lib/i18n/messages/pages.ts +++ b/lib/i18n/messages/pages.ts @@ -3,63 +3,63 @@ const fr = { privacy: { title: 'Politique de confidentialité', updated: 'Dernière mise à jour : 12 juillet 2026', - intro: 'ReelMark respecte ta vie privée. Cette déclaration explique quelles données personnelles nous traitons, pourquoi, et quels sont tes droits, conformément au Règlement général sur la protection des données (RGPD, UE 2016/679) et à la loi belge du 30 juillet 2018.', + intro: 'ReelMark respecte votre vie privée. Cette déclaration explique quelles données personnelles nous traitons, pourquoi, et quels sont vos droits, conformément au Règlement général sur la protection des données (RGPD, UE 2016/679) et à la loi belge du 30 juillet 2018.', sections: [ { title: '1. Responsable du traitement', body: [ - "ReelMark est édité par Silexio (Belgique). Pour toute question relative à tes données personnelles, écris-nous à l'adresse : contact@silexio.be.", + "ReelMark est édité par Silexio (Belgique). Pour toute question relative à vos données personnelles, contactez-nous à l'adresse : contact@silexio.be.", ], }, { title: '2. Données que nous collectons', body: [ - "Données de compte : adresse e-mail, mot de passe (haché, jamais stocké en clair), nom d'utilisateur, nom affiché, avatar, langue et région préférées. Si tu te connectes avec Google, nous recevons ton e-mail et les informations de base de ton profil Google.", - "Contenu que tu crées : ta watchlist, les épisodes marqués comme vus, tes notes et critiques, tes playlists et tes relations d'amis sur la plateforme.", - "Données techniques : cookies strictement nécessaires (session de connexion, préférence de langue, répartition de charge), préférences stockées localement dans ton navigateur (thème, affichage) et journaux d'erreurs techniques.", + "Données de compte : adresse e-mail, mot de passe (haché, jamais stocké en clair), nom d'utilisateur, nom affiché, avatar, langue et région préférées. Si vous vous connectez avec Google, nous recevons votre e-mail et les informations de base de votre profil Google.", + "Contenu que vous créez : votre watchlist, les épisodes marqués comme vus, vos notes et critiques, vos playlists et vos relations d'amis sur la plateforme.", + "Données techniques : cookies strictement nécessaires (session de connexion, préférence de langue, répartition de charge), préférences stockées localement dans votre navigateur (thème, affichage) et journaux d'erreurs techniques.", 'Nous ne collectons aucune donnée publicitaire et ne réalisons aucun profilage commercial.', ], }, { title: '3. Finalités et bases légales', body: [ - 'Fournir le service (création de compte, sauvegarde de ta bibliothèque, fonctionnalités sociales) : exécution du contrat qui nous lie lorsque tu crées un compte.', + 'Fournir le service (création de compte, sauvegarde de votre bibliothèque, fonctionnalités sociales) : exécution du contrat qui nous lie lorsque vous créez un compte.', "Assurer la sécurité de la plateforme (prévention des abus, journaux d'erreurs) : intérêt légitime.", - 'Mémoriser tes préférences (langue, thème) : consentement, exprimé par ton usage de ces réglages.', + 'Mémoriser vos préférences (langue, thème) : consentement, exprimé par votre usage de ces réglages.', ], }, { title: '4. Destinataires et sous-traitants', body: [ - 'Tes données sont traitées par nos sous-traitants techniques : Supabase (base de données et authentification), Infomaniak (hébergement en Suisse, pays reconnu adéquat par la Commission européenne) et Cloudflare (sécurité et diffusion du contenu, encadré par des clauses contractuelles types).', + 'Vos données sont traitées par nos sous-traitants techniques : Supabase (base de données et authentification), Infomaniak (hébergement en Suisse, pays reconnu adéquat par la Commission européenne) et Cloudflare (sécurité et diffusion du contenu, encadré par des clauses contractuelles types).', 'Les informations sur les films et séries proviennent de TMDB ; aucune donnée personnelle ne lui est transmise.', - 'Nous ne vendons ni ne louons tes données. Elles ne sont communiquées à aucun autre tiers, sauf obligation légale.', + 'Nous ne vendons ni ne louons vos données. Elles ne sont communiquées à aucun autre tiers, sauf obligation légale.', ], }, { title: '5. Durée de conservation', body: [ - 'Tes données sont conservées tant que ton compte est actif. La suppression de ton compte (Réglages → Zone de danger) entraîne la suppression de tes données personnelles. Les journaux techniques sont conservés pour une durée limitée.', + 'Vos données sont conservées tant que votre compte est actif. La suppression de votre compte (Réglages → Zone de danger) entraîne la suppression de vos données personnelles. Les journaux techniques sont conservés pour une durée limitée.', ], }, { - title: '6. Tes droits', + title: '6. Vos droits', body: [ - "Tu disposes des droits d'accès, de rectification, d'effacement, de portabilité, de limitation et d'opposition, ainsi que du droit de retirer ton consentement à tout moment.", - "Tu peux exporter tes données à tout moment (Réglages → Données), modifier ton profil ou supprimer ton compte directement dans l'application, ou nous écrire à contact@silexio.be.", - "Tu peux introduire une réclamation auprès de l'Autorité de protection des données belge (APD) : www.autoriteprotectiondonnees.be.", + "Vous disposez des droits d'accès, de rectification, d'effacement, de portabilité, de limitation et d'opposition, ainsi que du droit de retirer votre consentement à tout moment.", + "Vous pouvez exporter vos données à tout moment (Réglages → Données), modifier votre profil ou supprimer votre compte directement dans l'application, ou nous écrire à contact@silexio.be.", + "Vous pouvez introduire une réclamation auprès de l'Autorité de protection des données belge (APD) : www.autoriteprotectiondonnees.be.", ], }, { title: '7. Sécurité', body: [ - "Tes données sont chiffrées en transit (TLS), les mots de passe sont hachés, et l'accès aux données est restreint par des règles de sécurité au niveau de la base de données.", + "Vos données sont chiffrées en transit (TLS), les mots de passe sont hachés, et l'accès aux données est restreint par des règles de sécurité au niveau de la base de données.", ], }, { title: '8. Cookies et stockage local', body: [ - "Nous n'utilisons que des cookies strictement nécessaires au fonctionnement du service : session de connexion, préférence de langue et répartition de charge. Tes préférences d'affichage (thème) sont stockées localement dans ton navigateur. Aucun cookie publicitaire ni traceur tiers n'est utilisé.", + "Nous n'utilisons que des cookies strictement nécessaires au fonctionnement du service : session de connexion, préférence de langue et répartition de charge. Vos préférences d'affichage (thème) sont stockées localement dans votre navigateur. Aucun cookie publicitaire ni traceur tiers n'est utilisé.", ], }, { @@ -79,18 +79,18 @@ const fr = { terms: { title: "Conditions d'utilisation", updated: 'Dernière mise à jour : 12 juillet 2026', - intro: "Les présentes conditions régissent l'utilisation de ReelMark, une plateforme personnelle de suivi de films et de séries. En créant un compte ou en utilisant le service, tu les acceptes.", + intro: "Les présentes conditions régissent l'utilisation de ReelMark, une plateforme personnelle de suivi de films et de séries. En créant un compte ou en utilisant le service, vous les acceptez.", sections: [ { title: '1. Le service', body: [ - 'ReelMark te permet de suivre les films et séries que tu as vus ou souhaites voir, de noter et critiquer des œuvres, de créer des playlists et de partager ton activité avec des amis.', + 'ReelMark vous permet de suivre les films et séries que vous avez vus ou souhaitez voir, de noter et critiquer des œuvres, de créer des playlists et de partager votre activité avec des amis.', ], }, { - title: '2. Ton compte', + title: '2. Votre compte', body: [ - "Tu dois avoir au moins 13 ans pour créer un compte. Tu es responsable de la confidentialité de tes identifiants et de l'activité réalisée depuis ton compte. Les informations fournies doivent être exactes.", + "Vous devez avoir au moins 13 ans pour créer un compte. Vous êtes responsable de la confidentialité de vos identifiants et de l'activité réalisée depuis votre compte. Les informations fournies doivent être exactes.", ], }, { @@ -100,9 +100,9 @@ const fr = { ], }, { - title: '4. Ton contenu', + title: '4. Votre contenu', body: [ - 'Tu restes propriétaire des critiques, notes et playlists que tu crées. Tu nous accordes une licence limitée pour les afficher dans le service, selon les réglages de visibilité que tu choisis. Nous pouvons retirer tout contenu contraire à ces conditions.', + 'Vous restez propriétaire des critiques, notes et playlists que vous créez. Vous nous accordez une licence limitée pour les afficher dans le service, selon les réglages de visibilité que vous choisissez. Nous pouvons retirer tout contenu contraire à ces conditions.', ], }, { @@ -120,7 +120,7 @@ const fr = { { title: '7. Résiliation', body: [ - 'Tu peux supprimer ton compte à tout moment depuis les réglages. Nous pouvons suspendre ou supprimer un compte en cas de violation de ces conditions.', + 'Vous pouvez supprimer votre compte à tout moment depuis les réglages. Nous pouvons suspendre ou supprimer un compte en cas de violation de ces conditions.', ], }, { @@ -132,7 +132,7 @@ const fr = { { title: '9. Droit applicable', body: [ - 'Les présentes conditions sont régies par le droit belge. Tout litige relève des tribunaux belges compétents, sans préjudice des droits impératifs dont tu bénéficies en tant que consommateur.', + 'Les présentes conditions sont régies par le droit belge. Tout litige relève des tribunaux belges compétents, sans préjudice des droits impératifs dont vous bénéficiez en tant que consommateur.', ], }, { From cdb111d56c07641936c17786cd941f1fbe2307dc Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 14:36:44 +0200 Subject: [PATCH 24/98] =?UTF-8?q?fix(ux):=20contr=C3=B4les=20de=20card=20u?= =?UTF-8?q?tilisables=20au=20doigt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux défauts distincts rendaient la watchlist inaccessible sur mobile : - le dégradé décoratif de MediaCard couvre toute la carte en `inset-0` sans `pointer-events-none`, donc il absorbait les taps destinés aux boutons placés au-dessus de lui — y compris quand ceux-ci étaient parfaitement visibles. - les contrôles révélés au survol restaient inertes sur tactile, où `hover:` ne se déclenche jamais : sans être ni visibles ni atteignables, le bouton watchlist d'une card et l'icône de saison non commencée n'avaient aucun équivalent au doigt. `pointer-coarse:` (Tailwind 4.1+) les affiche et les active sur tactile, ce qui lève l'alternative « invisible mais tapable » / « inatteignable » sans toucher au comportement desktop. Vérifié sur dashboard, library, /movie/[id] et /tv/[id] en viewport 390×844 : plus aucun bouton dont le tap n'atteint pas sa cible. touch-ghost-controls et media-card-tap restent verts. --- components/media/card/MediaCard.tsx | 14 ++++++++------ components/media/tv/SeasonCard.tsx | 9 ++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index 925cae16..5c9e6db5 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -115,8 +115,9 @@ export function MediaCard({ {!compact && (
)} @@ -186,10 +187,11 @@ export function MediaCard({ className={cn( 'absolute inset-x-0 bottom-0 flex flex-col gap-3 p-4 transition-all duration-(--duration-base) z-10', 'translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100', - // Sans hover, l'overlay ne s'affiche jamais (Tailwind v4 borne `hover:` à - // @media (hover:hover)) : un tap au bas du poster tombait sur le WatchButton - // invisible et ajoutait le titre à la watchlist au lieu de naviguer. - 'pointer-events-none group-hover:pointer-events-auto' + 'pointer-events-none group-hover:pointer-events-auto', + // `hover:` ne se déclenche jamais sur tactile (Tailwind v4 le borne à + // @media (hover:hover)) : l'overlay y reste donc affiché et actif, sinon + // le bouton watchlist serait soit invisible et tapable, soit inatteignable. + 'pointer-coarse:translate-y-0 pointer-coarse:opacity-100 pointer-coarse:pointer-events-auto' )} >
diff --git a/components/media/tv/SeasonCard.tsx b/components/media/tv/SeasonCard.tsx index e304c013..3c96901d 100644 --- a/components/media/tv/SeasonCard.tsx +++ b/components/media/tv/SeasonCard.tsx @@ -127,9 +127,12 @@ export function SeasonCard({ 'absolute top-2 right-2 z-10 transition-all duration-(--duration-base)', watched > 0 ? 'opacity-100' - : // Masqué sans hover : sans `pointer-events-none`, un tap sur ce coin - // marquait toute la saison comme vue sur mobile, où l'icône est invisible. - 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto' + : cn( + 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto', + // Sur tactile `hover:` ne se déclenche pas : l'icône y reste visible + // et active, sinon elle serait tapable à l'aveugle ou inatteignable. + 'pointer-coarse:opacity-100 pointer-coarse:pointer-events-auto' + ) )} > Date: Sun, 26 Jul 2026 14:50:31 +0200 Subject: [PATCH 25/98] =?UTF-8?q?revert(ux):=20les=20contr=C3=B4les=20au?= =?UTF-8?q?=20survol=20restent=20masqu=C3=A9s=20sur=20tactile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Annule l'affichage permanent introduit par cdb111d : sur une card, l'overlay de survol doit rester invisible et inerte au doigt, pas occuper l'affiche. Seul `pointer-events-none` est conservé sur le dégradé décoratif de MediaCard : il couvre la carte en `inset-0` et n'a aucune raison d'être une cible de pointeur, même si ce n'était pas lui qui bloquait les boutons. --- components/media/card/MediaCard.tsx | 12 +++++------- components/media/tv/SeasonCard.tsx | 9 +++------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index 5c9e6db5..335851d4 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -116,8 +116,7 @@ export function MediaCard({
)} @@ -187,11 +186,10 @@ export function MediaCard({ className={cn( 'absolute inset-x-0 bottom-0 flex flex-col gap-3 p-4 transition-all duration-(--duration-base) z-10', 'translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100', - 'pointer-events-none group-hover:pointer-events-auto', - // `hover:` ne se déclenche jamais sur tactile (Tailwind v4 le borne à - // @media (hover:hover)) : l'overlay y reste donc affiché et actif, sinon - // le bouton watchlist serait soit invisible et tapable, soit inatteignable. - 'pointer-coarse:translate-y-0 pointer-coarse:opacity-100 pointer-coarse:pointer-events-auto' + // Sans hover, l'overlay ne s'affiche jamais (Tailwind v4 borne `hover:` à + // @media (hover:hover)) : un tap au bas du poster tombait sur le WatchButton + // invisible et ajoutait le titre à la watchlist au lieu de naviguer. + 'pointer-events-none group-hover:pointer-events-auto' )} >
diff --git a/components/media/tv/SeasonCard.tsx b/components/media/tv/SeasonCard.tsx index 3c96901d..e304c013 100644 --- a/components/media/tv/SeasonCard.tsx +++ b/components/media/tv/SeasonCard.tsx @@ -127,12 +127,9 @@ export function SeasonCard({ 'absolute top-2 right-2 z-10 transition-all duration-(--duration-base)', watched > 0 ? 'opacity-100' - : cn( - 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto', - // Sur tactile `hover:` ne se déclenche pas : l'icône y reste visible - // et active, sinon elle serait tapable à l'aveugle ou inatteignable. - 'pointer-coarse:opacity-100 pointer-coarse:pointer-events-auto' - ) + : // Masqué sans hover : sans `pointer-events-none`, un tap sur ce coin + // marquait toute la saison comme vue sur mobile, où l'icône est invisible. + 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto' )} > Date: Sun, 26 Jul 2026 15:06:50 +0200 Subject: [PATCH 26/98] =?UTF-8?q?fix(dev):=20autorise=20le=20r=C3=A9seau?= =?UTF-8?q?=20local=20=C3=A0=20joindre=20les=20ressources=20de=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next bloque `/_next/*` en cross-origin depuis Next 15.2 : tester depuis un téléphone sur l'IP du Mac faisait rejeter le HMR, les payloads RSC et les Server Actions. L'app se chargeait mais paraissait morte — chaque lien repassait en navigation pleine page (donc sans barre de progression) et aucun bouton ne réagissait, sans la moindre erreur visible côté client. `allowedDevOrigins` reçoit les IPv4 non internes de la machine, résolues au chargement de la config plutôt qu'écrites en dur puisque l'adresse suit le réseau. Sans effet en production, où l'option est ignorée. Vérifié en WebKit (le moteur de Safari) sur iPhone 13 émulé, via l'IP : navigation client rétablie et barre de progression de retour. --- next.config.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/next.config.ts b/next.config.ts index b19ffc0b..ce5b6b7f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,22 @@ +import { networkInterfaces } from 'node:os'; import type { NextConfig } from 'next'; import withSerwist from '@serwist/next'; const isDev = process.env.NODE_ENV === 'development'; + +/** + * IPv4 de la machine sur son réseau, pour tester depuis un téléphone en dev. + * Sans elles, Next bloque `/_next/*` en cross-origin : le routeur client et les + * Server Actions ne répondent plus et l'app paraît figée. Résolues à chaud plutôt + * qu'écrites en dur, l'adresse changeant avec le réseau. + */ +function localNetworkOrigins(): string[] { + return Object.values(networkInterfaces()) + .flat() + .filter((details) => details?.family === 'IPv4' && !details.internal) + .map((details) => details?.address ?? '') + .filter(Boolean); +} const supabaseHost = new URL( process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://localhost' ).hostname; @@ -44,6 +59,7 @@ const securityHeaders = [ const nextConfig: NextConfig = { output: 'standalone', cacheComponents: true, + allowedDevOrigins: localNetworkOrigins(), experimental: { optimizePackageImports: [ 'lucide-react', From 5871e006737b13188c1108780809880b563ccbd3 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 15:41:49 +0200 Subject: [PATCH 27/98] =?UTF-8?q?fix(push):=20l'=C3=A9tat=20d'abonnement?= =?UTF-8?q?=20ne=20reste=20plus=20bloqu=C3=A9=20sur=20=C2=AB=20loading=20?= =?UTF-8?q?=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `navigator.serviceWorker.ready` n'est jamais résolue tant qu'aucun worker n'est activé. Le serveur de dev ne génère pas `sw.js` : le statut restait donc sur « loading » indéfiniment, ce qui masquait le CTA d'activation (il exige `off`) et laissait le réglage des notifications inutilisable. Même impasse en production si l'enregistrement du worker échoue. `activeServiceWorker()` interroge d'abord `getRegistration()`, qui répond tout de suite, puis borne l'attente de `ready`. Sans worker, le statut devient `unsupported` — refus honnête plutôt qu'attente sans fin. --- hooks/usePushSubscription.ts | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/hooks/usePushSubscription.ts b/hooks/usePushSubscription.ts index 71e57ba8..8d0126bb 100644 --- a/hooks/usePushSubscription.ts +++ b/hooks/usePushSubscription.ts @@ -41,6 +41,26 @@ function isIOS(): boolean { return /iPad|iPhone|iPod/.test(navigator.userAgent) || isIPadOS; } +/** Au-delà, on considère qu'aucun service worker ne prendra le contrôle de la page. */ +const SERVICE_WORKER_WAIT_MS = 4000; + +/** + * Le service worker actif, ou `null` s'il n'y en a aucun. `navigator.serviceWorker.ready` + * reste en attente indéfiniment tant qu'aucun worker n'est activé — le cas du serveur de + * dev, qui ne génère pas `sw.js` — et laissait l'état d'abonnement bloqué sur « loading ». + */ +async function activeServiceWorker(): Promise { + const existing = await navigator.serviceWorker.getRegistration(); + if (existing?.active) return existing; + + return Promise.race([ + navigator.serviceWorker.ready, + new Promise((resolve) => + setTimeout(() => resolve(null), SERVICE_WORKER_WAIT_MS) + ), + ]); +} + function toSubscriptionInput(subscription: PushSubscription) { const json = subscription.toJSON(); return { @@ -71,7 +91,9 @@ export function usePushSubscription() { : 'unsupported'; } try { - const registration = await navigator.serviceWorker.ready; + const registration = await activeServiceWorker(); + if (!registration) return 'unsupported'; + const subscription = await registration.pushManager.getSubscription(); return subscription ? 'on' : 'off'; @@ -96,7 +118,12 @@ export function usePushSubscription() { try { if ((await Notification.requestPermission()) !== 'granted') return; - const registration = await navigator.serviceWorker.ready; + const registration = await activeServiceWorker(); + if (!registration) { + setStatus('unsupported'); + return; + } + const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY), @@ -114,9 +141,9 @@ export function usePushSubscription() { const disable = useCallback(async () => { setIsPending(true); try { - const registration = await navigator.serviceWorker.ready; + const registration = await activeServiceWorker(); const subscription = - await registration.pushManager.getSubscription(); + await registration?.pushManager.getSubscription(); if (subscription) { await deletePushSubscription(subscription.endpoint); await subscription.unsubscribe(); From 1ae63e0084dac435554f8b8307297e4c66ce5e26 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 15:41:49 +0200 Subject: [PATCH 28/98] =?UTF-8?q?fix(settings):=20la=20page=20R=C3=A9glage?= =?UTF-8?q?s=20ne=20partait=20plus=20en=20fronti=C3=A8re=20d'erreur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `isSettingsTab` vivait dans SettingsNav, marqué `'use client'`, et la page est un Server Component : Next refuse l'appel — « Attempted to call isSettingsTab() from the server » — et toute la page tombait sur ErrorCard. Introduit avec le deep-link `?section=` des CTA, invisible au build puisque l'erreur est à l'exécution, et non couvert : aucun test ne visitait /settings. Le tableau d'onglets et son garde passent dans `components/settings/tabs.ts`, sans directive, importable des deux côtés de la frontière. Ajoute 8 cas E2E sur /settings, dont chaque `?section=` visée par un CTA et une valeur inconnue, pour que la page ne puisse plus casser en silence. --- app/[lang]/(protected)/settings/page.tsx | 5 +-- components/settings/SettingsNav.tsx | 20 +--------- components/settings/tabs.ts | 23 +++++++++++ tests/e2e/protected/settings.spec.ts | 49 ++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 components/settings/tabs.ts create mode 100644 tests/e2e/protected/settings.spec.ts diff --git a/app/[lang]/(protected)/settings/page.tsx b/app/[lang]/(protected)/settings/page.tsx index 4ba02087..ceb83d48 100644 --- a/app/[lang]/(protected)/settings/page.tsx +++ b/app/[lang]/(protected)/settings/page.tsx @@ -3,10 +3,7 @@ import { requireAuth } from '@/lib/auth'; import { createClient } from '@/lib/supabase/server'; import { isOAuthOnly } from '@/lib/supabase/auth-helpers'; import { SettingsContent } from '@/components/settings/SettingsContent'; -import { - isSettingsTab, - type SettingsTab, -} from '@/components/settings/SettingsNav'; +import { isSettingsTab, type SettingsTab } from '@/components/settings/tabs'; import { SettingsContentSkeleton } from '@/components/settings/SettingsContentSkeleton'; import { PageLayout, PageHeader } from '@/components/layout/PageLayout'; import { getTranslations } from '@/lib/i18n/server'; diff --git a/components/settings/SettingsNav.tsx b/components/settings/SettingsNav.tsx index 6dbf9e9c..8160c9c2 100644 --- a/components/settings/SettingsNav.tsx +++ b/components/settings/SettingsNav.tsx @@ -3,25 +3,9 @@ import { cn } from '@/lib/utils'; import { useTranslation } from '@/lib/i18n/context'; -export const SETTINGS_TABS = [ - 'profile', - 'security', - 'notifications', - 'appearance', - 'services', - 'data', - 'privacy', -] as const; +import type { SettingsTab } from './tabs'; -export type SettingsTab = (typeof SETTINGS_TABS)[number]; - -/** Narrows the `?section=` query param used by the call-to-actions that deep-link here. */ -export function isSettingsTab(value: string | undefined): value is SettingsTab { - return ( - value !== undefined && - (SETTINGS_TABS as readonly string[]).includes(value) - ); -} +export type { SettingsTab }; interface SettingsNavProps { onTabChange: (tab: SettingsTab) => void; diff --git a/components/settings/tabs.ts b/components/settings/tabs.ts new file mode 100644 index 00000000..7982af20 --- /dev/null +++ b/components/settings/tabs.ts @@ -0,0 +1,23 @@ +export const SETTINGS_TABS = [ + 'profile', + 'security', + 'notifications', + 'appearance', + 'services', + 'data', + 'privacy', +] as const; + +export type SettingsTab = (typeof SETTINGS_TABS)[number]; + +/** + * Narrows the `?section=` query param used by the call-to-actions that deep-link here. + * Hors de SettingsNav, qui est un composant client : la page Réglages est un Server + * Component et ne peut pas appeler une fonction exportée depuis la frontière client. + */ +export function isSettingsTab(value: string | undefined): value is SettingsTab { + return ( + value !== undefined && + (SETTINGS_TABS as readonly string[]).includes(value) + ); +} diff --git a/tests/e2e/protected/settings.spec.ts b/tests/e2e/protected/settings.spec.ts new file mode 100644 index 00000000..3ba57043 --- /dev/null +++ b/tests/e2e/protected/settings.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { hasValidAuth } from '../../helpers/auth'; + +const SECTIONS = ['profile', 'notifications', 'services', 'data', 'privacy']; + +test.beforeEach(() => { + test.skip( + !hasValidAuth(), + 'No valid auth session — skipping authenticated tests' + ); +}); + +test.describe('Settings', () => { + test('renders instead of the error boundary', async ({ page }) => { + const pageErrors: string[] = []; + page.on('pageerror', (error) => pageErrors.push(error.message)); + + await page.goto('/fr/settings'); + + await expect( + page.getByRole('button', { name: /Profil|Profile/ }).first() + ).toBeVisible({ timeout: 20_000 }); + await expect(page.getByText('Oups')).toHaveCount(0); + expect(pageErrors).toEqual([]); + }); + + /** Les CTA renvoient vers `?section=…` : un onglet invalide ou serveur-only casserait la page. */ + for (const section of SECTIONS) { + test(`opens directly on ?section=${section}`, async ({ page }) => { + await page.goto(`/fr/settings?section=${section}`); + + await expect(page.getByText('Oups')).toHaveCount(0); + await expect( + page.getByRole('button', { name: /Profil|Profile/ }).first() + ).toBeVisible({ timeout: 20_000 }); + }); + } + + test('falls back to the profile tab on an unknown section', async ({ + page, + }) => { + await page.goto('/fr/settings?section=nimporte-quoi'); + + await expect(page.getByText('Oups')).toHaveCount(0); + await expect( + page.getByRole('button', { name: /Profil|Profile/ }).first() + ).toBeVisible({ timeout: 20_000 }); + }); +}); From 83133714857619eb51d11067a530d930754c43fe Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 16:04:23 +0200 Subject: [PATCH 29/98] refactor(cta): le CTA services rejoint le bandeau commun MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il était le seul des quatre à ne pas passer par PromptBanner : une carte pleine largeur posée dans le flux du dashboard, là où les trois autres partagent le bandeau sous la navbar. Deux styles pour un même rôle. - `streaming` entre dans BANNER_PRIORITY, en dernier - PromptSlot calcule son éligibilité côté serveur, en réutilisant le cache de getCachedStreamingProviders déjà lu par buildLibrarySections - SETTINGS_TARGET remplace l'URL de réglages codée en dur dans le gestionnaire d'action, chaque prompt déclarant l'onglet qu'il ouvre - StreamingPromptCard et StreamingPromptSection disparaissent Conséquence assumée : le services tombe sous la règle commune d'un seul CTA tous les trois jours, donc il s'affichera plus rarement qu'avant, où il vivait hors de la file. Import et services restent exclusifs de fait (moins de 3 titres contre 5 au moins). Vérifié en rendu réel : bandeau à y=64 avec l'icône téléviseur, carte inline absente, et le cas du moteur qui affirmait l'inverse est mis à jour. --- app/[lang]/(protected)/dashboard/page.tsx | 5 -- components/dashboard/StreamingPromptCard.tsx | 66 ------------------- .../dashboard/StreamingPromptSection.tsx | 27 -------- components/prompts/PromptHost.tsx | 26 +++++++- components/prompts/PromptSlot.tsx | 11 +++- lib/prompts/keys.ts | 7 +- tests/unit/prompts-engine.test.ts | 9 ++- 7 files changed, 46 insertions(+), 105 deletions(-) delete mode 100644 components/dashboard/StreamingPromptCard.tsx delete mode 100644 components/dashboard/StreamingPromptSection.tsx diff --git a/app/[lang]/(protected)/dashboard/page.tsx b/app/[lang]/(protected)/dashboard/page.tsx index 91f7351a..14771f9b 100644 --- a/app/[lang]/(protected)/dashboard/page.tsx +++ b/app/[lang]/(protected)/dashboard/page.tsx @@ -29,7 +29,6 @@ import { import { BentoStats } from '@/components/dashboard/BentoStats'; import { TrendingMarquee } from '@/components/dashboard/TrendingMarquee'; import { ContinueWatchingSection } from '@/components/dashboard/ContinueWatchingSection'; -import { StreamingPromptSection } from '@/components/dashboard/StreamingPromptSection'; import { ContinueWatchingSkeleton } from '@/components/dashboard/ContinueWatchingSkeleton'; import { DashboardHeroSkeleton, @@ -328,10 +327,6 @@ export default async function DashboardPage({ params: paramsPromise }: Props) { - - - - }> diff --git a/components/dashboard/StreamingPromptCard.tsx b/components/dashboard/StreamingPromptCard.tsx deleted file mode 100644 index 337186f0..00000000 --- a/components/dashboard/StreamingPromptCard.tsx +++ /dev/null @@ -1,66 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { Tv } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { resolvePrompt } from '@/app/actions/prompts'; -import type { PromptState } from '@/lib/prompts/keys'; -import { useTranslation } from '@/lib/i18n/context'; -import { localizedHref } from '@/lib/i18n/utils'; -import { reportSwallowed } from '@/lib/report'; - -/** Stands in for the "on your services" row while the user has picked no platform. */ -export function StreamingPromptCard() { - const { t, lang } = useTranslation(); - const router = useRouter(); - const [hidden, setHidden] = useState(false); - - if (hidden) return null; - - function settle(state: PromptState) { - setHidden(true); - resolvePrompt('streaming', state).catch((error) => - reportSwallowed('prompts:resolve', error) - ); - } - - return ( -
- - - - -
-

- {t.prompts.streaming.title} -

-

- {t.prompts.streaming.description} -

-
- -
- - -
-
- ); -} diff --git a/components/dashboard/StreamingPromptSection.tsx b/components/dashboard/StreamingPromptSection.tsx deleted file mode 100644 index fb3e6e1c..00000000 --- a/components/dashboard/StreamingPromptSection.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { getCachedStreamingProviders } from '@/lib/data/watchlist'; -import { - getCachedPromptStates, - getCachedWatchlistCount, -} from '@/lib/data/prompts'; -import { getUserContext } from '@/lib/supabase/auth-helpers'; -import { StreamingPromptCard } from '@/components/dashboard/StreamingPromptCard'; - -/** Enough of a library for "what's on my platforms" to return anything useful. */ -const MIN_WATCHLIST_ENTRIES = 5; - -export async function StreamingPromptSection() { - const { user } = await getUserContext(); - if (!user) return null; - - const [providerIds, states, watchlistCount] = await Promise.all([ - getCachedStreamingProviders(), - getCachedPromptStates(), - getCachedWatchlistCount(), - ]); - - if (providerIds.length > 0) return null; - if (states.streaming !== undefined) return null; - if (watchlistCount < MIN_WATCHLIST_ENTRIES) return null; - - return ; -} diff --git a/components/prompts/PromptHost.tsx b/components/prompts/PromptHost.tsx index 0f9e581a..b91ae498 100644 --- a/components/prompts/PromptHost.tsx +++ b/components/prompts/PromptHost.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, type ReactNode } from 'react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; -import { Bell, FolderDown } from 'lucide-react'; +import { Bell, FolderDown, Tv } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { PromptBanner } from '@/components/prompts/PromptBanner'; import { IosInstallHint } from '@/components/prompts/IosInstallHint'; @@ -87,6 +87,14 @@ function promptCopy( actionLabel: t.prompts.push.action, }; + if (key === 'streaming') + return { + icon: , + title: t.prompts.streaming.title, + description: t.prompts.streaming.description, + actionLabel: t.prompts.streaming.action, + }; + return { icon: , title: t.prompts.import.title, @@ -95,10 +103,17 @@ function promptCopy( }; } +/** Onglet des réglages que chaque call-to-action ouvre. */ +const SETTINGS_TARGET: Partial> = { + import: '/settings?section=data', + streaming: '/settings?section=services', +}; + interface PromptHostProps { initialStates: PromptStates; accountCreatedAt: number | null; canImport: boolean; + canPickServices: boolean; } /** Renders the single call-to-action the prompt engine allows for this session, if any. */ @@ -106,6 +121,7 @@ export function PromptHost({ initialStates, accountCreatedAt, canImport, + canPickServices, }: PromptHostProps) { const { t, lang } = useTranslation(); const router = useRouter(); @@ -122,6 +138,7 @@ export function PromptHost({ if (pwa.visible) eligible.push('install'); if (pushRequested && push.status === 'off') eligible.push('push'); if (canImport && visits >= IMPORT_MIN_VISITS) eligible.push('import'); + if (canPickServices) eligible.push('streaming'); promptStore.evaluate( eligible, @@ -134,6 +151,7 @@ export function PromptHost({ pushRequested, push.status, canImport, + canPickServices, initialStates, accountCreatedAt, ]); @@ -167,8 +185,10 @@ export function PromptHost({ await settle('push', 'done'); return; } - router.push(localizedHref(lang, '/settings?section=data')); - await settle('import', 'done'); + + const target = SETTINGS_TARGET[active]; + if (target) router.push(localizedHref(lang, target)); + await settle(active, 'done'); } finally { setPending(false); } diff --git a/components/prompts/PromptSlot.tsx b/components/prompts/PromptSlot.tsx index b83b69b8..d4136f08 100644 --- a/components/prompts/PromptSlot.tsx +++ b/components/prompts/PromptSlot.tsx @@ -3,11 +3,15 @@ import { getCachedPromptStates, getCachedWatchlistCount, } from '@/lib/data/prompts'; +import { getCachedStreamingProviders } from '@/lib/data/watchlist'; import { PromptHost } from '@/components/prompts/PromptHost'; /** Below this many watchlist entries, importing an existing library is worth offering. */ const IMPORT_THRESHOLD = 3; +/** Enough of a library for "what's on my platforms" to return anything useful. */ +const SERVICES_THRESHOLD = 5; + /** Feeds the call-to-action slot with what only the server knows; anonymous visitors still get the install banner. */ export async function PromptSlot() { const { user } = await getUserContext(); @@ -18,12 +22,14 @@ export async function PromptSlot() { initialStates={{}} accountCreatedAt={null} canImport={false} + canPickServices={false} /> ); - const [states, watchlistCount] = await Promise.all([ + const [states, watchlistCount, providerIds] = await Promise.all([ getCachedPromptStates(), getCachedWatchlistCount(), + getCachedStreamingProviders(), ]); const createdAt = Date.parse(user.created_at); @@ -33,6 +39,9 @@ export async function PromptSlot() { initialStates={states} accountCreatedAt={Number.isFinite(createdAt) ? createdAt : null} canImport={watchlistCount < IMPORT_THRESHOLD} + canPickServices={ + providerIds.length === 0 && watchlistCount >= SERVICES_THRESHOLD + } /> ); } diff --git a/lib/prompts/keys.ts b/lib/prompts/keys.ts index 2b366d4f..7f2d044a 100644 --- a/lib/prompts/keys.ts +++ b/lib/prompts/keys.ts @@ -13,7 +13,12 @@ export const ACCOUNT_PROMPTS = new Set(['import', 'streaming']); export const DEVICE_PROMPTS = new Set(['install', 'push']); /** Order the banner slot resolves ties in: install first, since iOS push requires the PWA. */ -export const BANNER_PRIORITY = ['install', 'push', 'import'] as const; +export const BANNER_PRIORITY = [ + 'install', + 'push', + 'import', + 'streaming', +] as const; export function isPromptKey(value: string): value is PromptKey { return (PROMPT_KEYS as readonly string[]).includes(value); diff --git a/tests/unit/prompts-engine.test.ts b/tests/unit/prompts-engine.test.ts index 8c80451c..87af2395 100644 --- a/tests/unit/prompts-engine.test.ts +++ b/tests/unit/prompts-engine.test.ts @@ -77,7 +77,12 @@ describe('pickPrompt', () => { ).toBeNull(); }); - it('ignores an eligible key that has no banner slot', () => { - expect(pickPrompt(context({ eligible: ['streaming'] }))).toBeNull(); + it('shows the streaming prompt, the last of the four', () => { + expect(pickPrompt(context({ eligible: ['streaming'] }))).toBe( + 'streaming' + ); + expect(pickPrompt(context({ eligible: ['streaming', 'import'] }))).toBe( + 'import' + ); }); }); From dcad8130b4c7cd68efb09b178abaa9b795b7e41d Mon Sep 17 00:00:00 2001 From: SAWKIT <23707419+TheSawkit@users.noreply.github.com> Date: Sat, 25 Jul 2026 03:59:25 +0200 Subject: [PATCH 30/98] Dev (#98) --- components/media/card/MediaCard.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index 335851d4..6caf4a74 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -64,6 +64,11 @@ export function MediaCard({ // correctement dès le premier rendu sans dépendre de l'hydratation. const showRatingBadge = !hideRating && media.vote_average > 0; + // Une note absente (upcoming, titre obscur) affichait un « N/A » doré à étoile, qui + // suggère une note inexistante. On masque le badge : décidé côté serveur, donc affiché + // correctement dès le premier rendu sans dépendre de l'hydratation. + const showRatingBadge = !hideRating && media.vote_average > 0; + const href = localizedHref(lang, getMediaHref(media)); const isWatched = watchlistEntry?.status === 'watched'; const badgeEntry = watchlistEntry ?? media.watchlistEntry; From 17a8871ae70697c67300c144999093bd78dfedd5 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 01:48:54 +0200 Subject: [PATCH 31/98] feat(cta): Add CTA for notifications, settings and platforms. --- app/[lang]/(protected)/dashboard/page.tsx | 5 ++ app/actions/prompts.ts | 52 +++++++++++++++ components/dashboard/StreamingPromptCard.tsx | 66 +++++++++++++++++++ .../dashboard/StreamingPromptSection.tsx | 27 ++++++++ 4 files changed, 150 insertions(+) create mode 100644 components/dashboard/StreamingPromptCard.tsx create mode 100644 components/dashboard/StreamingPromptSection.tsx diff --git a/app/[lang]/(protected)/dashboard/page.tsx b/app/[lang]/(protected)/dashboard/page.tsx index 14771f9b..91f7351a 100644 --- a/app/[lang]/(protected)/dashboard/page.tsx +++ b/app/[lang]/(protected)/dashboard/page.tsx @@ -29,6 +29,7 @@ import { import { BentoStats } from '@/components/dashboard/BentoStats'; import { TrendingMarquee } from '@/components/dashboard/TrendingMarquee'; import { ContinueWatchingSection } from '@/components/dashboard/ContinueWatchingSection'; +import { StreamingPromptSection } from '@/components/dashboard/StreamingPromptSection'; import { ContinueWatchingSkeleton } from '@/components/dashboard/ContinueWatchingSkeleton'; import { DashboardHeroSkeleton, @@ -327,6 +328,10 @@ export default async function DashboardPage({ params: paramsPromise }: Props) { + + + + }> diff --git a/app/actions/prompts.ts b/app/actions/prompts.ts index effc464e..0a8006c6 100644 --- a/app/actions/prompts.ts +++ b/app/actions/prompts.ts @@ -52,3 +52,55 @@ export async function resolvePrompt( ); if (error) throw new Error(error.message); } +'use server'; + +import { getAuthenticatedUser, getOptionalUser } from '@/lib/supabase/auth-helpers'; +import { + ACCOUNT_PROMPTS, + PROMPT_KEYS, + type PromptKey, + type PromptState, + type PromptStates, +} from '@/lib/prompts/keys'; + +const VALID_STATES = new Set(['done', 'dismissed']); + +/** Answers already given to account-scoped call-to-actions, so none is ever asked twice. */ +export async function getPromptStates(): Promise { + const { supabase, userId } = await getOptionalUser(); + if (!userId) return {}; + + const { data } = await supabase + .from('user_prompts') + .select('prompt_key, state') + .eq('user_id', userId); + + const states: PromptStates = {}; + for (const row of data ?? []) { + const key = row.prompt_key as PromptKey; + if (PROMPT_KEYS.includes(key)) states[key] = row.state as PromptState; + } + return states; +} + +/** Records the user's answer to a call-to-action; deliberately never revalidates. */ +export async function resolvePrompt( + key: PromptKey, + state: PromptState +): Promise { + if (!ACCOUNT_PROMPTS.has(key)) throw new Error('Invalid prompt_key'); + if (!VALID_STATES.has(state)) throw new Error('Invalid state'); + + const { supabase, userId } = await getAuthenticatedUser(); + + const { error } = await supabase.from('user_prompts').upsert( + { + user_id: userId, + prompt_key: key, + state, + updated_at: new Date().toISOString(), + }, + { onConflict: 'user_id,prompt_key' } + ); + if (error) throw new Error(error.message); +} diff --git a/components/dashboard/StreamingPromptCard.tsx b/components/dashboard/StreamingPromptCard.tsx new file mode 100644 index 00000000..337186f0 --- /dev/null +++ b/components/dashboard/StreamingPromptCard.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Tv } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { resolvePrompt } from '@/app/actions/prompts'; +import type { PromptState } from '@/lib/prompts/keys'; +import { useTranslation } from '@/lib/i18n/context'; +import { localizedHref } from '@/lib/i18n/utils'; +import { reportSwallowed } from '@/lib/report'; + +/** Stands in for the "on your services" row while the user has picked no platform. */ +export function StreamingPromptCard() { + const { t, lang } = useTranslation(); + const router = useRouter(); + const [hidden, setHidden] = useState(false); + + if (hidden) return null; + + function settle(state: PromptState) { + setHidden(true); + resolvePrompt('streaming', state).catch((error) => + reportSwallowed('prompts:resolve', error) + ); + } + + return ( +
+ + + + +
+

+ {t.prompts.streaming.title} +

+

+ {t.prompts.streaming.description} +

+
+ +
+ + +
+
+ ); +} diff --git a/components/dashboard/StreamingPromptSection.tsx b/components/dashboard/StreamingPromptSection.tsx new file mode 100644 index 00000000..a58da55c --- /dev/null +++ b/components/dashboard/StreamingPromptSection.tsx @@ -0,0 +1,27 @@ +import { getMyStreamingProviders } from '@/app/actions/recommendations'; +import { getPromptStates } from '@/app/actions/prompts'; +import { getUserContext } from '@/lib/supabase/auth-helpers'; +import { StreamingPromptCard } from '@/components/dashboard/StreamingPromptCard'; + +/** Enough of a library for "what's on my platforms" to return anything useful. */ +const MIN_WATCHLIST_ENTRIES = 5; + +export async function StreamingPromptSection() { + const { supabase, user } = await getUserContext(); + if (!user) return null; + + const [providerIds, states, watchlist] = await Promise.all([ + getMyStreamingProviders(), + getPromptStates(), + supabase + .from('watchlist') + .select('id', { count: 'exact', head: true }) + .eq('user_id', user.id), + ]); + + if (providerIds.length > 0) return null; + if (states.streaming !== undefined) return null; + if ((watchlist.count ?? 0) < MIN_WATCHLIST_ENTRIES) return null; + + return ; +} From 2596bfee528b3e15a8d89fc5c0eba498ad291984 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 02:32:26 +0200 Subject: [PATCH 32/98] =?UTF-8?q?perf(cta):=20d=C3=A9duplique=20les=20lect?= =?UTF-8?q?ures=20serveur=20du=20slot=20de=20CTA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le slot est monté dans le layout, donc rendu sur chaque page : sur /dashboard il déclenchait cinq requêtes dont trois doublons (états des prompts et compte watchlist demandés deux fois, plateformes hors du cache déjà utilisé par buildLibrarySections). - lib/data/prompts.ts : getCachedPromptStates + getCachedWatchlistCount, même pattern React.cache que lib/data/watchlist.ts - StreamingPromptSection passe sur getCachedStreamingProviders - le compte reste un count exact plutôt que la liste complète, puisque le slot s'affiche aussi sur les pages qui n'ont pas besoin de la watchlist Cinq requêtes ramenées à deux par rendu du dashboard. --- .../dashboard/StreamingPromptSection.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/components/dashboard/StreamingPromptSection.tsx b/components/dashboard/StreamingPromptSection.tsx index a58da55c..fb3e6e1c 100644 --- a/components/dashboard/StreamingPromptSection.tsx +++ b/components/dashboard/StreamingPromptSection.tsx @@ -1,5 +1,8 @@ -import { getMyStreamingProviders } from '@/app/actions/recommendations'; -import { getPromptStates } from '@/app/actions/prompts'; +import { getCachedStreamingProviders } from '@/lib/data/watchlist'; +import { + getCachedPromptStates, + getCachedWatchlistCount, +} from '@/lib/data/prompts'; import { getUserContext } from '@/lib/supabase/auth-helpers'; import { StreamingPromptCard } from '@/components/dashboard/StreamingPromptCard'; @@ -7,21 +10,18 @@ import { StreamingPromptCard } from '@/components/dashboard/StreamingPromptCard' const MIN_WATCHLIST_ENTRIES = 5; export async function StreamingPromptSection() { - const { supabase, user } = await getUserContext(); + const { user } = await getUserContext(); if (!user) return null; - const [providerIds, states, watchlist] = await Promise.all([ - getMyStreamingProviders(), - getPromptStates(), - supabase - .from('watchlist') - .select('id', { count: 'exact', head: true }) - .eq('user_id', user.id), + const [providerIds, states, watchlistCount] = await Promise.all([ + getCachedStreamingProviders(), + getCachedPromptStates(), + getCachedWatchlistCount(), ]); if (providerIds.length > 0) return null; if (states.streaming !== undefined) return null; - if ((watchlist.count ?? 0) < MIN_WATCHLIST_ENTRIES) return null; + if (watchlistCount < MIN_WATCHLIST_ENTRIES) return null; return ; } From 86844c9cb2dc9b243512ade26295e217acc46034 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 02:32:35 +0200 Subject: [PATCH 33/98] =?UTF-8?q?fix(cta):=20comptage=20des=20visites,=20b?= =?UTF-8?q?andeau=20ferm=C3=A9=20et=20validation=20des=20cl=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - countVisit() était évalué derrière un court-circuit `canImport &&`, donc le compteur de visites n'avançait que pour les comptes à watchlist vide. Il est maintenant lu en tête d'effet, indépendamment de qui s'en sert. - le bandeau fermé restait annoncé par les lecteurs d'écran et focusable : translate-y ne le sort que visuellement, d'où aria-hidden + inert. - isPromptKey / isPromptState remplacent les `as` posés avant vérification sur les lignes user_prompts. - usePWAInstall annule son timer avant d'en reposer un, si le navigateur réémet beforeinstallprompt. --- app/actions/prompts.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/actions/prompts.ts b/app/actions/prompts.ts index 0a8006c6..8796cfc8 100644 --- a/app/actions/prompts.ts +++ b/app/actions/prompts.ts @@ -54,10 +54,14 @@ export async function resolvePrompt( } 'use server'; -import { getAuthenticatedUser, getOptionalUser } from '@/lib/supabase/auth-helpers'; +import { + getAuthenticatedUser, + getOptionalUser, +} from '@/lib/supabase/auth-helpers'; import { ACCOUNT_PROMPTS, - PROMPT_KEYS, + isPromptKey, + isPromptState, type PromptKey, type PromptState, type PromptStates, @@ -77,8 +81,8 @@ export async function getPromptStates(): Promise { const states: PromptStates = {}; for (const row of data ?? []) { - const key = row.prompt_key as PromptKey; - if (PROMPT_KEYS.includes(key)) states[key] = row.state as PromptState; + if (isPromptKey(row.prompt_key) && isPromptState(row.state)) + states[row.prompt_key] = row.state; } return states; } @@ -89,7 +93,7 @@ export async function resolvePrompt( state: PromptState ): Promise { if (!ACCOUNT_PROMPTS.has(key)) throw new Error('Invalid prompt_key'); - if (!VALID_STATES.has(state)) throw new Error('Invalid state'); + if (!isPromptState(state)) throw new Error('Invalid state'); const { supabase, userId } = await getAuthenticatedUser(); From 63617d09972c835e8ef12577be8efec44a4a0cf8 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 02:33:03 +0200 Subject: [PATCH 34/98] =?UTF-8?q?chore(i18n):=20tutoiement=20sur=20toute?= =?UTF-8?q?=20l'interface=20fran=C3=A7aise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le dashboard affichait « Tes statistiques » juste au-dessus de « Vos prochains visionnages » : settings tutoyait déjà, le reste vouvoyait. 93 chaînes alignées sur le tutoiement, y compris les impératifs pluriels (« Découvrez » → « Découvre ») que le premier passage sur vous/votre/vos avait laissés. app/manifest.ts duplique quatre descriptions de metadata.ts et suit le même changement. Concerne aussi /terms et /privacy : « Tes droits », « Ton compte ». --- lib/i18n/messages/pages.ts | 48 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/i18n/messages/pages.ts b/lib/i18n/messages/pages.ts index 7b8e3f33..e5368287 100644 --- a/lib/i18n/messages/pages.ts +++ b/lib/i18n/messages/pages.ts @@ -3,63 +3,63 @@ const fr = { privacy: { title: 'Politique de confidentialité', updated: 'Dernière mise à jour : 12 juillet 2026', - intro: 'ReelMark respecte votre vie privée. Cette déclaration explique quelles données personnelles nous traitons, pourquoi, et quels sont vos droits, conformément au Règlement général sur la protection des données (RGPD, UE 2016/679) et à la loi belge du 30 juillet 2018.', + intro: 'ReelMark respecte ta vie privée. Cette déclaration explique quelles données personnelles nous traitons, pourquoi, et quels sont tes droits, conformément au Règlement général sur la protection des données (RGPD, UE 2016/679) et à la loi belge du 30 juillet 2018.', sections: [ { title: '1. Responsable du traitement', body: [ - "ReelMark est édité par Silexio (Belgique). Pour toute question relative à vos données personnelles, contactez-nous à l'adresse : contact@silexio.be.", + "ReelMark est édité par Silexio (Belgique). Pour toute question relative à tes données personnelles, écris-nous à l'adresse : contact@silexio.be.", ], }, { title: '2. Données que nous collectons', body: [ - "Données de compte : adresse e-mail, mot de passe (haché, jamais stocké en clair), nom d'utilisateur, nom affiché, avatar, langue et région préférées. Si vous vous connectez avec Google, nous recevons votre e-mail et les informations de base de votre profil Google.", - "Contenu que vous créez : votre watchlist, les épisodes marqués comme vus, vos notes et critiques, vos playlists et vos relations d'amis sur la plateforme.", - "Données techniques : cookies strictement nécessaires (session de connexion, préférence de langue, répartition de charge), préférences stockées localement dans votre navigateur (thème, affichage) et journaux d'erreurs techniques.", + "Données de compte : adresse e-mail, mot de passe (haché, jamais stocké en clair), nom d'utilisateur, nom affiché, avatar, langue et région préférées. Si tu te connectes avec Google, nous recevons ton e-mail et les informations de base de ton profil Google.", + "Contenu que tu crées : ta watchlist, les épisodes marqués comme vus, tes notes et critiques, tes playlists et tes relations d'amis sur la plateforme.", + "Données techniques : cookies strictement nécessaires (session de connexion, préférence de langue, répartition de charge), préférences stockées localement dans ton navigateur (thème, affichage) et journaux d'erreurs techniques.", 'Nous ne collectons aucune donnée publicitaire et ne réalisons aucun profilage commercial.', ], }, { title: '3. Finalités et bases légales', body: [ - 'Fournir le service (création de compte, sauvegarde de votre bibliothèque, fonctionnalités sociales) : exécution du contrat qui nous lie lorsque vous créez un compte.', + 'Fournir le service (création de compte, sauvegarde de ta bibliothèque, fonctionnalités sociales) : exécution du contrat qui nous lie lorsque tu crées un compte.', "Assurer la sécurité de la plateforme (prévention des abus, journaux d'erreurs) : intérêt légitime.", - 'Mémoriser vos préférences (langue, thème) : consentement, exprimé par votre usage de ces réglages.', + 'Mémoriser tes préférences (langue, thème) : consentement, exprimé par ton usage de ces réglages.', ], }, { title: '4. Destinataires et sous-traitants', body: [ - 'Vos données sont traitées par nos sous-traitants techniques : Supabase (base de données et authentification), Infomaniak (hébergement en Suisse, pays reconnu adéquat par la Commission européenne) et Cloudflare (sécurité et diffusion du contenu, encadré par des clauses contractuelles types).', + 'Tes données sont traitées par nos sous-traitants techniques : Supabase (base de données et authentification), Infomaniak (hébergement en Suisse, pays reconnu adéquat par la Commission européenne) et Cloudflare (sécurité et diffusion du contenu, encadré par des clauses contractuelles types).', 'Les informations sur les films et séries proviennent de TMDB ; aucune donnée personnelle ne lui est transmise.', - 'Nous ne vendons ni ne louons vos données. Elles ne sont communiquées à aucun autre tiers, sauf obligation légale.', + 'Nous ne vendons ni ne louons tes données. Elles ne sont communiquées à aucun autre tiers, sauf obligation légale.', ], }, { title: '5. Durée de conservation', body: [ - 'Vos données sont conservées tant que votre compte est actif. La suppression de votre compte (Réglages → Zone de danger) entraîne la suppression de vos données personnelles. Les journaux techniques sont conservés pour une durée limitée.', + 'Tes données sont conservées tant que ton compte est actif. La suppression de ton compte (Réglages → Zone de danger) entraîne la suppression de tes données personnelles. Les journaux techniques sont conservés pour une durée limitée.', ], }, { - title: '6. Vos droits', + title: '6. Tes droits', body: [ - "Vous disposez des droits d'accès, de rectification, d'effacement, de portabilité, de limitation et d'opposition, ainsi que du droit de retirer votre consentement à tout moment.", - "Vous pouvez exporter vos données à tout moment (Réglages → Données), modifier votre profil ou supprimer votre compte directement dans l'application, ou nous écrire à contact@silexio.be.", - "Vous pouvez introduire une réclamation auprès de l'Autorité de protection des données belge (APD) : www.autoriteprotectiondonnees.be.", + "Tu disposes des droits d'accès, de rectification, d'effacement, de portabilité, de limitation et d'opposition, ainsi que du droit de retirer ton consentement à tout moment.", + "Tu peux exporter tes données à tout moment (Réglages → Données), modifier ton profil ou supprimer ton compte directement dans l'application, ou nous écrire à contact@silexio.be.", + "Tu peux introduire une réclamation auprès de l'Autorité de protection des données belge (APD) : www.autoriteprotectiondonnees.be.", ], }, { title: '7. Sécurité', body: [ - "Vos données sont chiffrées en transit (TLS), les mots de passe sont hachés, et l'accès aux données est restreint par des règles de sécurité au niveau de la base de données.", + "Tes données sont chiffrées en transit (TLS), les mots de passe sont hachés, et l'accès aux données est restreint par des règles de sécurité au niveau de la base de données.", ], }, { title: '8. Cookies et stockage local', body: [ - "Nous n'utilisons que des cookies strictement nécessaires au fonctionnement du service : session de connexion, préférence de langue et répartition de charge. Vos préférences d'affichage (thème) sont stockées localement dans votre navigateur. Aucun cookie publicitaire ni traceur tiers n'est utilisé.", + "Nous n'utilisons que des cookies strictement nécessaires au fonctionnement du service : session de connexion, préférence de langue et répartition de charge. Tes préférences d'affichage (thème) sont stockées localement dans ton navigateur. Aucun cookie publicitaire ni traceur tiers n'est utilisé.", ], }, { @@ -79,18 +79,18 @@ const fr = { terms: { title: "Conditions d'utilisation", updated: 'Dernière mise à jour : 12 juillet 2026', - intro: "Les présentes conditions régissent l'utilisation de ReelMark, une plateforme personnelle de suivi de films et de séries. En créant un compte ou en utilisant le service, vous les acceptez.", + intro: "Les présentes conditions régissent l'utilisation de ReelMark, une plateforme personnelle de suivi de films et de séries. En créant un compte ou en utilisant le service, tu les acceptes.", sections: [ { title: '1. Le service', body: [ - 'ReelMark vous permet de suivre les films et séries que vous avez vus ou souhaitez voir, de noter et critiquer des œuvres, de créer des playlists et de partager votre activité avec des amis.', + 'ReelMark te permet de suivre les films et séries que tu as vus ou souhaites voir, de noter et critiquer des œuvres, de créer des playlists et de partager ton activité avec des amis.', ], }, { - title: '2. Votre compte', + title: '2. Ton compte', body: [ - "Vous devez avoir au moins 13 ans pour créer un compte. Vous êtes responsable de la confidentialité de vos identifiants et de l'activité réalisée depuis votre compte. Les informations fournies doivent être exactes.", + "Tu dois avoir au moins 13 ans pour créer un compte. Tu es responsable de la confidentialité de tes identifiants et de l'activité réalisée depuis ton compte. Les informations fournies doivent être exactes.", ], }, { @@ -100,9 +100,9 @@ const fr = { ], }, { - title: '4. Votre contenu', + title: '4. Ton contenu', body: [ - 'Vous restez propriétaire des critiques, notes et playlists que vous créez. Vous nous accordez une licence limitée pour les afficher dans le service, selon les réglages de visibilité que vous choisissez. Nous pouvons retirer tout contenu contraire à ces conditions.', + 'Tu restes propriétaire des critiques, notes et playlists que tu crées. Tu nous accordes une licence limitée pour les afficher dans le service, selon les réglages de visibilité que tu choisis. Nous pouvons retirer tout contenu contraire à ces conditions.', ], }, { @@ -120,7 +120,7 @@ const fr = { { title: '7. Résiliation', body: [ - 'Vous pouvez supprimer votre compte à tout moment depuis les réglages. Nous pouvons suspendre ou supprimer un compte en cas de violation de ces conditions.', + 'Tu peux supprimer ton compte à tout moment depuis les réglages. Nous pouvons suspendre ou supprimer un compte en cas de violation de ces conditions.', ], }, { @@ -132,7 +132,7 @@ const fr = { { title: '9. Droit applicable', body: [ - 'Les présentes conditions sont régies par le droit belge. Tout litige relève des tribunaux belges compétents, sans préjudice des droits impératifs dont vous bénéficiez en tant que consommateur.', + 'Les présentes conditions sont régies par le droit belge. Tout litige relève des tribunaux belges compétents, sans préjudice des droits impératifs dont tu bénéficies en tant que consommateur.', ], }, { From 2ed79b326108940dd9e088ea125f67d20e071399 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 14:19:05 +0200 Subject: [PATCH 35/98] =?UTF-8?q?chore(i18n):=20r=C3=A9tablit=20le=20vouvo?= =?UTF-8?q?iement=20sur=20/terms=20et=20/privacy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le tutoiement généralisé avait emporté les pages légales, où le vouvoiement reste la convention. Seul le bloc legal français revient en arrière : le dashboard, l'explorer et le reste de pages.ts gardent le tutoiement. --- lib/i18n/messages/pages.ts | 48 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/i18n/messages/pages.ts b/lib/i18n/messages/pages.ts index e5368287..7b8e3f33 100644 --- a/lib/i18n/messages/pages.ts +++ b/lib/i18n/messages/pages.ts @@ -3,63 +3,63 @@ const fr = { privacy: { title: 'Politique de confidentialité', updated: 'Dernière mise à jour : 12 juillet 2026', - intro: 'ReelMark respecte ta vie privée. Cette déclaration explique quelles données personnelles nous traitons, pourquoi, et quels sont tes droits, conformément au Règlement général sur la protection des données (RGPD, UE 2016/679) et à la loi belge du 30 juillet 2018.', + intro: 'ReelMark respecte votre vie privée. Cette déclaration explique quelles données personnelles nous traitons, pourquoi, et quels sont vos droits, conformément au Règlement général sur la protection des données (RGPD, UE 2016/679) et à la loi belge du 30 juillet 2018.', sections: [ { title: '1. Responsable du traitement', body: [ - "ReelMark est édité par Silexio (Belgique). Pour toute question relative à tes données personnelles, écris-nous à l'adresse : contact@silexio.be.", + "ReelMark est édité par Silexio (Belgique). Pour toute question relative à vos données personnelles, contactez-nous à l'adresse : contact@silexio.be.", ], }, { title: '2. Données que nous collectons', body: [ - "Données de compte : adresse e-mail, mot de passe (haché, jamais stocké en clair), nom d'utilisateur, nom affiché, avatar, langue et région préférées. Si tu te connectes avec Google, nous recevons ton e-mail et les informations de base de ton profil Google.", - "Contenu que tu crées : ta watchlist, les épisodes marqués comme vus, tes notes et critiques, tes playlists et tes relations d'amis sur la plateforme.", - "Données techniques : cookies strictement nécessaires (session de connexion, préférence de langue, répartition de charge), préférences stockées localement dans ton navigateur (thème, affichage) et journaux d'erreurs techniques.", + "Données de compte : adresse e-mail, mot de passe (haché, jamais stocké en clair), nom d'utilisateur, nom affiché, avatar, langue et région préférées. Si vous vous connectez avec Google, nous recevons votre e-mail et les informations de base de votre profil Google.", + "Contenu que vous créez : votre watchlist, les épisodes marqués comme vus, vos notes et critiques, vos playlists et vos relations d'amis sur la plateforme.", + "Données techniques : cookies strictement nécessaires (session de connexion, préférence de langue, répartition de charge), préférences stockées localement dans votre navigateur (thème, affichage) et journaux d'erreurs techniques.", 'Nous ne collectons aucune donnée publicitaire et ne réalisons aucun profilage commercial.', ], }, { title: '3. Finalités et bases légales', body: [ - 'Fournir le service (création de compte, sauvegarde de ta bibliothèque, fonctionnalités sociales) : exécution du contrat qui nous lie lorsque tu crées un compte.', + 'Fournir le service (création de compte, sauvegarde de votre bibliothèque, fonctionnalités sociales) : exécution du contrat qui nous lie lorsque vous créez un compte.', "Assurer la sécurité de la plateforme (prévention des abus, journaux d'erreurs) : intérêt légitime.", - 'Mémoriser tes préférences (langue, thème) : consentement, exprimé par ton usage de ces réglages.', + 'Mémoriser vos préférences (langue, thème) : consentement, exprimé par votre usage de ces réglages.', ], }, { title: '4. Destinataires et sous-traitants', body: [ - 'Tes données sont traitées par nos sous-traitants techniques : Supabase (base de données et authentification), Infomaniak (hébergement en Suisse, pays reconnu adéquat par la Commission européenne) et Cloudflare (sécurité et diffusion du contenu, encadré par des clauses contractuelles types).', + 'Vos données sont traitées par nos sous-traitants techniques : Supabase (base de données et authentification), Infomaniak (hébergement en Suisse, pays reconnu adéquat par la Commission européenne) et Cloudflare (sécurité et diffusion du contenu, encadré par des clauses contractuelles types).', 'Les informations sur les films et séries proviennent de TMDB ; aucune donnée personnelle ne lui est transmise.', - 'Nous ne vendons ni ne louons tes données. Elles ne sont communiquées à aucun autre tiers, sauf obligation légale.', + 'Nous ne vendons ni ne louons vos données. Elles ne sont communiquées à aucun autre tiers, sauf obligation légale.', ], }, { title: '5. Durée de conservation', body: [ - 'Tes données sont conservées tant que ton compte est actif. La suppression de ton compte (Réglages → Zone de danger) entraîne la suppression de tes données personnelles. Les journaux techniques sont conservés pour une durée limitée.', + 'Vos données sont conservées tant que votre compte est actif. La suppression de votre compte (Réglages → Zone de danger) entraîne la suppression de vos données personnelles. Les journaux techniques sont conservés pour une durée limitée.', ], }, { - title: '6. Tes droits', + title: '6. Vos droits', body: [ - "Tu disposes des droits d'accès, de rectification, d'effacement, de portabilité, de limitation et d'opposition, ainsi que du droit de retirer ton consentement à tout moment.", - "Tu peux exporter tes données à tout moment (Réglages → Données), modifier ton profil ou supprimer ton compte directement dans l'application, ou nous écrire à contact@silexio.be.", - "Tu peux introduire une réclamation auprès de l'Autorité de protection des données belge (APD) : www.autoriteprotectiondonnees.be.", + "Vous disposez des droits d'accès, de rectification, d'effacement, de portabilité, de limitation et d'opposition, ainsi que du droit de retirer votre consentement à tout moment.", + "Vous pouvez exporter vos données à tout moment (Réglages → Données), modifier votre profil ou supprimer votre compte directement dans l'application, ou nous écrire à contact@silexio.be.", + "Vous pouvez introduire une réclamation auprès de l'Autorité de protection des données belge (APD) : www.autoriteprotectiondonnees.be.", ], }, { title: '7. Sécurité', body: [ - "Tes données sont chiffrées en transit (TLS), les mots de passe sont hachés, et l'accès aux données est restreint par des règles de sécurité au niveau de la base de données.", + "Vos données sont chiffrées en transit (TLS), les mots de passe sont hachés, et l'accès aux données est restreint par des règles de sécurité au niveau de la base de données.", ], }, { title: '8. Cookies et stockage local', body: [ - "Nous n'utilisons que des cookies strictement nécessaires au fonctionnement du service : session de connexion, préférence de langue et répartition de charge. Tes préférences d'affichage (thème) sont stockées localement dans ton navigateur. Aucun cookie publicitaire ni traceur tiers n'est utilisé.", + "Nous n'utilisons que des cookies strictement nécessaires au fonctionnement du service : session de connexion, préférence de langue et répartition de charge. Vos préférences d'affichage (thème) sont stockées localement dans votre navigateur. Aucun cookie publicitaire ni traceur tiers n'est utilisé.", ], }, { @@ -79,18 +79,18 @@ const fr = { terms: { title: "Conditions d'utilisation", updated: 'Dernière mise à jour : 12 juillet 2026', - intro: "Les présentes conditions régissent l'utilisation de ReelMark, une plateforme personnelle de suivi de films et de séries. En créant un compte ou en utilisant le service, tu les acceptes.", + intro: "Les présentes conditions régissent l'utilisation de ReelMark, une plateforme personnelle de suivi de films et de séries. En créant un compte ou en utilisant le service, vous les acceptez.", sections: [ { title: '1. Le service', body: [ - 'ReelMark te permet de suivre les films et séries que tu as vus ou souhaites voir, de noter et critiquer des œuvres, de créer des playlists et de partager ton activité avec des amis.', + 'ReelMark vous permet de suivre les films et séries que vous avez vus ou souhaitez voir, de noter et critiquer des œuvres, de créer des playlists et de partager votre activité avec des amis.', ], }, { - title: '2. Ton compte', + title: '2. Votre compte', body: [ - "Tu dois avoir au moins 13 ans pour créer un compte. Tu es responsable de la confidentialité de tes identifiants et de l'activité réalisée depuis ton compte. Les informations fournies doivent être exactes.", + "Vous devez avoir au moins 13 ans pour créer un compte. Vous êtes responsable de la confidentialité de vos identifiants et de l'activité réalisée depuis votre compte. Les informations fournies doivent être exactes.", ], }, { @@ -100,9 +100,9 @@ const fr = { ], }, { - title: '4. Ton contenu', + title: '4. Votre contenu', body: [ - 'Tu restes propriétaire des critiques, notes et playlists que tu crées. Tu nous accordes une licence limitée pour les afficher dans le service, selon les réglages de visibilité que tu choisis. Nous pouvons retirer tout contenu contraire à ces conditions.', + 'Vous restez propriétaire des critiques, notes et playlists que vous créez. Vous nous accordez une licence limitée pour les afficher dans le service, selon les réglages de visibilité que vous choisissez. Nous pouvons retirer tout contenu contraire à ces conditions.', ], }, { @@ -120,7 +120,7 @@ const fr = { { title: '7. Résiliation', body: [ - 'Tu peux supprimer ton compte à tout moment depuis les réglages. Nous pouvons suspendre ou supprimer un compte en cas de violation de ces conditions.', + 'Vous pouvez supprimer votre compte à tout moment depuis les réglages. Nous pouvons suspendre ou supprimer un compte en cas de violation de ces conditions.', ], }, { @@ -132,7 +132,7 @@ const fr = { { title: '9. Droit applicable', body: [ - 'Les présentes conditions sont régies par le droit belge. Tout litige relève des tribunaux belges compétents, sans préjudice des droits impératifs dont tu bénéficies en tant que consommateur.', + 'Les présentes conditions sont régies par le droit belge. Tout litige relève des tribunaux belges compétents, sans préjudice des droits impératifs dont vous bénéficiez en tant que consommateur.', ], }, { From 0900a50a7dfe7d400e6d9474561ca22f0188f718 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 14:36:44 +0200 Subject: [PATCH 36/98] =?UTF-8?q?fix(ux):=20contr=C3=B4les=20de=20card=20u?= =?UTF-8?q?tilisables=20au=20doigt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux défauts distincts rendaient la watchlist inaccessible sur mobile : - le dégradé décoratif de MediaCard couvre toute la carte en `inset-0` sans `pointer-events-none`, donc il absorbait les taps destinés aux boutons placés au-dessus de lui — y compris quand ceux-ci étaient parfaitement visibles. - les contrôles révélés au survol restaient inertes sur tactile, où `hover:` ne se déclenche jamais : sans être ni visibles ni atteignables, le bouton watchlist d'une card et l'icône de saison non commencée n'avaient aucun équivalent au doigt. `pointer-coarse:` (Tailwind 4.1+) les affiche et les active sur tactile, ce qui lève l'alternative « invisible mais tapable » / « inatteignable » sans toucher au comportement desktop. Vérifié sur dashboard, library, /movie/[id] et /tv/[id] en viewport 390×844 : plus aucun bouton dont le tap n'atteint pas sa cible. touch-ghost-controls et media-card-tap restent verts. --- components/media/card/MediaCard.tsx | 9 +++++---- components/media/tv/SeasonCard.tsx | 9 ++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index 6caf4a74..98733b16 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -191,10 +191,11 @@ export function MediaCard({ className={cn( 'absolute inset-x-0 bottom-0 flex flex-col gap-3 p-4 transition-all duration-(--duration-base) z-10', 'translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100', - // Sans hover, l'overlay ne s'affiche jamais (Tailwind v4 borne `hover:` à - // @media (hover:hover)) : un tap au bas du poster tombait sur le WatchButton - // invisible et ajoutait le titre à la watchlist au lieu de naviguer. - 'pointer-events-none group-hover:pointer-events-auto' + 'pointer-events-none group-hover:pointer-events-auto', + // `hover:` ne se déclenche jamais sur tactile (Tailwind v4 le borne à + // @media (hover:hover)) : l'overlay y reste donc affiché et actif, sinon + // le bouton watchlist serait soit invisible et tapable, soit inatteignable. + 'pointer-coarse:translate-y-0 pointer-coarse:opacity-100 pointer-coarse:pointer-events-auto' )} >
diff --git a/components/media/tv/SeasonCard.tsx b/components/media/tv/SeasonCard.tsx index e304c013..3c96901d 100644 --- a/components/media/tv/SeasonCard.tsx +++ b/components/media/tv/SeasonCard.tsx @@ -127,9 +127,12 @@ export function SeasonCard({ 'absolute top-2 right-2 z-10 transition-all duration-(--duration-base)', watched > 0 ? 'opacity-100' - : // Masqué sans hover : sans `pointer-events-none`, un tap sur ce coin - // marquait toute la saison comme vue sur mobile, où l'icône est invisible. - 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto' + : cn( + 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto', + // Sur tactile `hover:` ne se déclenche pas : l'icône y reste visible + // et active, sinon elle serait tapable à l'aveugle ou inatteignable. + 'pointer-coarse:opacity-100 pointer-coarse:pointer-events-auto' + ) )} > Date: Sun, 26 Jul 2026 14:50:31 +0200 Subject: [PATCH 37/98] =?UTF-8?q?revert(ux):=20les=20contr=C3=B4les=20au?= =?UTF-8?q?=20survol=20restent=20masqu=C3=A9s=20sur=20tactile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Annule l'affichage permanent introduit par cdb111d : sur une card, l'overlay de survol doit rester invisible et inerte au doigt, pas occuper l'affiche. Seul `pointer-events-none` est conservé sur le dégradé décoratif de MediaCard : il couvre la carte en `inset-0` et n'a aucune raison d'être une cible de pointeur, même si ce n'était pas lui qui bloquait les boutons. --- components/media/card/MediaCard.tsx | 9 ++++----- components/media/tv/SeasonCard.tsx | 9 +++------ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index 98733b16..6caf4a74 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -191,11 +191,10 @@ export function MediaCard({ className={cn( 'absolute inset-x-0 bottom-0 flex flex-col gap-3 p-4 transition-all duration-(--duration-base) z-10', 'translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100', - 'pointer-events-none group-hover:pointer-events-auto', - // `hover:` ne se déclenche jamais sur tactile (Tailwind v4 le borne à - // @media (hover:hover)) : l'overlay y reste donc affiché et actif, sinon - // le bouton watchlist serait soit invisible et tapable, soit inatteignable. - 'pointer-coarse:translate-y-0 pointer-coarse:opacity-100 pointer-coarse:pointer-events-auto' + // Sans hover, l'overlay ne s'affiche jamais (Tailwind v4 borne `hover:` à + // @media (hover:hover)) : un tap au bas du poster tombait sur le WatchButton + // invisible et ajoutait le titre à la watchlist au lieu de naviguer. + 'pointer-events-none group-hover:pointer-events-auto' )} >
diff --git a/components/media/tv/SeasonCard.tsx b/components/media/tv/SeasonCard.tsx index 3c96901d..e304c013 100644 --- a/components/media/tv/SeasonCard.tsx +++ b/components/media/tv/SeasonCard.tsx @@ -127,12 +127,9 @@ export function SeasonCard({ 'absolute top-2 right-2 z-10 transition-all duration-(--duration-base)', watched > 0 ? 'opacity-100' - : cn( - 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto', - // Sur tactile `hover:` ne se déclenche pas : l'icône y reste visible - // et active, sinon elle serait tapable à l'aveugle ou inatteignable. - 'pointer-coarse:opacity-100 pointer-coarse:pointer-events-auto' - ) + : // Masqué sans hover : sans `pointer-events-none`, un tap sur ce coin + // marquait toute la saison comme vue sur mobile, où l'icône est invisible. + 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto' )} > Date: Sun, 26 Jul 2026 16:04:23 +0200 Subject: [PATCH 38/98] refactor(cta): le CTA services rejoint le bandeau commun MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il était le seul des quatre à ne pas passer par PromptBanner : une carte pleine largeur posée dans le flux du dashboard, là où les trois autres partagent le bandeau sous la navbar. Deux styles pour un même rôle. - `streaming` entre dans BANNER_PRIORITY, en dernier - PromptSlot calcule son éligibilité côté serveur, en réutilisant le cache de getCachedStreamingProviders déjà lu par buildLibrarySections - SETTINGS_TARGET remplace l'URL de réglages codée en dur dans le gestionnaire d'action, chaque prompt déclarant l'onglet qu'il ouvre - StreamingPromptCard et StreamingPromptSection disparaissent Conséquence assumée : le services tombe sous la règle commune d'un seul CTA tous les trois jours, donc il s'affichera plus rarement qu'avant, où il vivait hors de la file. Import et services restent exclusifs de fait (moins de 3 titres contre 5 au moins). Vérifié en rendu réel : bandeau à y=64 avec l'icône téléviseur, carte inline absente, et le cas du moteur qui affirmait l'inverse est mis à jour. --- app/[lang]/(protected)/dashboard/page.tsx | 5 -- components/dashboard/StreamingPromptCard.tsx | 66 ------------------- .../dashboard/StreamingPromptSection.tsx | 27 -------- 3 files changed, 98 deletions(-) delete mode 100644 components/dashboard/StreamingPromptCard.tsx delete mode 100644 components/dashboard/StreamingPromptSection.tsx diff --git a/app/[lang]/(protected)/dashboard/page.tsx b/app/[lang]/(protected)/dashboard/page.tsx index 91f7351a..14771f9b 100644 --- a/app/[lang]/(protected)/dashboard/page.tsx +++ b/app/[lang]/(protected)/dashboard/page.tsx @@ -29,7 +29,6 @@ import { import { BentoStats } from '@/components/dashboard/BentoStats'; import { TrendingMarquee } from '@/components/dashboard/TrendingMarquee'; import { ContinueWatchingSection } from '@/components/dashboard/ContinueWatchingSection'; -import { StreamingPromptSection } from '@/components/dashboard/StreamingPromptSection'; import { ContinueWatchingSkeleton } from '@/components/dashboard/ContinueWatchingSkeleton'; import { DashboardHeroSkeleton, @@ -328,10 +327,6 @@ export default async function DashboardPage({ params: paramsPromise }: Props) { - - - - }> diff --git a/components/dashboard/StreamingPromptCard.tsx b/components/dashboard/StreamingPromptCard.tsx deleted file mode 100644 index 337186f0..00000000 --- a/components/dashboard/StreamingPromptCard.tsx +++ /dev/null @@ -1,66 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { Tv } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { resolvePrompt } from '@/app/actions/prompts'; -import type { PromptState } from '@/lib/prompts/keys'; -import { useTranslation } from '@/lib/i18n/context'; -import { localizedHref } from '@/lib/i18n/utils'; -import { reportSwallowed } from '@/lib/report'; - -/** Stands in for the "on your services" row while the user has picked no platform. */ -export function StreamingPromptCard() { - const { t, lang } = useTranslation(); - const router = useRouter(); - const [hidden, setHidden] = useState(false); - - if (hidden) return null; - - function settle(state: PromptState) { - setHidden(true); - resolvePrompt('streaming', state).catch((error) => - reportSwallowed('prompts:resolve', error) - ); - } - - return ( -
- - - - -
-

- {t.prompts.streaming.title} -

-

- {t.prompts.streaming.description} -

-
- -
- - -
-
- ); -} diff --git a/components/dashboard/StreamingPromptSection.tsx b/components/dashboard/StreamingPromptSection.tsx deleted file mode 100644 index fb3e6e1c..00000000 --- a/components/dashboard/StreamingPromptSection.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { getCachedStreamingProviders } from '@/lib/data/watchlist'; -import { - getCachedPromptStates, - getCachedWatchlistCount, -} from '@/lib/data/prompts'; -import { getUserContext } from '@/lib/supabase/auth-helpers'; -import { StreamingPromptCard } from '@/components/dashboard/StreamingPromptCard'; - -/** Enough of a library for "what's on my platforms" to return anything useful. */ -const MIN_WATCHLIST_ENTRIES = 5; - -export async function StreamingPromptSection() { - const { user } = await getUserContext(); - if (!user) return null; - - const [providerIds, states, watchlistCount] = await Promise.all([ - getCachedStreamingProviders(), - getCachedPromptStates(), - getCachedWatchlistCount(), - ]); - - if (providerIds.length > 0) return null; - if (states.streaming !== undefined) return null; - if (watchlistCount < MIN_WATCHLIST_ENTRIES) return null; - - return ; -} From cd803e6cb6ffe9c9bce05985dfca16b14e08cfc9 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 16:27:40 +0200 Subject: [PATCH 39/98] chroe(format): Prettier apply --- app/actions/prompts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/actions/prompts.ts b/app/actions/prompts.ts index 8796cfc8..d6523a0a 100644 --- a/app/actions/prompts.ts +++ b/app/actions/prompts.ts @@ -52,7 +52,7 @@ export async function resolvePrompt( ); if (error) throw new Error(error.message); } -'use server'; +('use server'); import { getAuthenticatedUser, From e01e50fd8a3f5e49284ca021094adcc4df663130 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 16:43:07 +0200 Subject: [PATCH 40/98] chore(pnpm): Update dependencies --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e82b335b..902757a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -317,10 +317,10 @@ packages: integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==, } - '@emnapi/runtime@1.11.2': + '@emnapi/runtime@1.11.3': resolution: { - integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==, + integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==, } '@emnapi/wasi-threads@1.2.1': @@ -6200,7 +6200,7 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.11.2': + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true @@ -6379,7 +6379,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.11.2 + '@emnapi/runtime': 1.11.3 optional: true '@img/sharp-win32-arm64@0.34.5': From 79d0a61afc5d5bb194d6e01bcf2156a0e9705d05 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 16:46:16 +0200 Subject: [PATCH 41/98] fix: Resolve conflits du dimanche --- app/actions/prompts.ts | 58 +---------------------------- components/media/card/MediaCard.tsx | 5 --- 2 files changed, 1 insertion(+), 62 deletions(-) diff --git a/app/actions/prompts.ts b/app/actions/prompts.ts index d6523a0a..7d079d6d 100644 --- a/app/actions/prompts.ts +++ b/app/actions/prompts.ts @@ -51,60 +51,4 @@ export async function resolvePrompt( { onConflict: 'user_id,prompt_key' } ); if (error) throw new Error(error.message); -} -('use server'); - -import { - getAuthenticatedUser, - getOptionalUser, -} from '@/lib/supabase/auth-helpers'; -import { - ACCOUNT_PROMPTS, - isPromptKey, - isPromptState, - type PromptKey, - type PromptState, - type PromptStates, -} from '@/lib/prompts/keys'; - -const VALID_STATES = new Set(['done', 'dismissed']); - -/** Answers already given to account-scoped call-to-actions, so none is ever asked twice. */ -export async function getPromptStates(): Promise { - const { supabase, userId } = await getOptionalUser(); - if (!userId) return {}; - - const { data } = await supabase - .from('user_prompts') - .select('prompt_key, state') - .eq('user_id', userId); - - const states: PromptStates = {}; - for (const row of data ?? []) { - if (isPromptKey(row.prompt_key) && isPromptState(row.state)) - states[row.prompt_key] = row.state; - } - return states; -} - -/** Records the user's answer to a call-to-action; deliberately never revalidates. */ -export async function resolvePrompt( - key: PromptKey, - state: PromptState -): Promise { - if (!ACCOUNT_PROMPTS.has(key)) throw new Error('Invalid prompt_key'); - if (!isPromptState(state)) throw new Error('Invalid state'); - - const { supabase, userId } = await getAuthenticatedUser(); - - const { error } = await supabase.from('user_prompts').upsert( - { - user_id: userId, - prompt_key: key, - state, - updated_at: new Date().toISOString(), - }, - { onConflict: 'user_id,prompt_key' } - ); - if (error) throw new Error(error.message); -} +} \ No newline at end of file diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index 6caf4a74..335851d4 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -64,11 +64,6 @@ export function MediaCard({ // correctement dès le premier rendu sans dépendre de l'hydratation. const showRatingBadge = !hideRating && media.vote_average > 0; - // Une note absente (upcoming, titre obscur) affichait un « N/A » doré à étoile, qui - // suggère une note inexistante. On masque le badge : décidé côté serveur, donc affiché - // correctement dès le premier rendu sans dépendre de l'hydratation. - const showRatingBadge = !hideRating && media.vote_average > 0; - const href = localizedHref(lang, getMediaHref(media)); const isWatched = watchlistEntry?.status === 'watched'; const badgeEntry = watchlistEntry ?? media.watchlistEntry; From b7321697d4457706e837f2e7deee94935f7e878c Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 16:53:25 +0200 Subject: [PATCH 42/98] fix: Resolve dependencies du dimanche --- app/actions/prompts.ts | 2 +- pnpm-lock.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/actions/prompts.ts b/app/actions/prompts.ts index 7d079d6d..effc464e 100644 --- a/app/actions/prompts.ts +++ b/app/actions/prompts.ts @@ -51,4 +51,4 @@ export async function resolvePrompt( { onConflict: 'user_id,prompt_key' } ); if (error) throw new Error(error.message); -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 902757a5..167474f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,7 +58,7 @@ importers: specifier: ^7.5.0 version: 7.5.0 lucide-react: - specifier: ^1.27.0 + specifier: '>=1.27.0' version: 1.27.0(react@19.2.7) next: specifier: 16.2.10 @@ -76,7 +76,7 @@ importers: specifier: ^9.5.12 version: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) simple-icons: - specifier: ^16.27.1 + specifier: '>=16.27.1' version: 16.27.1 sonner: specifier: ^2.0.7 From ae9126ee1e001cc57bf2d6851ab99ceec44d11cf Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 22:47:27 +0200 Subject: [PATCH 43/98] =?UTF-8?q?fix(auth):=20requireAuth=20n'est=20plus?= =?UTF-8?q?=20publi=C3=A9=20comme=20Server=20Action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/auth.ts portait 'use server', donc requireAuth() était compilé comme Server Action et exposé en endpoint POST public alors qu'il n'est appelé que depuis des Server Components (settings, profil, onboarding). Remplacé par le garde 'server-only' : même comportement côté rendu, plus d'endpoint généré. --- lib/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/auth.ts b/lib/auth.ts index abdcdb66..1c9c3522 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,4 +1,4 @@ -'use server'; +import 'server-only'; import { redirect } from 'next/navigation'; import { getUserContext } from '@/lib/supabase/auth-helpers'; From 92cd8309f7d4a43a2abbcfde6e577b7e0aa697c0 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 22:48:16 +0200 Subject: [PATCH 44/98] refactor(archi): les lectures quittent les fichiers 'use server' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tout export d'un fichier 'use server' est compilé en Server Action, donc en endpoint POST public. Les fichiers app/actions/* mélangeaient mutations et requêtes de lecture : 27 fonctions get*/fetch* n'étaient appelées que depuis des Server Components mais étaient malgré tout publiées. - nouvelle couche lib/data/* (episodes, friends, notifications, playlists, profile, recommendations, reviews, watchlist) gardée par 'server-only' - app/actions/* ne conserve que les mutations, plus 4 lectures réellement pilotées par le client : fetchMoreMedia, getUserReviews (pagination), getNotifications (panneau à la demande), getUnreadCount (resync realtime) - les Server Components importent désormais lib/data/* - les préférences de notification sont rendues côté serveur au lieu d'être chargées dans un useEffect : plus de flash des toggles par défaut Surface exposée : 78 -> 51 fonctions. Aucun changement de comportement pour les mutations optimistes (épisodes, watchlist, notes) qui restent client. --- app/[lang]/(protected)/dashboard/page.tsx | 2 +- app/[lang]/(protected)/library/page.tsx | 4 +- app/[lang]/(protected)/notifications/page.tsx | 2 +- .../(protected)/profile/[username]/page.tsx | 8 +- app/[lang]/(protected)/settings/page.tsx | 20 +- app/[lang]/movie/[id]/page.tsx | 4 +- app/[lang]/playlist/[id]/page.tsx | 4 +- app/[lang]/tv/[id]/page.tsx | 6 +- .../tv/[id]/season/[seasonNumber]/page.tsx | 4 +- app/actions/episodes.ts | 75 +----- app/actions/friends.ts | 143 ----------- app/actions/media.ts | 2 +- app/actions/notifications.ts | 46 +--- app/actions/playlists.ts | 71 +----- app/actions/profile.ts | 54 ---- app/actions/prompts.ts | 25 +- app/actions/recommendations.ts | 42 +--- app/actions/reviews.ts | 230 +----------------- app/actions/watchlist.ts | 64 +---- .../media/reviews/PublicReviewsSection.tsx | 2 +- components/settings/NotificationSettings.tsx | 29 +-- components/settings/SettingsContent.tsx | 9 +- lib/data/continue-watching.ts | 2 + lib/data/episodes.ts | 75 ++++++ lib/data/friends.ts | 146 +++++++++++ lib/data/notifications.ts | 48 ++++ lib/data/playlists.ts | 71 ++++++ lib/data/profile.ts | 57 +++++ lib/data/prompts.ts | 26 +- lib/data/recommendations.ts | 41 ++++ lib/data/reviews.ts | 230 ++++++++++++++++++ lib/data/watchlist.ts | 76 +++++- package.json | 1 + pnpm-lock.yaml | 11 + 34 files changed, 854 insertions(+), 776 deletions(-) create mode 100644 lib/data/episodes.ts create mode 100644 lib/data/friends.ts create mode 100644 lib/data/notifications.ts create mode 100644 lib/data/playlists.ts create mode 100644 lib/data/profile.ts create mode 100644 lib/data/recommendations.ts create mode 100644 lib/data/reviews.ts diff --git a/app/[lang]/(protected)/dashboard/page.tsx b/app/[lang]/(protected)/dashboard/page.tsx index 14771f9b..2635c846 100644 --- a/app/[lang]/(protected)/dashboard/page.tsx +++ b/app/[lang]/(protected)/dashboard/page.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import { Suspense } from 'react'; import type { Metadata } from 'next'; -import { getUserWatchlist } from '@/app/actions/watchlist'; +import { getUserWatchlist } from '@/lib/data/watchlist'; import { getMovieDetails, getTvShowDetails, diff --git a/app/[lang]/(protected)/library/page.tsx b/app/[lang]/(protected)/library/page.tsx index 1a112995..f1a095ad 100644 --- a/app/[lang]/(protected)/library/page.tsx +++ b/app/[lang]/(protected)/library/page.tsx @@ -1,8 +1,8 @@ import { Skeleton } from '@/components/ui/skeleton'; import { Suspense } from 'react'; -import { getAllTvShowsWatchProgress } from '@/app/actions/episodes'; +import { getAllTvShowsWatchProgress } from '@/lib/data/episodes'; import { getCachedUserWatchlist } from '@/lib/data/watchlist'; -import { getMyReviewRatings } from '@/app/actions/reviews'; +import { getMyReviewRatings } from '@/lib/data/reviews'; import { LibraryView, LibraryLiveSubtitle, diff --git a/app/[lang]/(protected)/notifications/page.tsx b/app/[lang]/(protected)/notifications/page.tsx index 8a5d1820..4de7cbe9 100644 --- a/app/[lang]/(protected)/notifications/page.tsx +++ b/app/[lang]/(protected)/notifications/page.tsx @@ -1,6 +1,6 @@ import { getTranslations } from '@/lib/i18n/server'; import { PageLayout, PageHeader } from '@/components/layout/PageLayout'; -import { getNotifications } from '@/app/actions/notifications'; +import { getNotifications } from '@/lib/data/notifications'; import { NotificationsList } from '@/components/notifications/NotificationsList'; import { BASE_URL, buildPageMetadata } from '@/lib/metadata'; import type { Language } from '@/lib/i18n/translations'; diff --git a/app/[lang]/(protected)/profile/[username]/page.tsx b/app/[lang]/(protected)/profile/[username]/page.tsx index 4ed86674..61410b10 100644 --- a/app/[lang]/(protected)/profile/[username]/page.tsx +++ b/app/[lang]/(protected)/profile/[username]/page.tsx @@ -15,14 +15,14 @@ import { ProfileOptionsMenu } from '@/components/profile/ProfileOptionsMenu'; import { getProfileByUsername, getPrivacySettings, -} from '@/app/actions/profile'; -import { getUserReviews, getUserReviewRatings } from '@/app/actions/reviews'; -import { getUserPlaylists } from '@/app/actions/playlists'; +} from '@/lib/data/profile'; +import { getUserReviews, getUserReviewRatings } from '@/lib/data/reviews'; +import { getUserPlaylists } from '@/lib/data/playlists'; import { getFriendsWithProfiles, getFriendshipStatus, getPendingRequestsWithProfiles, -} from '@/app/actions/friends'; +} from '@/lib/data/friends'; import { getGenres } from '@/lib/tmdb'; import { ListMetadataBackfill } from '@/components/library/ListMetadataBackfill'; import type { WatchlistEntry } from '@/types/tmdb'; diff --git a/app/[lang]/(protected)/settings/page.tsx b/app/[lang]/(protected)/settings/page.tsx index ceb83d48..9ab48042 100644 --- a/app/[lang]/(protected)/settings/page.tsx +++ b/app/[lang]/(protected)/settings/page.tsx @@ -10,7 +10,8 @@ import { getTranslations } from '@/lib/i18n/server'; import { USER_PROFILE_COLUMNS, PRIVACY_COLUMNS } from '@/lib/supabase/columns'; import { getAvailableProviders } from '@/lib/tmdb'; import { getUserRegion } from '@/lib/tmdb/client'; -import { getMyStreamingProviders } from '@/app/actions/recommendations'; +import { getMyStreamingProviders } from '@/lib/data/recommendations'; +import { getNotificationPreferences } from '@/lib/data/notifications'; import type { Language } from '@/lib/i18n/translations'; import type { UserProfile, PrivacySettings } from '@/types/profile'; import type { User } from '@supabase/supabase-js'; @@ -45,8 +46,13 @@ async function SettingsSection({ }) { const supabase = await createClient(); - const [profileResult, privacyResult, region, selectedProviderIds] = - await Promise.all([ + const [ + profileResult, + privacyResult, + region, + selectedProviderIds, + notificationPreferences, + ] = await Promise.all([ supabase .from('user_profiles') .select(USER_PROFILE_COLUMNS) @@ -57,9 +63,10 @@ async function SettingsSection({ .select(PRIVACY_COLUMNS) .eq('user_id', user.id) .maybeSingle(), - getUserRegion(lang), - getMyStreamingProviders(), - ]); + getUserRegion(lang), + getMyStreamingProviders(), + getNotificationPreferences(), + ]); const streamingProviders = await getAvailableProviders(region, lang); @@ -75,6 +82,7 @@ async function SettingsSection({ isOAuthOnly={isOAuthOnly(user)} streamingProviders={streamingProviders} selectedProviderIds={selectedProviderIds} + notificationPreferences={notificationPreferences} initialTab={initialTab} /> ); diff --git a/app/[lang]/movie/[id]/page.tsx b/app/[lang]/movie/[id]/page.tsx index eafa7f60..1e28ddab 100644 --- a/app/[lang]/movie/[id]/page.tsx +++ b/app/[lang]/movie/[id]/page.tsx @@ -25,8 +25,8 @@ import { WatchActionsSkeleton, } from '@/components/media/detail/MediaDetailSkeleton'; import { PublicReviewsSection } from '@/components/media/reviews/PublicReviewsSection'; -import { getMediaWatchlistEntry } from '@/app/actions/watchlist'; -import { getAverageRating, getMediaReview } from '@/app/actions/reviews'; +import { getMediaWatchlistEntry } from '@/lib/data/watchlist'; +import { getAverageRating, getMediaReview } from '@/lib/data/reviews'; import { CommunityRatingBadge } from '@/components/media/detail/CommunityRatingBadge'; import { MediaCommunityRating } from '@/components/media/detail/MediaCommunityRating'; import { filterTrailers, buildMediaDetailMetadata } from '@/lib/media-detail'; diff --git a/app/[lang]/playlist/[id]/page.tsx b/app/[lang]/playlist/[id]/page.tsx index e71bf18b..f99aa0aa 100644 --- a/app/[lang]/playlist/[id]/page.tsx +++ b/app/[lang]/playlist/[id]/page.tsx @@ -8,9 +8,9 @@ import { EmptyState } from '@/components/ui/EmptyState'; import { PlaylistHero } from '@/components/profile/PlaylistHero'; import { PlaylistItemsView } from '@/components/profile/PlaylistItemsView'; import { PlaylistPageSkeleton } from '@/components/profile/PlaylistPageSkeleton'; -import { getPlaylistById } from '@/app/actions/playlists'; +import { getPlaylistById } from '@/lib/data/playlists'; import { getPublicPlaylistMeta } from '@/lib/playlists/public-metadata'; -import { getUserReviewRatings } from '@/app/actions/reviews'; +import { getUserReviewRatings } from '@/lib/data/reviews'; import { playlistItemToMediaItem } from '@/lib/mappers'; import { getGenres } from '@/lib/tmdb'; import { getImageUrl } from '@/lib/tmdb/images'; diff --git a/app/[lang]/tv/[id]/page.tsx b/app/[lang]/tv/[id]/page.tsx index a9f47463..6668668e 100644 --- a/app/[lang]/tv/[id]/page.tsx +++ b/app/[lang]/tv/[id]/page.tsx @@ -28,9 +28,9 @@ import { SeasonCardSkeleton } from '@/components/media/tv/SeasonCardSkeleton'; import { TvWatchSummary } from '@/components/media/tv/TvWatchSummary'; import { SectionHeading } from '@/components/ui/SectionHeading'; import { PublicReviewsSection } from '@/components/media/reviews/PublicReviewsSection'; -import { getMediaWatchlistEntry } from '@/app/actions/watchlist'; -import { getTvShowWatchProgress } from '@/app/actions/episodes'; -import { getShowAverageRating, getMediaReview } from '@/app/actions/reviews'; +import { getMediaWatchlistEntry } from '@/lib/data/watchlist'; +import { getTvShowWatchProgress } from '@/lib/data/episodes'; +import { getShowAverageRating, getMediaReview } from '@/lib/data/reviews'; import { CommunityRatingBadge } from '@/components/media/detail/CommunityRatingBadge'; import { MediaCommunityRating } from '@/components/media/detail/MediaCommunityRating'; import { filterTrailers, buildMediaDetailMetadata } from '@/lib/media-detail'; diff --git a/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx b/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx index 9afad812..2d37bd61 100644 --- a/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx +++ b/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx @@ -9,12 +9,12 @@ import { } from '@/lib/tmdb'; import { getServerLocale, getTranslations } from '@/lib/i18n/server'; import type { Language } from '@/lib/i18n/translations'; -import { getSeasonEpisodeWatches } from '@/app/actions/episodes'; +import { getSeasonEpisodeWatches } from '@/lib/data/episodes'; import { getSeasonAverageRating, getPublicEpisodeReviews, getMyEpisodeReviews, -} from '@/app/actions/reviews'; +} from '@/lib/data/reviews'; import { SeasonBanner } from '@/components/media/tv/SeasonBanner'; import { SeasonWatchButton } from '@/components/media/tv/SeasonWatchButton'; import { MediaActionsBar } from '@/components/media/detail/MediaActionsBar'; diff --git a/app/actions/episodes.ts b/app/actions/episodes.ts index 4b1776a8..59f85c2a 100644 --- a/app/actions/episodes.ts +++ b/app/actions/episodes.ts @@ -1,15 +1,11 @@ 'use server'; -import { - getAuthenticatedUser, - getOptionalUser, -} from '@/lib/supabase/auth-helpers'; +import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; import { SHARED_REVALIDATE_PATHS, revalidateLocalizedAfterResponse, } from '@/app/actions/_helpers'; import { getCachedTvShowDetails } from '@/lib/tmdb/cached'; -import { fetchAllRows } from '@/lib/supabase/pagination'; import { reportCritical, reportSwallowed } from '@/lib/report'; import type { SupabaseServerClient } from '@/lib/supabase/server'; import type { WatchStatus } from '@/types/tmdb'; @@ -361,72 +357,3 @@ export async function setSeasonWatched( addedToWatchlist: previousStatus === 'none' && tvStatus !== 'none', }; } - -/** - * Returns the set of watched episode numbers for a given season. - * Returns an empty set for unauthenticated users. - * - * @param tvId - TMDB TV show ID. - * @param seasonNumber - Season number (1-based). - * @returns Set of watched episode numbers. - */ -export async function getSeasonEpisodeWatches( - tvId: number, - seasonNumber: number -): Promise> { - const { supabase, userId } = await getOptionalUser(); - - if (!userId) return new Set(); - - const { data: watches } = await supabase - .from('episode_watches') - .select('episode_number') - .eq('user_id', userId) - .eq('tv_id', tvId) - .eq('season_number', seasonNumber); - - return new Set((watches ?? []).map((w) => w.episode_number)); -} - -export async function getTvShowWatchProgress( - tvId: number -): Promise> { - const { supabase, userId } = await getOptionalUser(); - - if (!userId) return new Map(); - - const watches = await fetchAllRows((from, to) => - supabase - .from('episode_watches') - .select('season_number, episode_number') - .eq('user_id', userId) - .eq('tv_id', tvId) - .order('season_number') - .order('episode_number') - .range(from, to) - ); - - const progress = new Map(); - for (const w of watches) { - progress.set(w.season_number, (progress.get(w.season_number) ?? 0) + 1); - } - return progress; -} - -export async function getAllTvShowsWatchProgress( - tvIds: number[] -): Promise> { - if (tvIds.length === 0) return {}; - - const { supabase, userId } = await getOptionalUser(); - if (!userId) return {}; - - const { data: counts } = await supabase.rpc('episode_watch_counts'); - - const wanted = new Set(tvIds); - const totals: Record = {}; - for (const row of counts ?? []) { - if (wanted.has(row.tv_id)) totals[row.tv_id] = row.watched_count; - } - return totals; -} diff --git a/app/actions/friends.ts b/app/actions/friends.ts index 0d643ee1..80814332 100644 --- a/app/actions/friends.ts +++ b/app/actions/friends.ts @@ -1,129 +1,9 @@ 'use server'; import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; -import { createAdminClient } from '@/lib/supabase/server'; -import { getPrivacySettings } from '@/app/actions/profile'; -import { FRIENDSHIP_COLUMNS } from '@/lib/supabase/columns'; -import { fetchAllRows } from '@/lib/supabase/pagination'; -import { resolveAvatarUrl } from '@/lib/avatar'; import { revalidateProfileAfterResponse } from '@/app/actions/_helpers'; import { sendFriendPush } from '@/lib/push/notify-friend'; import { after } from 'next/server'; -import type { - Friendship, - FriendEntry, - PendingRequestEntry, -} from '@/types/profile'; - -/** - * Returns pending friend requests addressed to the authenticated user, enriched with profile data. - * - * @returns Array of PendingRequestEntry records with username and optional avatar/fullName. - */ -export async function getPendingRequestsWithProfiles(): Promise< - PendingRequestEntry[] -> { - const { supabase, userId } = await getAuthenticatedUser(); - - const rawPending = await fetchAllRows((from, to) => - supabase - .from('friendships') - .select(FRIENDSHIP_COLUMNS) - .eq('addressee_id', userId) - .eq('status', 'pending') - .order('id') - .range(from, to) - ); - - if (rawPending.length === 0) return []; - - const pending = rawPending as Friendship[]; - const requesterIds = pending.map((f) => f.requester_id); - - const { data: profiles } = await supabase - .from('user_profiles') - .select('user_id, username, avatar_url, full_name') - .in('user_id', requesterIds); - - const profileByUserId = new Map(profiles?.map((p) => [p.user_id, p]) ?? []); - - const entries: PendingRequestEntry[] = []; - for (const f of pending) { - const profile = profileByUserId.get(f.requester_id); - if (!profile) continue; - entries.push({ - friendship: f, - username: profile.username, - avatarUrl: resolveAvatarUrl(profile.avatar_url, null) ?? undefined, - fullName: profile.full_name ?? undefined, - }); - } - - return entries; -} - -/** - * Returns accepted friendships for a given user, enriched with profile data and avatars. - * - * @param userId - Supabase user ID of the profile owner. - * @returns Array of FriendEntry records with username, avatarUrl, and fullName. - */ -export async function getFriendsWithProfiles( - userId: string -): Promise { - const { userId: viewerId } = await getAuthenticatedUser(); - - if (viewerId !== userId) { - const { friends_visibility } = await getPrivacySettings(userId); - if (friends_visibility === 'private') return []; - if (friends_visibility === 'friends') { - const friendship = await getFriendshipStatus(userId); - if (friendship?.status !== 'accepted') return []; - } - } - - const supabase = createAdminClient(); - - const rawFriends = await fetchAllRows((from, to) => - supabase - .from('friendships') - .select(FRIENDSHIP_COLUMNS) - .or(`requester_id.eq.${userId},addressee_id.eq.${userId}`) - .eq('status', 'accepted') - .order('id') - .range(from, to) - ); - - if (rawFriends.length === 0) return []; - - const friends = rawFriends as Friendship[]; - const friendUserIds = friends.map((f) => - f.requester_id === userId ? f.addressee_id : f.requester_id - ); - - const { data: profiles } = await supabase - .from('user_profiles') - .select('user_id, username, avatar_url, full_name') - .in('user_id', friendUserIds); - - const profileByUserId = new Map(profiles?.map((p) => [p.user_id, p]) ?? []); - - const entries: FriendEntry[] = []; - for (const f of friends) { - const otherId = - f.requester_id === userId ? f.addressee_id : f.requester_id; - const profile = profileByUserId.get(otherId); - if (!profile) continue; - entries.push({ - friendship: f, - username: profile.username, - avatarUrl: resolveAvatarUrl(profile.avatar_url, null) ?? undefined, - fullName: profile.full_name ?? undefined, - }); - } - - return entries; -} /** * Cancels a pending friend request sent by the authenticated user. @@ -149,29 +29,6 @@ export async function cancelFriendRequest( revalidateProfileAfterResponse(supabase, user, addresseeId); } -/** - * Returns the friendship record between the authenticated user and a target user, or null. - * - * @param targetUserId - Supabase user ID of the other party. - * @returns Friendship or null. - */ -export async function getFriendshipStatus( - targetUserId: string -): Promise { - const { supabase, userId } = await getAuthenticatedUser(); - - const { data } = await supabase - .from('friendships') - .select(FRIENDSHIP_COLUMNS) - .or( - `and(requester_id.eq.${userId},addressee_id.eq.${targetUserId}),` + - `and(requester_id.eq.${targetUserId},addressee_id.eq.${userId})` - ) - .maybeSingle(); - - return (data as Friendship) ?? null; -} - /** * Sends a friend request from the authenticated user to another user. * diff --git a/app/actions/media.ts b/app/actions/media.ts index 10e71587..b5967a40 100644 --- a/app/actions/media.ts +++ b/app/actions/media.ts @@ -21,7 +21,7 @@ import { } from '@/lib/tmdb'; import type { MediaItem, MediaType, WatchlistEntry } from '@/types/tmdb'; import { getMediaKey } from '@/lib/media'; -import { getMediaWatchlistEntries } from './watchlist'; +import { getMediaWatchlistEntries } from '@/lib/data/watchlist'; /** * Enriches a list of media items with their corresponding watchlist entries for the current user. diff --git a/app/actions/notifications.ts b/app/actions/notifications.ts index 3eafd3b3..d057f472 100644 --- a/app/actions/notifications.ts +++ b/app/actions/notifications.ts @@ -2,39 +2,23 @@ import { revalidateLocalizedAfterResponse } from '@/app/actions/_helpers'; import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; -import { rowToAppNotification } from '@/lib/notifications'; import { - DEFAULT_NOTIFICATION_PREFERENCES, - type AppNotification, - type NotificationPreferences, + getNotifications as readNotifications, + getUnreadCount as readUnreadCount, +} from '@/lib/data/notifications'; +import type { + AppNotification, + NotificationPreferences, } from '@/types/notifications'; -const NOTIFICATION_COLUMNS = - 'id, type, sender_username, media_id, media_type, media_title, poster_path, season_number, episode_number, url, read_at, created_at'; - -/** Returns the authenticated user's notifications, newest first. */ +/** Lazy-loaded notification list for the dropdown panel, which only opens on demand. */ export async function getNotifications(limit = 30): Promise { - const { supabase, userId } = await getAuthenticatedUser(); - const { data, error } = await supabase - .from('notifications') - .select(NOTIFICATION_COLUMNS) - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .limit(limit); - if (error) throw new Error(error.message); - return (data ?? []).map(rowToAppNotification); + return readNotifications(limit); } -/** Returns the count of unread notifications for the authenticated user. */ +/** Badge count resynced by the client after a realtime update or delete. */ export async function getUnreadCount(): Promise { - const { supabase, userId } = await getAuthenticatedUser(); - const { count, error } = await supabase - .from('notifications') - .select('id', { count: 'exact', head: true }) - .eq('user_id', userId) - .is('read_at', null); - if (error) throw new Error(error.message); - return count ?? 0; + return readUnreadCount(); } export async function markNotificationRead(id: string): Promise { @@ -71,16 +55,6 @@ export async function deleteNotification(id: string): Promise { revalidateLocalizedAfterResponse(['/notifications']); } -export async function getNotificationPreferences(): Promise { - const { supabase, userId } = await getAuthenticatedUser(); - const { data } = await supabase - .from('notification_preferences') - .select('friend_requests, friend_accepted, new_episodes, suggestions') - .eq('user_id', userId) - .maybeSingle(); - return data ?? DEFAULT_NOTIFICATION_PREFERENCES; -} - export async function updateNotificationPreferences( prefs: NotificationPreferences ): Promise { diff --git a/app/actions/playlists.ts b/app/actions/playlists.ts index 11e877e9..e87260b1 100644 --- a/app/actions/playlists.ts +++ b/app/actions/playlists.ts @@ -1,82 +1,15 @@ 'use server'; -import { - getAuthenticatedUser, - getOptionalUser, -} from '@/lib/supabase/auth-helpers'; +import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; import { getTranslations } from '@/lib/i18n/server'; -import { fetchAllRows } from '@/lib/supabase/pagination'; import { revalidateProfileAfterResponse, revalidatePlaylistMetaAfterResponse, } from '@/app/actions/_helpers'; import { parseVisibility, isVisibility } from '@/lib/privacy'; import { getListMediaMetadata } from '@/lib/tmdb'; -import { resolveAvatarUrl } from '@/lib/avatar'; import type { MediaType } from '@/types/tmdb'; -import type { Playlist, PrivacyVisibility } from '@/types/profile'; - -/** - * Returns all playlists for a given user visible to the current viewer, newest first. - * RLS automatically filters by per-playlist visibility. - * - * @param userId - Supabase user ID. - * @returns Array of Playlist objects with nested items. - */ -export async function getUserPlaylists(userId: string): Promise { - const { supabase } = await getAuthenticatedUser(); - - const data = await fetchAllRows((from, to) => - supabase - .from('playlists') - .select('*, items:playlist_items(*)') - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .order('id') - .range(from, to) - ); - - return data as Playlist[]; -} - -/** - * Returns a single playlist with items and owner username, or null if inaccessible. - * RLS enforces per-playlist visibility for the current viewer (anon or authenticated). - * - * @param id - UUID of the playlist. - */ -export async function getPlaylistById(id: string): Promise<{ - playlist: Playlist; - isOwn: boolean; - ownerUsername: string | null; - ownerAvatarUrl: string | null; -} | null> { - const { supabase, userId } = await getOptionalUser(); - - const { data } = await supabase - .from('playlists') - .select('*, items:playlist_items(*)') - .eq('id', id) - .maybeSingle(); - - if (!data) return null; - - const { data: ownerProfile } = await supabase - .from('user_profiles') - .select('username, avatar_url') - .eq('user_id', data.user_id) - .maybeSingle(); - - const ownerUsername = ownerProfile?.username ?? null; - const ownerAvatarUrl = resolveAvatarUrl(ownerProfile?.avatar_url, null); - - return { - playlist: data as Playlist, - isOwn: userId === data.user_id, - ownerUsername, - ownerAvatarUrl, - }; -} +import type { PrivacyVisibility } from '@/types/profile'; /** * Creates a new playlist for the authenticated user. diff --git a/app/actions/profile.ts b/app/actions/profile.ts index 19778e54..74203966 100644 --- a/app/actions/profile.ts +++ b/app/actions/profile.ts @@ -1,7 +1,6 @@ 'use server'; import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; -import { createClient } from '@/lib/supabase/server'; import { formStr } from '@/lib/validators'; import { getTranslations } from '@/lib/i18n/server'; import { @@ -9,32 +8,6 @@ import { revalidateLocalizedAfterResponse, } from '@/app/actions/_helpers'; import { parseVisibility } from '@/lib/privacy'; -import { USER_PROFILE_COLUMNS, PRIVACY_COLUMNS } from '@/lib/supabase/columns'; -import type { - PrivacySettings, - PrivacyDefaults, - UserProfile, -} from '@/types/profile'; - -/** - * Returns the public profile for a given username (case-insensitive), or null if not found. - * - * @param username - The profile's username slug. - * @returns UserProfile or null. - */ -export async function getProfileByUsername( - username: string -): Promise { - const supabase = await createClient(); - - const { data } = await supabase - .from('user_profiles') - .select(USER_PROFILE_COLUMNS) - .ilike('username', username) - .maybeSingle(); - - return data ?? null; -} export async function updateSocialLinks( prevState: unknown, @@ -115,33 +88,6 @@ export async function updateSocialLinks( }; } -/** - * Returns privacy settings for a given user, falling back to all-public defaults. - * - * @param userId - Supabase user ID. - * @returns PrivacySettings object. - */ -export async function getPrivacySettings( - userId: string -): Promise { - const supabase = await createClient(); - - const { data } = await supabase - .from('privacy_settings') - .select(PRIVACY_COLUMNS) - .eq('user_id', userId) - .maybeSingle(); - - const fallback: PrivacyDefaults = { - watchlist_visibility: 'public', - watched_visibility: 'public', - reviews_visibility: 'public', - playlists_visibility: 'public', - friends_visibility: 'public', - }; - return (data as PrivacySettings) ?? { user_id: userId, ...fallback }; -} - export async function updatePrivacySettings( prevState: unknown, formData: FormData diff --git a/app/actions/prompts.ts b/app/actions/prompts.ts index effc464e..b973992b 100644 --- a/app/actions/prompts.ts +++ b/app/actions/prompts.ts @@ -1,36 +1,13 @@ 'use server'; -import { - getAuthenticatedUser, - getOptionalUser, -} from '@/lib/supabase/auth-helpers'; +import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; import { ACCOUNT_PROMPTS, - isPromptKey, isPromptState, type PromptKey, type PromptState, - type PromptStates, } from '@/lib/prompts/keys'; -/** Answers already given to account-scoped call-to-actions, so none is ever asked twice. */ -export async function getPromptStates(): Promise { - const { supabase, userId } = await getOptionalUser(); - if (!userId) return {}; - - const { data } = await supabase - .from('user_prompts') - .select('prompt_key, state') - .eq('user_id', userId); - - const states: PromptStates = {}; - for (const row of data ?? []) { - if (isPromptKey(row.prompt_key) && isPromptState(row.state)) - states[row.prompt_key] = row.state; - } - return states; -} - /** Records the user's answer to a call-to-action; deliberately never revalidates. */ export async function resolvePrompt( key: PromptKey, diff --git a/app/actions/recommendations.ts b/app/actions/recommendations.ts index 38ddcf45..b9e4ebea 100644 --- a/app/actions/recommendations.ts +++ b/app/actions/recommendations.ts @@ -1,9 +1,6 @@ 'use server'; -import { - getAuthenticatedUser, - getOptionalUser, -} from '@/lib/supabase/auth-helpers'; +import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; import { revalidateLocalizedAfterResponse } from '@/app/actions/_helpers'; import { VALID_MEDIA_TYPES } from '@/lib/validators'; import type { MediaType } from '@/types/tmdb'; @@ -11,12 +8,6 @@ import type { MediaType } from '@/types/tmdb'; const MAX_GENRE_IDS = 10; const MAX_PROVIDER_IDS = 100; -export interface RecommendationDismissal { - media_id: number; - media_type: MediaType; - genre_ids: number[]; -} - /** * Marks a recommended title as "not interested": it never comes back and its * genres feed the negative signal of the recommendation engine. @@ -51,37 +42,6 @@ export async function dismissRecommendation( revalidateLocalizedAfterResponse(['/dashboard']); } -/** Returns every title the user dismissed from recommendations. */ -export async function getMyDismissals(): Promise { - const { supabase, userId } = await getOptionalUser(); - if (!userId) return []; - - const { data } = await supabase - .from('recommendation_dismissals') - .select('media_id, media_type, genre_ids') - .eq('user_id', userId); - - return (data ?? []).map((row) => ({ - media_id: row.media_id, - media_type: row.media_type as MediaType, - genre_ids: row.genre_ids, - })); -} - -/** Returns the TMDB provider IDs of the user's streaming services. */ -export async function getMyStreamingProviders(): Promise { - const { supabase, userId } = await getOptionalUser(); - if (!userId) return []; - - const { data } = await supabase - .from('user_streaming_providers') - .select('provider_ids') - .eq('user_id', userId) - .maybeSingle(); - - return data?.provider_ids ?? []; -} - /** Replaces the user's streaming services selection. */ export async function updateStreamingProviders( providerIds: number[] diff --git a/app/actions/reviews.ts b/app/actions/reviews.ts index 1fd0d364..08159edc 100644 --- a/app/actions/reviews.ts +++ b/app/actions/reviews.ts @@ -1,13 +1,8 @@ 'use server'; -import { - getAuthenticatedUser, - getOptionalUser, -} from '@/lib/supabase/auth-helpers'; -import { createClient } from '@/lib/supabase/server'; -import { fetchAllRows } from '@/lib/supabase/pagination'; -import { reportSwallowed } from '@/lib/report'; +import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; import { getTranslations } from '@/lib/i18n/server'; +import { getUserReviews as readUserReviewsPage } from '@/lib/data/reviews'; import { validateRating, validateReviewContent, @@ -15,229 +10,18 @@ import { } from '@/lib/validators'; import { revalidateProfileAfterResponse } from '@/app/actions/_helpers'; import { MAX_REVIEW_LENGTH } from '@/types/profile'; -import type { - Review, - PublicReview, - ReviewMediaType, - UserReviewsPage, -} from '@/types/profile'; +import type { Review, ReviewMediaType, UserReviewsPage } from '@/types/profile'; import { REVIEW_COLUMNS } from '@/lib/supabase/columns'; -function parseRatingRow(data: unknown): { avg: number; count: number } | null { - const row = - (data as Array<{ avg: string | null; count: string }> | null)?.[0] ?? - null; - if (!row || row.avg === null) return null; - return { avg: Number(row.avg), count: Number(row.count) }; -} - -const REVIEWS_PAGE_SIZE = 20; -const REVIEWS_MAX_PAGE_SIZE = 50; - /** - * Returns a page of reviews for a given user, newest first. - * Pass nextCursor from the previous page to get the next batch. + * Cursor-paginated page of a user's reviews, for the profile list's "load more" button. + * Only review read reachable from the client — every other read is server-rendered. */ export async function getUserReviews( userId: string, - cursor?: string, - limit: number = REVIEWS_PAGE_SIZE + cursor?: string ): Promise { - const supabase = await createClient(); - const pageSize = Math.min(Math.max(1, limit), REVIEWS_MAX_PAGE_SIZE); - - let query = supabase - .from('reviews') - .select(REVIEW_COLUMNS) - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .order('id', { ascending: false }) - .limit(pageSize + 1); - - if (cursor) query = query.lt('created_at', cursor); - - const { data, error } = await query; - if (error) throw new Error(error.message); - - const rows = (data as Review[]) ?? []; - const hasMore = rows.length > pageSize; - const reviews = hasMore ? rows.slice(0, pageSize) : rows; - const nextCursor = hasMore ? reviews[reviews.length - 1].created_at : null; - - return { reviews, nextCursor }; -} - -/** - * Returns a `media_key → rating` map of a user's own movie/tv ratings, for sorting lists - * by user rating. Reviews RLS restricts direct reads to the user's own rows, so callers - * should only request the rating map of the list owner viewing their own profile. - * - * @param userId - Owner of the reviews. - */ -export async function getUserReviewRatings( - userId: string -): Promise> { - const supabase = await createClient(); - - const data = await fetchAllRows((from, to) => - supabase - .from('reviews') - .select('media_id, media_type, rating') - .eq('user_id', userId) - .in('media_type', ['movie', 'tv']) - .not('rating', 'is', null) - .order('media_type') - .order('media_id') - .range(from, to) - ).catch((error: unknown) => { - reportSwallowed('reviews:ratings', error); - return []; - }); - - const map: Record = {}; - for (const row of data) { - if (row.rating !== null) { - map[`${row.media_type}-${row.media_id}`] = row.rating; - } - } - return map; -} - -/** Returns the authenticated user's own `media_key → rating` map, or an empty map if signed out. */ -export async function getMyReviewRatings(): Promise> { - const { userId } = await getOptionalUser(); - if (!userId) return {}; - return getUserReviewRatings(userId); -} - -/** - * Returns the authenticated user's review for a specific media item, or null if none. - */ -export async function getMediaReview( - mediaId: number, - mediaType: ReviewMediaType -): Promise { - const { supabase, userId } = await getOptionalUser(); - if (!userId) return null; - - const { data } = await supabase - .from('reviews') - .select(REVIEW_COLUMNS) - .eq('user_id', userId) - .eq('media_id', mediaId) - .eq('media_type', mediaType) - .maybeSingle(); - - return (data as Review) ?? null; -} - -/** - * Returns the authenticated user's own episode reviews, keyed by episode ID, so a season - * page can render inline ratings without one query per episode. - */ -export async function getMyEpisodeReviews( - episodeIds: number[] -): Promise> { - if (episodeIds.length === 0) return {}; - const { supabase, userId } = await getOptionalUser(); - if (!userId) return {}; - - const { data } = await supabase - .from('reviews') - .select(REVIEW_COLUMNS) - .eq('user_id', userId) - .eq('media_type', 'episode') - .in('media_id', episodeIds); - - const byEpisodeId: Record = {}; - for (const review of (data ?? []) as Review[]) { - byEpisodeId[review.media_id] = review; - } - return byEpisodeId; -} - -/** - * Returns the community average rating (1–10 scale) and count for a media item. - * Returns null if no ratings exist. - */ -export async function getAverageRating( - mediaId: number, - mediaType: ReviewMediaType -): Promise<{ avg: number; count: number } | null> { - const supabase = await createClient(); - - const { data } = await supabase.rpc('get_media_rating', { - p_media_id: mediaId, - p_media_type: mediaType, - }); - return parseRatingRow(data); -} - -/** - * Returns the community average rating for a season, aggregated from its episode reviews. - * Returns null if no episode ratings exist for this season. - */ -export async function getSeasonAverageRating( - tvId: number, - seasonNumber: number -): Promise<{ avg: number; count: number } | null> { - const supabase = await createClient(); - - const { data } = await supabase.rpc('get_season_rating', { - p_tv_id: tvId, - p_season_number: seasonNumber, - }); - return parseRatingRow(data); -} - -/** - * Returns the community average rating for a show, aggregated from all its episode reviews. - * Returns null if no ratings exist. - */ -export async function getShowAverageRating( - tvId: number -): Promise<{ avg: number; count: number } | null> { - const supabase = await createClient(); - - const { data } = await supabase.rpc('get_show_rating', { p_tv_id: tvId }); - return parseRatingRow(data); -} - -/** - * Returns public reviews for a media item, filtered by the viewer's auth/friendship status. - */ -export async function getPublicReviews( - mediaId: number, - mediaType: ReviewMediaType -): Promise { - const { supabase, userId } = await getOptionalUser(); - - const { data, error } = await supabase.rpc('get_public_reviews', { - p_media_id: mediaId, - p_media_type: mediaType, - p_viewer_id: userId ?? undefined, - }); - - if (error) return []; - return (data as PublicReview[]) ?? []; -} - -/** - * Returns public reviews for a set of episode IDs, filtered by viewer's auth/friendship status. - */ -export async function getPublicEpisodeReviews( - episodeIds: number[] -): Promise { - if (episodeIds.length === 0) return []; - const { supabase, userId } = await getOptionalUser(); - - const { data, error } = await supabase.rpc('get_public_episode_reviews', { - p_episode_ids: episodeIds, - p_viewer_id: userId ?? undefined, - }); - - if (error) return []; - return (data as PublicReview[]) ?? []; + return readUserReviewsPage(userId, cursor); } /** diff --git a/app/actions/watchlist.ts b/app/actions/watchlist.ts index cc80c6cf..e98fbcef 100644 --- a/app/actions/watchlist.ts +++ b/app/actions/watchlist.ts @@ -1,18 +1,13 @@ 'use server'; -import { - getAuthenticatedUser, - getOptionalUser, -} from '@/lib/supabase/auth-helpers'; +import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; import { SHARED_REVALIDATE_PATHS, revalidateLocalizedAfterResponse, } from '@/app/actions/_helpers'; import { VALID_STATUSES, VALID_MEDIA_TYPES } from '@/lib/validators'; -import type { WatchStatus, WatchlistEntry, MediaType } from '@/types/tmdb'; -import { WATCHLIST_COLUMNS } from '@/lib/supabase/columns'; +import type { WatchStatus, MediaType } from '@/types/tmdb'; import type { Database } from '@/types/database'; -import { fetchAllRows } from '@/lib/supabase/pagination'; import { getListMediaMetadata, type ListMediaMetadata } from '@/lib/tmdb'; function revalidateWatchlistPaths(mediaType: MediaType, mediaId: number) { @@ -258,58 +253,3 @@ export async function removeFromWatchlist( revalidateWatchlistPaths(mediaType, mediaId); } - -export async function getUserWatchlist(): Promise { - const { supabase, userId } = await getOptionalUser(); - - if (!userId) return []; - - const entries = await fetchAllRows((from, to) => - supabase - .from('watchlist') - .select(WATCHLIST_COLUMNS) - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .order('id') - .range(from, to) - ); - - return entries as WatchlistEntry[]; -} - -export async function getMediaWatchlistEntry( - mediaId: number, - mediaType: MediaType -): Promise { - const { supabase, userId } = await getOptionalUser(); - - if (!userId) return null; - - const { data: entry } = await supabase - .from('watchlist') - .select(WATCHLIST_COLUMNS) - .eq('user_id', userId) - .eq('media_id', mediaId) - .eq('media_type', mediaType) - .maybeSingle(); - - return (entry as WatchlistEntry) ?? null; -} - -export async function getMediaWatchlistEntries( - mediaIds: number[] -): Promise { - const { supabase, userId } = await getOptionalUser(); - - if (!userId || mediaIds.length === 0) return []; - - const { data: entries, error } = await supabase - .from('watchlist') - .select(WATCHLIST_COLUMNS) - .eq('user_id', userId) - .in('media_id', mediaIds); - - if (error) throw new Error(error.message); - - return (entries as WatchlistEntry[]) ?? []; -} diff --git a/components/media/reviews/PublicReviewsSection.tsx b/components/media/reviews/PublicReviewsSection.tsx index 6f01650e..39d4e150 100644 --- a/components/media/reviews/PublicReviewsSection.tsx +++ b/components/media/reviews/PublicReviewsSection.tsx @@ -1,4 +1,4 @@ -import { getPublicReviews } from '@/app/actions/reviews'; +import { getPublicReviews } from '@/lib/data/reviews'; import { SectionHeading } from '@/components/ui/SectionHeading'; import { getTranslations } from '@/lib/i18n/server'; import { ReviewsList } from '@/components/media/reviews/ReviewsList'; diff --git a/components/settings/NotificationSettings.tsx b/components/settings/NotificationSettings.tsx index 106d9236..bc1e62f5 100644 --- a/components/settings/NotificationSettings.tsx +++ b/components/settings/NotificationSettings.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { Card, CardContent, @@ -9,16 +9,10 @@ import { CardTitle, } from '@/components/ui/card'; import { Switch } from '@/components/ui/switch'; -import { - getNotificationPreferences, - updateNotificationPreferences, -} from '@/app/actions/notifications'; +import { updateNotificationPreferences } from '@/app/actions/notifications'; import { useTranslation } from '@/lib/i18n/context'; import { usePushSubscription } from '@/hooks/usePushSubscription'; -import { - DEFAULT_NOTIFICATION_PREFERENCES, - type NotificationPreferences, -} from '@/types/notifications'; +import type { NotificationPreferences } from '@/types/notifications'; const KEYS = [ 'friend_requests', @@ -27,12 +21,17 @@ const KEYS = [ 'suggestions', ] as const; -export function NotificationSettings() { +interface NotificationSettingsProps { + initialPreferences: NotificationPreferences; +} + +export function NotificationSettings({ + initialPreferences, +}: NotificationSettingsProps) { const { t } = useTranslation(); const push = usePushSubscription(); - const [prefs, setPrefs] = useState( - DEFAULT_NOTIFICATION_PREFERENCES - ); + const [prefs, setPrefs] = + useState(initialPreferences); const pushHint = push.status === 'unsupported' @@ -41,10 +40,6 @@ export function NotificationSettings() { ? t.settings.notifications.iosHint : null; - useEffect(() => { - void getNotificationPreferences().then(setPrefs); - }, []); - const toggle = (key: (typeof KEYS)[number]) => { const next = { ...prefs, [key]: !prefs[key] }; setPrefs(next); diff --git a/components/settings/SettingsContent.tsx b/components/settings/SettingsContent.tsx index f8570e6a..279c0635 100644 --- a/components/settings/SettingsContent.tsx +++ b/components/settings/SettingsContent.tsx @@ -22,6 +22,7 @@ import type { PrivacySettings as PrivacySettingsType, } from '@/types/profile'; import type { WatchProvider } from '@/types/tmdb'; +import type { NotificationPreferences } from '@/types/notifications'; interface SettingsContentProps { user: User | null; @@ -30,6 +31,7 @@ interface SettingsContentProps { isOAuthOnly: boolean; streamingProviders: WatchProvider[]; selectedProviderIds: number[]; + notificationPreferences: NotificationPreferences; initialTab: SettingsTab; } @@ -40,6 +42,7 @@ export function SettingsContent({ isOAuthOnly, streamingProviders, selectedProviderIds, + notificationPreferences, initialTab, }: SettingsContentProps) { const [activeTab, setActiveTab] = useState(initialTab); @@ -66,7 +69,11 @@ export function SettingsContent({
)} - {activeTab === 'notifications' && } + {activeTab === 'notifications' && ( + + )} {activeTab === 'appearance' && (
diff --git a/lib/data/continue-watching.ts b/lib/data/continue-watching.ts index 9fe3787f..f86ad146 100644 --- a/lib/data/continue-watching.ts +++ b/lib/data/continue-watching.ts @@ -1,3 +1,5 @@ +import 'server-only'; + import { cache } from 'react'; import { getOptionalUser } from '@/lib/supabase/auth-helpers'; import { getWatchlistWithProgress } from '@/lib/data/watchlist'; diff --git a/lib/data/episodes.ts b/lib/data/episodes.ts new file mode 100644 index 00000000..7207f08b --- /dev/null +++ b/lib/data/episodes.ts @@ -0,0 +1,75 @@ +import 'server-only'; + +import { getOptionalUser } from '@/lib/supabase/auth-helpers'; +import { fetchAllRows } from '@/lib/supabase/pagination'; + +/** + * Returns the set of watched episode numbers for a given season. + * Returns an empty set for unauthenticated users. + * + * @param tvId - TMDB TV show ID. + * @param seasonNumber - Season number (1-based). + * @returns Set of watched episode numbers. + */ +export async function getSeasonEpisodeWatches( + tvId: number, + seasonNumber: number +): Promise> { + const { supabase, userId } = await getOptionalUser(); + + if (!userId) return new Set(); + + const { data: watches } = await supabase + .from('episode_watches') + .select('episode_number') + .eq('user_id', userId) + .eq('tv_id', tvId) + .eq('season_number', seasonNumber); + + return new Set((watches ?? []).map((w) => w.episode_number)); +} + +/** Watched episode count per season for one show, keyed by season number. */ +export async function getTvShowWatchProgress( + tvId: number +): Promise> { + const { supabase, userId } = await getOptionalUser(); + + if (!userId) return new Map(); + + const watches = await fetchAllRows((from, to) => + supabase + .from('episode_watches') + .select('season_number, episode_number') + .eq('user_id', userId) + .eq('tv_id', tvId) + .order('season_number') + .order('episode_number') + .range(from, to) + ); + + const progress = new Map(); + for (const w of watches) { + progress.set(w.season_number, (progress.get(w.season_number) ?? 0) + 1); + } + return progress; +} + +/** Total watched episode count per show, for the requested TMDB show IDs. */ +export async function getAllTvShowsWatchProgress( + tvIds: number[] +): Promise> { + if (tvIds.length === 0) return {}; + + const { supabase, userId } = await getOptionalUser(); + if (!userId) return {}; + + const { data: counts } = await supabase.rpc('episode_watch_counts'); + + const wanted = new Set(tvIds); + const totals: Record = {}; + for (const row of counts ?? []) { + if (wanted.has(row.tv_id)) totals[row.tv_id] = row.watched_count; + } + return totals; +} diff --git a/lib/data/friends.ts b/lib/data/friends.ts new file mode 100644 index 00000000..eb81b994 --- /dev/null +++ b/lib/data/friends.ts @@ -0,0 +1,146 @@ +import 'server-only'; + +import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; +import { createAdminClient } from '@/lib/supabase/server'; +import { getPrivacySettings } from '@/lib/data/profile'; +import { FRIENDSHIP_COLUMNS } from '@/lib/supabase/columns'; +import { fetchAllRows } from '@/lib/supabase/pagination'; +import { resolveAvatarUrl } from '@/lib/avatar'; +import type { + Friendship, + FriendEntry, + PendingRequestEntry, +} from '@/types/profile'; + +/** + * Returns the friendship record between the authenticated user and a target user, or null. + * + * @param targetUserId - Supabase user ID of the other party. + * @returns Friendship or null. + */ +export async function getFriendshipStatus( + targetUserId: string +): Promise { + const { supabase, userId } = await getAuthenticatedUser(); + + const { data } = await supabase + .from('friendships') + .select(FRIENDSHIP_COLUMNS) + .or( + `and(requester_id.eq.${userId},addressee_id.eq.${targetUserId}),` + + `and(requester_id.eq.${targetUserId},addressee_id.eq.${userId})` + ) + .maybeSingle(); + + return (data as Friendship) ?? null; +} + +/** + * Returns pending friend requests addressed to the authenticated user, enriched with profile data. + * + * @returns Array of PendingRequestEntry records with username and optional avatar/fullName. + */ +export async function getPendingRequestsWithProfiles(): Promise< + PendingRequestEntry[] +> { + const { supabase, userId } = await getAuthenticatedUser(); + + const rawPending = await fetchAllRows((from, to) => + supabase + .from('friendships') + .select(FRIENDSHIP_COLUMNS) + .eq('addressee_id', userId) + .eq('status', 'pending') + .order('id') + .range(from, to) + ); + + if (rawPending.length === 0) return []; + + const pending = rawPending as Friendship[]; + const requesterIds = pending.map((f) => f.requester_id); + + const { data: profiles } = await supabase + .from('user_profiles') + .select('user_id, username, avatar_url, full_name') + .in('user_id', requesterIds); + + const profileByUserId = new Map(profiles?.map((p) => [p.user_id, p]) ?? []); + + const entries: PendingRequestEntry[] = []; + for (const f of pending) { + const profile = profileByUserId.get(f.requester_id); + if (!profile) continue; + entries.push({ + friendship: f, + username: profile.username, + avatarUrl: resolveAvatarUrl(profile.avatar_url, null) ?? undefined, + fullName: profile.full_name ?? undefined, + }); + } + + return entries; +} + +/** + * Returns accepted friendships for a given user, enriched with profile data and avatars. + * + * @param userId - Supabase user ID of the profile owner. + * @returns Array of FriendEntry records with username, avatarUrl, and fullName. + */ +export async function getFriendsWithProfiles( + userId: string +): Promise { + const { userId: viewerId } = await getAuthenticatedUser(); + + if (viewerId !== userId) { + const { friends_visibility } = await getPrivacySettings(userId); + if (friends_visibility === 'private') return []; + if (friends_visibility === 'friends') { + const friendship = await getFriendshipStatus(userId); + if (friendship?.status !== 'accepted') return []; + } + } + + const supabase = createAdminClient(); + + const rawFriends = await fetchAllRows((from, to) => + supabase + .from('friendships') + .select(FRIENDSHIP_COLUMNS) + .or(`requester_id.eq.${userId},addressee_id.eq.${userId}`) + .eq('status', 'accepted') + .order('id') + .range(from, to) + ); + + if (rawFriends.length === 0) return []; + + const friends = rawFriends as Friendship[]; + const friendUserIds = friends.map((f) => + f.requester_id === userId ? f.addressee_id : f.requester_id + ); + + const { data: profiles } = await supabase + .from('user_profiles') + .select('user_id, username, avatar_url, full_name') + .in('user_id', friendUserIds); + + const profileByUserId = new Map(profiles?.map((p) => [p.user_id, p]) ?? []); + + const entries: FriendEntry[] = []; + for (const f of friends) { + const otherId = + f.requester_id === userId ? f.addressee_id : f.requester_id; + const profile = profileByUserId.get(otherId); + if (!profile) continue; + entries.push({ + friendship: f, + username: profile.username, + avatarUrl: resolveAvatarUrl(profile.avatar_url, null) ?? undefined, + fullName: profile.full_name ?? undefined, + }); + } + + return entries; +} diff --git a/lib/data/notifications.ts b/lib/data/notifications.ts new file mode 100644 index 00000000..5885dc02 --- /dev/null +++ b/lib/data/notifications.ts @@ -0,0 +1,48 @@ +import 'server-only'; + +import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; +import { rowToAppNotification } from '@/lib/notifications'; +import { + DEFAULT_NOTIFICATION_PREFERENCES, + type AppNotification, + type NotificationPreferences, +} from '@/types/notifications'; + +const NOTIFICATION_COLUMNS = + 'id, type, sender_username, media_id, media_type, media_title, poster_path, season_number, episode_number, url, read_at, created_at'; + +/** Returns the authenticated user's notifications, newest first. */ +export async function getNotifications(limit = 30): Promise { + const { supabase, userId } = await getAuthenticatedUser(); + const { data, error } = await supabase + .from('notifications') + .select(NOTIFICATION_COLUMNS) + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .limit(limit); + if (error) throw new Error(error.message); + return (data ?? []).map(rowToAppNotification); +} + +/** Returns the count of unread notifications for the authenticated user. */ +export async function getUnreadCount(): Promise { + const { supabase, userId } = await getAuthenticatedUser(); + const { count, error } = await supabase + .from('notifications') + .select('id', { count: 'exact', head: true }) + .eq('user_id', userId) + .is('read_at', null); + if (error) throw new Error(error.message); + return count ?? 0; +} + +/** Per-channel notification opt-ins, falling back to the all-on defaults. */ +export async function getNotificationPreferences(): Promise { + const { supabase, userId } = await getAuthenticatedUser(); + const { data } = await supabase + .from('notification_preferences') + .select('friend_requests, friend_accepted, new_episodes, suggestions') + .eq('user_id', userId) + .maybeSingle(); + return data ?? DEFAULT_NOTIFICATION_PREFERENCES; +} diff --git a/lib/data/playlists.ts b/lib/data/playlists.ts new file mode 100644 index 00000000..c7d25a9f --- /dev/null +++ b/lib/data/playlists.ts @@ -0,0 +1,71 @@ +import 'server-only'; + +import { + getAuthenticatedUser, + getOptionalUser, +} from '@/lib/supabase/auth-helpers'; +import { fetchAllRows } from '@/lib/supabase/pagination'; +import { resolveAvatarUrl } from '@/lib/avatar'; +import type { Playlist } from '@/types/profile'; + +/** + * Returns all playlists for a given user visible to the current viewer, newest first. + * RLS automatically filters by per-playlist visibility. + * + * @param userId - Supabase user ID. + * @returns Array of Playlist objects with nested items. + */ +export async function getUserPlaylists(userId: string): Promise { + const { supabase } = await getAuthenticatedUser(); + + const data = await fetchAllRows((from, to) => + supabase + .from('playlists') + .select('*, items:playlist_items(*)') + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .order('id') + .range(from, to) + ); + + return data as Playlist[]; +} + +/** + * Returns a single playlist with items and owner username, or null if inaccessible. + * RLS enforces per-playlist visibility for the current viewer (anon or authenticated). + * + * @param id - UUID of the playlist. + */ +export async function getPlaylistById(id: string): Promise<{ + playlist: Playlist; + isOwn: boolean; + ownerUsername: string | null; + ownerAvatarUrl: string | null; +} | null> { + const { supabase, userId } = await getOptionalUser(); + + const { data } = await supabase + .from('playlists') + .select('*, items:playlist_items(*)') + .eq('id', id) + .maybeSingle(); + + if (!data) return null; + + const { data: ownerProfile } = await supabase + .from('user_profiles') + .select('username, avatar_url') + .eq('user_id', data.user_id) + .maybeSingle(); + + const ownerUsername = ownerProfile?.username ?? null; + const ownerAvatarUrl = resolveAvatarUrl(ownerProfile?.avatar_url, null); + + return { + playlist: data as Playlist, + isOwn: userId === data.user_id, + ownerUsername, + ownerAvatarUrl, + }; +} diff --git a/lib/data/profile.ts b/lib/data/profile.ts new file mode 100644 index 00000000..11d7c528 --- /dev/null +++ b/lib/data/profile.ts @@ -0,0 +1,57 @@ +import 'server-only'; + +import { createClient } from '@/lib/supabase/server'; +import { USER_PROFILE_COLUMNS, PRIVACY_COLUMNS } from '@/lib/supabase/columns'; +import type { + PrivacySettings, + PrivacyDefaults, + UserProfile, +} from '@/types/profile'; + +const ALL_PUBLIC: PrivacyDefaults = { + watchlist_visibility: 'public', + watched_visibility: 'public', + reviews_visibility: 'public', + playlists_visibility: 'public', + friends_visibility: 'public', +}; + +/** + * Returns the public profile for a given username (case-insensitive), or null if not found. + * + * @param username - The profile's username slug. + * @returns UserProfile or null. + */ +export async function getProfileByUsername( + username: string +): Promise { + const supabase = await createClient(); + + const { data } = await supabase + .from('user_profiles') + .select(USER_PROFILE_COLUMNS) + .ilike('username', username) + .maybeSingle(); + + return data ?? null; +} + +/** + * Returns privacy settings for a given user, falling back to all-public defaults. + * + * @param userId - Supabase user ID. + * @returns PrivacySettings object. + */ +export async function getPrivacySettings( + userId: string +): Promise { + const supabase = await createClient(); + + const { data } = await supabase + .from('privacy_settings') + .select(PRIVACY_COLUMNS) + .eq('user_id', userId) + .maybeSingle(); + + return (data as PrivacySettings) ?? { user_id: userId, ...ALL_PUBLIC }; +} diff --git a/lib/data/prompts.ts b/lib/data/prompts.ts index 7d05345c..66b436be 100644 --- a/lib/data/prompts.ts +++ b/lib/data/prompts.ts @@ -1,8 +1,30 @@ +import 'server-only'; + import { cache } from 'react'; -import { getPromptStates } from '@/app/actions/prompts'; import { getUserContext } from '@/lib/supabase/auth-helpers'; +import { + isPromptKey, + isPromptState, + type PromptStates, +} from '@/lib/prompts/keys'; + +/** Answers already given to account-scoped call-to-actions, so none is ever asked twice. */ +export const getCachedPromptStates = cache(async (): Promise => { + const { supabase, user } = await getUserContext(); + if (!user) return {}; -export const getCachedPromptStates = cache(getPromptStates); + const { data } = await supabase + .from('user_prompts') + .select('prompt_key, state') + .eq('user_id', user.id); + + const states: PromptStates = {}; + for (const row of data ?? []) { + if (isPromptKey(row.prompt_key) && isPromptState(row.state)) + states[row.prompt_key] = row.state; + } + return states; +}); /** * Watchlist size only, request-deduped: the call-to-action slot renders on every page, diff --git a/lib/data/recommendations.ts b/lib/data/recommendations.ts new file mode 100644 index 00000000..6ad99ff2 --- /dev/null +++ b/lib/data/recommendations.ts @@ -0,0 +1,41 @@ +import 'server-only'; + +import { getOptionalUser } from '@/lib/supabase/auth-helpers'; +import type { MediaType } from '@/types/tmdb'; + +export interface RecommendationDismissal { + media_id: number; + media_type: MediaType; + genre_ids: number[]; +} + +/** Returns every title the user dismissed from recommendations. */ +export async function getMyDismissals(): Promise { + const { supabase, userId } = await getOptionalUser(); + if (!userId) return []; + + const { data } = await supabase + .from('recommendation_dismissals') + .select('media_id, media_type, genre_ids') + .eq('user_id', userId); + + return (data ?? []).map((row) => ({ + media_id: row.media_id, + media_type: row.media_type as MediaType, + genre_ids: row.genre_ids, + })); +} + +/** Returns the TMDB provider IDs of the user's streaming services. */ +export async function getMyStreamingProviders(): Promise { + const { supabase, userId } = await getOptionalUser(); + if (!userId) return []; + + const { data } = await supabase + .from('user_streaming_providers') + .select('provider_ids') + .eq('user_id', userId) + .maybeSingle(); + + return data?.provider_ids ?? []; +} diff --git a/lib/data/reviews.ts b/lib/data/reviews.ts new file mode 100644 index 00000000..c233f9f7 --- /dev/null +++ b/lib/data/reviews.ts @@ -0,0 +1,230 @@ +import 'server-only'; + +import { getOptionalUser } from '@/lib/supabase/auth-helpers'; +import { createClient } from '@/lib/supabase/server'; +import { fetchAllRows } from '@/lib/supabase/pagination'; +import { reportSwallowed } from '@/lib/report'; +import { REVIEW_COLUMNS } from '@/lib/supabase/columns'; +import type { + Review, + PublicReview, + ReviewMediaType, + UserReviewsPage, +} from '@/types/profile'; + +export const REVIEWS_PAGE_SIZE = 20; +const REVIEWS_MAX_PAGE_SIZE = 50; + +function parseRatingRow(data: unknown): { avg: number; count: number } | null { + const row = + (data as Array<{ avg: string | null; count: string }> | null)?.[0] ?? + null; + if (!row || row.avg === null) return null; + return { avg: Number(row.avg), count: Number(row.count) }; +} + +/** + * Returns a page of reviews for a given user, newest first. + * Pass nextCursor from the previous page to get the next batch. + */ +export async function getUserReviews( + userId: string, + cursor?: string, + limit: number = REVIEWS_PAGE_SIZE +): Promise { + const supabase = await createClient(); + const pageSize = Math.min(Math.max(1, limit), REVIEWS_MAX_PAGE_SIZE); + + let query = supabase + .from('reviews') + .select(REVIEW_COLUMNS) + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .order('id', { ascending: false }) + .limit(pageSize + 1); + + if (cursor) query = query.lt('created_at', cursor); + + const { data, error } = await query; + if (error) throw new Error(error.message); + + const rows = (data as Review[]) ?? []; + const hasMore = rows.length > pageSize; + const reviews = hasMore ? rows.slice(0, pageSize) : rows; + const nextCursor = hasMore ? reviews[reviews.length - 1].created_at : null; + + return { reviews, nextCursor }; +} + +/** + * Returns a `media_key → rating` map of a user's own movie/tv ratings, for sorting lists + * by user rating. Reviews RLS restricts direct reads to the user's own rows, so callers + * should only request the rating map of the list owner viewing their own profile. + * + * @param userId - Owner of the reviews. + */ +export async function getUserReviewRatings( + userId: string +): Promise> { + const supabase = await createClient(); + + const data = await fetchAllRows((from, to) => + supabase + .from('reviews') + .select('media_id, media_type, rating') + .eq('user_id', userId) + .in('media_type', ['movie', 'tv']) + .not('rating', 'is', null) + .order('media_type') + .order('media_id') + .range(from, to) + ).catch((error: unknown) => { + reportSwallowed('reviews:ratings', error); + return []; + }); + + const map: Record = {}; + for (const row of data) { + if (row.rating !== null) { + map[`${row.media_type}-${row.media_id}`] = row.rating; + } + } + return map; +} + +/** Returns the authenticated user's own `media_key → rating` map, or an empty map if signed out. */ +export async function getMyReviewRatings(): Promise> { + const { userId } = await getOptionalUser(); + if (!userId) return {}; + return getUserReviewRatings(userId); +} + +/** + * Returns the authenticated user's review for a specific media item, or null if none. + */ +export async function getMediaReview( + mediaId: number, + mediaType: ReviewMediaType +): Promise { + const { supabase, userId } = await getOptionalUser(); + if (!userId) return null; + + const { data } = await supabase + .from('reviews') + .select(REVIEW_COLUMNS) + .eq('user_id', userId) + .eq('media_id', mediaId) + .eq('media_type', mediaType) + .maybeSingle(); + + return (data as Review) ?? null; +} + +/** + * Returns the authenticated user's own episode reviews, keyed by episode ID, so a season + * page can render inline ratings without one query per episode. + */ +export async function getMyEpisodeReviews( + episodeIds: number[] +): Promise> { + if (episodeIds.length === 0) return {}; + const { supabase, userId } = await getOptionalUser(); + if (!userId) return {}; + + const { data } = await supabase + .from('reviews') + .select(REVIEW_COLUMNS) + .eq('user_id', userId) + .eq('media_type', 'episode') + .in('media_id', episodeIds); + + const byEpisodeId: Record = {}; + for (const review of (data ?? []) as Review[]) { + byEpisodeId[review.media_id] = review; + } + return byEpisodeId; +} + +/** + * Returns the community average rating (1–10 scale) and count for a media item. + * Returns null if no ratings exist. + */ +export async function getAverageRating( + mediaId: number, + mediaType: ReviewMediaType +): Promise<{ avg: number; count: number } | null> { + const supabase = await createClient(); + + const { data } = await supabase.rpc('get_media_rating', { + p_media_id: mediaId, + p_media_type: mediaType, + }); + return parseRatingRow(data); +} + +/** + * Returns the community average rating for a season, aggregated from its episode reviews. + * Returns null if no episode ratings exist for this season. + */ +export async function getSeasonAverageRating( + tvId: number, + seasonNumber: number +): Promise<{ avg: number; count: number } | null> { + const supabase = await createClient(); + + const { data } = await supabase.rpc('get_season_rating', { + p_tv_id: tvId, + p_season_number: seasonNumber, + }); + return parseRatingRow(data); +} + +/** + * Returns the community average rating for a show, aggregated from all its episode reviews. + * Returns null if no ratings exist. + */ +export async function getShowAverageRating( + tvId: number +): Promise<{ avg: number; count: number } | null> { + const supabase = await createClient(); + + const { data } = await supabase.rpc('get_show_rating', { p_tv_id: tvId }); + return parseRatingRow(data); +} + +/** + * Returns public reviews for a media item, filtered by the viewer's auth/friendship status. + */ +export async function getPublicReviews( + mediaId: number, + mediaType: ReviewMediaType +): Promise { + const { supabase, userId } = await getOptionalUser(); + + const { data, error } = await supabase.rpc('get_public_reviews', { + p_media_id: mediaId, + p_media_type: mediaType, + p_viewer_id: userId ?? undefined, + }); + + if (error) return []; + return (data as PublicReview[]) ?? []; +} + +/** + * Returns public reviews for a set of episode IDs, filtered by viewer's auth/friendship status. + */ +export async function getPublicEpisodeReviews( + episodeIds: number[] +): Promise { + if (episodeIds.length === 0) return []; + const { supabase, userId } = await getOptionalUser(); + + const { data, error } = await supabase.rpc('get_public_episode_reviews', { + p_episode_ids: episodeIds, + p_viewer_id: userId ?? undefined, + }); + + if (error) return []; + return (data as PublicReview[]) ?? []; +} diff --git a/lib/data/watchlist.ts b/lib/data/watchlist.ts index dd6345af..8ab51013 100644 --- a/lib/data/watchlist.ts +++ b/lib/data/watchlist.ts @@ -1,12 +1,78 @@ +import 'server-only'; + import { cache } from 'react'; -import { getUserWatchlist } from '@/app/actions/watchlist'; -import { getAllTvShowsWatchProgress } from '@/app/actions/episodes'; -import { getMyReviewRatings } from '@/app/actions/reviews'; +import { getOptionalUser } from '@/lib/supabase/auth-helpers'; +import { fetchAllRows } from '@/lib/supabase/pagination'; +import { WATCHLIST_COLUMNS } from '@/lib/supabase/columns'; +import { getAllTvShowsWatchProgress } from '@/lib/data/episodes'; +import { getMyReviewRatings } from '@/lib/data/reviews'; import { getMyDismissals, getMyStreamingProviders, -} from '@/app/actions/recommendations'; -import type { MediaItem, WatchlistEntry } from '@/types/tmdb'; +} from '@/lib/data/recommendations'; +import type { + MediaItem, + MediaType, + WatchlistEntry, +} from '@/types/tmdb'; + +/** Every watchlist row of the authenticated user, newest first; empty when signed out. */ +export async function getUserWatchlist(): Promise { + const { supabase, userId } = await getOptionalUser(); + + if (!userId) return []; + + const entries = await fetchAllRows((from, to) => + supabase + .from('watchlist') + .select(WATCHLIST_COLUMNS) + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .order('id') + .range(from, to) + ); + + return entries as WatchlistEntry[]; +} + +/** The authenticated user's watchlist row for one title, or null when absent. */ +export async function getMediaWatchlistEntry( + mediaId: number, + mediaType: MediaType +): Promise { + const { supabase, userId } = await getOptionalUser(); + + if (!userId) return null; + + const { data: entry } = await supabase + .from('watchlist') + .select(WATCHLIST_COLUMNS) + .eq('user_id', userId) + .eq('media_id', mediaId) + .eq('media_type', mediaType) + .maybeSingle(); + + return (entry as WatchlistEntry) ?? null; +} + +/** Watchlist rows for a batch of TMDB IDs, so a grid can badge its cards in one query. */ +export async function getMediaWatchlistEntries( + mediaIds: number[] +): Promise { + const { supabase, userId } = await getOptionalUser(); + + if (!userId || mediaIds.length === 0) return []; + + const { data: entries, error } = await supabase + .from('watchlist') + .select(WATCHLIST_COLUMNS) + .eq('user_id', userId) + .in('media_id', mediaIds); + + if (error) throw new Error(error.message); + + return (entries as WatchlistEntry[]) ?? []; +} export const getCachedUserWatchlist = cache(getUserWatchlist); export const getCachedMyRatings = cache(getMyReviewRatings); diff --git a/package.json b/package.json index 1625ca8b..74e2a84c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "nextjs-toploader": "^3.9.17", "react": "19.2.7", "react-dom": "19.2.7", + "server-only": "^0.0.1", "serwist": "^9.5.12", "simple-icons": "^16.27.1", "sonner": "^2.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 167474f8..d07ef99d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: react-dom: specifier: 19.2.7 version: 19.2.7(react@19.2.7) + server-only: + specifier: ^0.0.1 + version: 0.0.1 serwist: specifier: ^9.5.12 version: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) @@ -5328,6 +5331,12 @@ packages: engines: { node: '>=10' } hasBin: true + server-only@0.0.1: + resolution: + { + integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==, + } + serwist@9.5.12: resolution: { @@ -9322,6 +9331,8 @@ snapshots: semver@7.8.5: {} + server-only@0.0.1: {} + serwist@9.5.12(browserslist@4.28.6)(typescript@6.0.3): dependencies: '@serwist/utils': 9.5.12(browserslist@4.28.6) From 935adf60d48c5a0c65a82baf1f495b8b474ad1f8 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 23:18:40 +0200 Subject: [PATCH 45/98] =?UTF-8?q?feat(securite):=20budget=20par=20utilisat?= =?UTF-8?q?eur=20sur=20les=20actions=20co=C3=BBteuses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sendFriendRequest déclenche un push à chaque appel : sans limite, un compte pouvait notifier tout le monde en énumérant les user_id. exportUserData dumpe l'intégralité des lignes du compte à chaque clic. - enforceUserRateLimit() dans lib/rate-limiter.ts, clé par userId et non par IP : les NAT partagés ne sont pas punis et la rotation d'IP ne remet pas la fenêtre à zéro - sendFriendRequest : 30/heure, vérifié après le garde SELF_REQUEST pour qu'un bug client ne consomme pas le budget - exportUserData : 5/heure - code RATE_LIMITED isolé dans lib/action-errors.ts, sans dépendance, pour qu'un composant client puisse le comparer sans embarquer le limiteur serveur dans le bundle - messages fr + en, 4 tests unitaires sur le helper --- app/actions/data.ts | 12 ++++++++ app/actions/friends.ts | 15 +++++++++ components/profile/FriendshipButton.tsx | 3 ++ components/settings/DataSettings.tsx | 10 ++++-- lib/action-errors.ts | 6 ++++ lib/i18n/messages/profile.ts | 2 ++ lib/i18n/messages/settings.ts | 2 ++ lib/rate-limiter.ts | 18 +++++++++++ tests/unit/rate-limiter.test.ts | 41 ++++++++++++++++++++++++- 9 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 lib/action-errors.ts diff --git a/app/actions/data.ts b/app/actions/data.ts index 67ca0afe..eee5c21d 100644 --- a/app/actions/data.ts +++ b/app/actions/data.ts @@ -4,6 +4,7 @@ import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; import { fetchAllRows } from '@/lib/supabase/pagination'; import { parseVisibility } from '@/lib/privacy'; import { reportCritical } from '@/lib/report'; +import { enforceUserRateLimit } from '@/lib/rate-limiter'; import { revalidateProfileAfterResponse } from '@/app/actions/_helpers'; import { VALID_STATUSES, validateRating } from '@/lib/validators'; import { @@ -19,6 +20,9 @@ import type { } from '@/lib/data-transfer/types'; import type { WatchStatus } from '@/types/tmdb'; +const EXPORT_LIMIT = 5; +const EXPORT_WINDOW_MS = 3_600_000; + type WatchlistRow = { user_id: string; media_id: number; @@ -28,9 +32,17 @@ type WatchlistRow = { status: WatchStatus; }; +/** + * Full RGPD export of the authenticated user's data. + * Rate-limited: one call dumps every watchlist, review and episode row the account owns. + * + * @throws Error('RATE_LIMITED') once the hourly export budget is exhausted. + */ export async function exportUserData(): Promise { const { supabase, userId } = await getAuthenticatedUser(); + enforceUserRateLimit('export', userId, EXPORT_LIMIT, EXPORT_WINDOW_MS); + const [watchlistRows, reviewRows, episodeRows] = await Promise.all([ fetchAllRows((from, to) => supabase diff --git a/app/actions/friends.ts b/app/actions/friends.ts index 80814332..3a6b7cdf 100644 --- a/app/actions/friends.ts +++ b/app/actions/friends.ts @@ -3,8 +3,12 @@ import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; import { revalidateProfileAfterResponse } from '@/app/actions/_helpers'; import { sendFriendPush } from '@/lib/push/notify-friend'; +import { enforceUserRateLimit } from '@/lib/rate-limiter'; import { after } from 'next/server'; +const FRIEND_REQUEST_LIMIT = 30; +const FRIEND_REQUEST_WINDOW_MS = 3_600_000; + /** * Cancels a pending friend request sent by the authenticated user. * @@ -32,15 +36,26 @@ export async function cancelFriendRequest( /** * Sends a friend request from the authenticated user to another user. * + * Rate-limited: each accepted request fires a push notification, so the budget bounds how + * fast one account can notify its way through the user base. + * * @param addresseeId - Supabase user ID of the recipient. * @throws Error('SELF_REQUEST') if trying to friend oneself. * @throws Error('DUPLICATE_REQUEST') if a request already exists. + * @throws Error('RATE_LIMITED') once the hourly request budget is exhausted. */ export async function sendFriendRequest(addresseeId: string): Promise { const { supabase, userId, user } = await getAuthenticatedUser(); if (addresseeId === userId) throw new Error('SELF_REQUEST'); + enforceUserRateLimit( + 'friend-request', + userId, + FRIEND_REQUEST_LIMIT, + FRIEND_REQUEST_WINDOW_MS + ); + const { error } = await supabase .from('friendships') .insert({ requester_id: userId, addressee_id: addresseeId }); diff --git a/components/profile/FriendshipButton.tsx b/components/profile/FriendshipButton.tsx index bfa184b7..75728559 100644 --- a/components/profile/FriendshipButton.tsx +++ b/components/profile/FriendshipButton.tsx @@ -11,6 +11,7 @@ import { cancelFriendRequest, } from '@/app/actions/friends'; import { createClient } from '@/lib/supabase/client'; +import { RATE_LIMITED } from '@/lib/action-errors'; import type { RealtimePostgresChangesPayload } from '@supabase/supabase-js'; import type { Friendship } from '@/types/profile'; import { useTranslation } from '@/lib/i18n/context'; @@ -101,6 +102,8 @@ export function FriendshipButton({ toast.error(t.profile.errors.selfRequest); else if (msg === 'DUPLICATE_REQUEST') toast.error(t.profile.errors.duplicateRequest); + else if (msg === RATE_LIMITED) + toast.error(t.profile.errors.rateLimited); else toast.error(t.common.actionError); } }); diff --git a/components/settings/DataSettings.tsx b/components/settings/DataSettings.tsx index 7a038a6b..2420b17c 100644 --- a/components/settings/DataSettings.tsx +++ b/components/settings/DataSettings.tsx @@ -27,6 +27,7 @@ import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { useTranslation } from '@/lib/i18n/context'; import { exportUserData, importBatch, importLists } from '@/app/actions/data'; +import { RATE_LIMITED } from '@/lib/action-errors'; import type { ImportItem, ImportedList } from '@/lib/data-transfer/types'; import { parseImportFile, type Platform } from '@/lib/parsers/import-watchlist'; import { @@ -105,8 +106,13 @@ export function DataSettings() { document.body.removeChild(a); URL.revokeObjectURL(url); toast.success(td.exportSuccess); - } catch { - toast.error(t.common.actionError); + } catch (err) { + const msg = err instanceof Error ? err.message : ''; + toast.error( + msg === RATE_LIMITED + ? td.exportRateLimited + : t.common.actionError + ); } }); } diff --git a/lib/action-errors.ts b/lib/action-errors.ts new file mode 100644 index 00000000..4cbef8c9 --- /dev/null +++ b/lib/action-errors.ts @@ -0,0 +1,6 @@ +/** + * Error codes thrown by Server Actions and matched by their client callers. + * Kept dependency-free so a client component can import a code without pulling + * the server module that throws it into the browser bundle. + */ +export const RATE_LIMITED = 'RATE_LIMITED'; diff --git a/lib/i18n/messages/profile.ts b/lib/i18n/messages/profile.ts index acffc9a6..b0fed5fb 100644 --- a/lib/i18n/messages/profile.ts +++ b/lib/i18n/messages/profile.ts @@ -93,6 +93,7 @@ const fr = { invalidVisibility: 'Visibilité invalide', selfRequest: "Tu ne peux pas t'inviter toi-même", duplicateRequest: 'Une demande existe déjà avec cet utilisateur', + rateLimited: 'Trop de demandes envoyées, réessaie dans un moment', }, }; @@ -190,6 +191,7 @@ const en = { invalidVisibility: 'Invalid visibility value', selfRequest: 'You cannot send an invitation to yourself', duplicateRequest: 'A friend request already exists with this user', + rateLimited: 'Too many requests sent, try again in a while', }, } satisfies typeof fr; diff --git a/lib/i18n/messages/settings.ts b/lib/i18n/messages/settings.ts index a57577b5..8c93166f 100644 --- a/lib/i18n/messages/settings.ts +++ b/lib/i18n/messages/settings.ts @@ -172,6 +172,7 @@ const fr = { 'Télécharge toutes tes données (watchlist, avis, épisodes vus) au format JSON.', exportButton: 'Exporter', exportSuccess: 'Export téléchargé', + exportRateLimited: "Trop d'exports demandés, réessaie dans un moment", importTitle: 'Importer des données', importDescription: 'Importe ton historique depuis Letterboxd, Trakt ou un export ReelMark.', @@ -405,6 +406,7 @@ const en = { 'Download all your data (watchlist, reviews, watched episodes) as JSON.', exportButton: 'Export', exportSuccess: 'Export downloaded', + exportRateLimited: 'Too many exports requested, try again in a while', importTitle: 'Import Data', importDescription: 'Import your history from Letterboxd, Trakt, or a ReelMark export.', diff --git a/lib/rate-limiter.ts b/lib/rate-limiter.ts index 7ecf6c01..cc0cef1b 100644 --- a/lib/rate-limiter.ts +++ b/lib/rate-limiter.ts @@ -1,3 +1,5 @@ +import { RATE_LIMITED } from '@/lib/action-errors'; + interface RateLimitEntry { count: number; resetAt: number; @@ -60,3 +62,19 @@ export function checkRateLimit( resetAt: entry.resetAt, }; } + +/** + * Applies a per-user budget to an authenticated Server Action, keyed on the user rather + * than the IP so shared NATs are not punished and IP rotation does not reset the window. + * + * @throws Error(RATE_LIMITED) once the budget for this scope is exhausted. + */ +export function enforceUserRateLimit( + scope: string, + userId: string, + limit: number, + windowMs: number +): void { + const { allowed } = checkRateLimit(`${scope}:${userId}`, limit, windowMs); + if (!allowed) throw new Error(RATE_LIMITED); +} diff --git a/tests/unit/rate-limiter.test.ts b/tests/unit/rate-limiter.test.ts index cb78004f..ad8e37e0 100644 --- a/tests/unit/rate-limiter.test.ts +++ b/tests/unit/rate-limiter.test.ts @@ -1,5 +1,6 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { checkRateLimit } from '@/lib/rate-limiter'; +import { checkRateLimit, enforceUserRateLimit } from '@/lib/rate-limiter'; +import { RATE_LIMITED } from '@/lib/action-errors'; beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); @@ -56,3 +57,41 @@ describe('checkRateLimit', () => { expect(second.allowed).toBe(false); }); }); + +describe('enforceUserRateLimit', () => { + it('stays silent while the budget lasts, then throws RATE_LIMITED', () => { + for (let i = 0; i < 3; i++) { + expect(() => + enforceUserRateLimit('scope-a', 'user-1', 3, 60_000) + ).not.toThrow(); + } + expect(() => + enforceUserRateLimit('scope-a', 'user-1', 3, 60_000) + ).toThrow(RATE_LIMITED); + }); + + it('budgets each user separately', () => { + for (let i = 0; i < 3; i++) + enforceUserRateLimit('scope-b', 'user-1', 3, 60_000); + expect(() => + enforceUserRateLimit('scope-b', 'user-2', 3, 60_000) + ).not.toThrow(); + }); + + it('budgets each scope separately for the same user', () => { + for (let i = 0; i < 3; i++) + enforceUserRateLimit('scope-c', 'user-3', 3, 60_000); + expect(() => + enforceUserRateLimit('scope-d', 'user-3', 3, 60_000) + ).not.toThrow(); + }); + + it('lets the user through again once the window expires', () => { + for (let i = 0; i < 3; i++) + enforceUserRateLimit('scope-e', 'user-4', 3, 60_000); + vi.advanceTimersByTime(61_000); + expect(() => + enforceUserRateLimit('scope-e', 'user-4', 3, 60_000) + ).not.toThrow(); + }); +}); From 94c1e217d2de6cbc1adc43ddd354988c437084cd Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sun, 26 Jul 2026 23:35:38 +0200 Subject: [PATCH 46/98] chore(git): versionne le .gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il n'était pas suivi (exclu par le .gitignore_global de la machine), donc tout worktree, clone ou checkout CI démarrait sans aucune règle : un git add -A y stageait .next, node_modules et surtout .env.local. Deux doublons exacts retirés au passage (playwright-report, .claude/*), sans effet sur les motifs appliqués. --- .gitignore | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b6ed343a --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.DS_Store +.env.local +.next +next-env.d.ts +node_modules +tsconfig.tsbuildinfo +playwright-report +public/sw.js +public/sw.js.map +public/workbox-*.js +public/workbox-*.js.map +public/swe-worker-*.js +tests/.auth +test-results +.vscode/* +.claude/* +CLAUDE.md +context-ai/* From 650093072489bd9eff817952d1cf10b921fcead4 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 00:25:59 +0200 Subject: [PATCH 47/98] =?UTF-8?q?fix(episodes):=20la=20synchro=20du=20stat?= =?UTF-8?q?ut=20de=20s=C3=A9rie=20ne=20partait=20plus=20jamais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getCachedTvShowDetails enveloppait getTvShowDetails dans unstable_cache. fetchTMDB y appelle getServerLanguage() -> headers(), interdit dans un scope de cache : l'appel throwait. Le .catch(() => null) l'avalait en silence, donc syncTvShowWatchlistStatus repartait toujours sur totalEpisodes = 0 et abandonnait la synchro (« episode total unknown for tvId N » à chaque clic). L'échec était intermittent selon le contexte : getServerLanguage est mémoïsé par React.cache, donc une page qui l'avait déjà appelé passait, alors qu'une Server Action, qui n'amorce rien, échouait systématiquement. Conséquence : terminer la dernière saison d'une série ne mettait plus son statut à jour — l'invariant décrit dans CLAUDE.md. - lib/tmdb/cached.ts supprimé : fetchTMDBUrl porte déjà 'use cache' + cacheLife, le wrapper était redondant autant que cassé (et c'était le seul unstable_cache du repo, incompatible avec cacheComponents: true) - les trois catch muets sur les détails TMDB passent par reportSwallowed, comme l'impose le CLAUDE.md — c'est ce silence qui masquait le bug --- app/actions/episodes.ts | 7 +++++-- lib/data/continue-watching.ts | 15 +++++++++++---- lib/tmdb/cached.ts | 14 -------------- 3 files changed, 16 insertions(+), 20 deletions(-) delete mode 100644 lib/tmdb/cached.ts diff --git a/app/actions/episodes.ts b/app/actions/episodes.ts index 59f85c2a..f166ca54 100644 --- a/app/actions/episodes.ts +++ b/app/actions/episodes.ts @@ -5,7 +5,7 @@ import { SHARED_REVALIDATE_PATHS, revalidateLocalizedAfterResponse, } from '@/app/actions/_helpers'; -import { getCachedTvShowDetails } from '@/lib/tmdb/cached'; +import { getTvShowDetails } from '@/lib/tmdb/tv'; import { reportCritical, reportSwallowed } from '@/lib/report'; import type { SupabaseServerClient } from '@/lib/supabase/server'; import type { WatchStatus } from '@/types/tmdb'; @@ -32,7 +32,10 @@ async function syncTvShowWatchlistStatus( userId: string, tvId: number ): Promise { - const details = await getCachedTvShowDetails(tvId).catch(() => null); + const details = await getTvShowDetails(tvId).catch((error: unknown) => { + reportSwallowed('episodes:tv-details', error); + return null; + }); let totalEpisodes = (details?.seasons ?? []) .filter((s: { season_number: number }) => s.season_number > 0) diff --git a/lib/data/continue-watching.ts b/lib/data/continue-watching.ts index f86ad146..db48166f 100644 --- a/lib/data/continue-watching.ts +++ b/lib/data/continue-watching.ts @@ -5,7 +5,8 @@ import { getOptionalUser } from '@/lib/supabase/auth-helpers'; import { getWatchlistWithProgress } from '@/lib/data/watchlist'; import { fetchAllRows } from '@/lib/supabase/pagination'; import { getSeasonDetails } from '@/lib/tmdb'; -import { getCachedTvShowDetails } from '@/lib/tmdb/cached'; +import { getTvShowDetails } from '@/lib/tmdb/tv'; +import { reportSwallowed } from '@/lib/report'; import { findNextEpisode, type SeasonEpisodeCount } from '@/lib/next-episode'; import { orderByWatchRecency } from '@/lib/continue-watching'; import type { WatchlistEntry } from '@/types/tmdb'; @@ -46,8 +47,11 @@ async function buildItem( entry: WatchlistEntry, watchedBySeason: WatchedBySeason ): Promise { - const details = await getCachedTvShowDetails(entry.media_id).catch( - () => null + const details = await getTvShowDetails(entry.media_id).catch( + (error: unknown) => { + reportSwallowed('continue-watching:tv-details', error); + return null; + } ); if (!details) return null; @@ -64,7 +68,10 @@ async function buildItem( const season = await getSeasonDetails( entry.media_id, next.seasonNumber - ).catch(() => null); + ).catch((error: unknown) => { + reportSwallowed('continue-watching:season-details', error); + return null; + }); if (!season) return null; const watchedEpisodes = watchedBySeason.get(next.seasonNumber) ?? new Set(); diff --git a/lib/tmdb/cached.ts b/lib/tmdb/cached.ts deleted file mode 100644 index 1a661187..00000000 --- a/lib/tmdb/cached.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { unstable_cache } from 'next/cache'; -import { getTvShowDetails } from '@/lib/tmdb/tv'; - -const TV_DETAILS_TTL_SECONDS = 300; - -/** - * TV show details shared by every server action that only needs the season/episode - * structure, kept in a single cache entry so the same show is fetched once. - */ -export const getCachedTvShowDetails = unstable_cache( - (tvId: number) => getTvShowDetails(tvId), - ['tv-show-details'], - { revalidate: TV_DETAILS_TTL_SECONDS } -); From 67a3e0aa8ed38687d79e454633b56a2fabc85793 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 00:44:51 +0200 Subject: [PATCH 48/98] =?UTF-8?q?fix(observabilite):=20plus=20aucun=20?= =?UTF-8?q?=C3=A9chec=20serveur=20aval=C3=A9=20en=20silence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un catch muet est ce qui a masqué le bug de synchro des épisodes pendant tout ce temps. Le CLAUDE.md l'interdit déjà : tout fallback avalé passe par lib/report.ts. - pages similar (film + série), crew, saison : un catch { notFound() } faisait passer une panne TMDB pour un titre inexistant, sans laisser de trace - recommendations : les crédits et la rangée de nouveautés échouaient en silence, dégradant les suggestions sans rien signaler Vérifié par un crawl des 35 routes de l'app : aucune de ces sondes ne se déclenche, ces chemins sont sains. Elles serviront de détecteur si ça change. --- app/[lang]/crew/[id]/page.tsx | 4 +++- app/[lang]/movie/[id]/similar/page.tsx | 4 +++- app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx | 4 +++- app/[lang]/tv/[id]/similar/page.tsx | 4 +++- lib/recommendations/sections.ts | 11 +++++++++-- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/[lang]/crew/[id]/page.tsx b/app/[lang]/crew/[id]/page.tsx index 2690779a..041b5339 100644 --- a/app/[lang]/crew/[id]/page.tsx +++ b/app/[lang]/crew/[id]/page.tsx @@ -22,6 +22,7 @@ import { mergeWithWatchlist } from '@/lib/data/watchlist'; import { getTranslations } from '@/lib/i18n/server'; import type { Language } from '@/lib/i18n/translations'; import { localizedAlternates } from '@/lib/metadata'; +import { reportSwallowed } from '@/lib/report'; type CrewPageParams = Promise<{ lang: Language; id: string }>; interface CrewPageProps { @@ -155,7 +156,8 @@ export default async function CrewPage(props: CrewPageProps) { let crew; try { crew = await getCrewDetails(crewId, lang); - } catch { + } catch (error) { + reportSwallowed('crew:details', error); notFound(); } diff --git a/app/[lang]/movie/[id]/similar/page.tsx b/app/[lang]/movie/[id]/similar/page.tsx index 468afad9..8d76d788 100644 --- a/app/[lang]/movie/[id]/similar/page.tsx +++ b/app/[lang]/movie/[id]/similar/page.tsx @@ -9,6 +9,7 @@ import { RelatedMediaPage } from '@/components/media/RelatedMediaPage'; import { getTranslations } from '@/lib/i18n/server'; import { buildPageMetadata, localizedAlternates } from '@/lib/metadata'; import type { Language } from '@/lib/i18n/translations'; +import { reportSwallowed } from '@/lib/report'; type SimilarPageParams = Promise<{ lang: Language; id: string }>; interface SimilarPageProps { @@ -53,7 +54,8 @@ export default async function SimilarMoviesPage(props: SimilarPageProps) { let details; try { details = await getMovieDetails(movieId, lang); - } catch { + } catch (error) { + reportSwallowed('movie/similar:details', error); notFound(); } const t = await getTranslations(lang); diff --git a/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx b/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx index 2d37bd61..08d0a2a9 100644 --- a/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx +++ b/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx @@ -24,6 +24,7 @@ import { WatchProviders } from '@/components/media/detail/WatchProviders'; import { DetailSectionSkeleton } from '@/components/media/detail/MediaDetailSkeleton'; import { SeasonEpisodesList } from '@/components/media/tv/SeasonEpisodesList'; import { localizedAlternates } from '@/lib/metadata'; +import { reportSwallowed } from '@/lib/report'; type SeasonPageParams = Promise<{ lang: Language; @@ -118,7 +119,8 @@ export default async function SeasonPage(props: SeasonPageProps) { getTvShowDetails(tvId, lang), getSeasonDetails(tvId, seasonNumber, lang), ]); - } catch { + } catch (error) { + reportSwallowed('season:details', error); notFound(); } diff --git a/app/[lang]/tv/[id]/similar/page.tsx b/app/[lang]/tv/[id]/similar/page.tsx index 98fbd668..f47d581f 100644 --- a/app/[lang]/tv/[id]/similar/page.tsx +++ b/app/[lang]/tv/[id]/similar/page.tsx @@ -9,6 +9,7 @@ import { RelatedMediaPage } from '@/components/media/RelatedMediaPage'; import { getTranslations } from '@/lib/i18n/server'; import { buildPageMetadata, localizedAlternates } from '@/lib/metadata'; import type { Language } from '@/lib/i18n/translations'; +import { reportSwallowed } from '@/lib/report'; type SimilarPageParams = Promise<{ lang: Language; id: string }>; interface SimilarPageProps { @@ -53,7 +54,8 @@ export default async function SimilarTvShowsPage(props: SimilarPageProps) { let details; try { details = await getTvShowDetails(tvId, lang); - } catch { + } catch (error) { + reportSwallowed('tv/similar:details', error); notFound(); } const t = await getTranslations(lang); diff --git a/lib/recommendations/sections.ts b/lib/recommendations/sections.ts index dde9587b..58c23ee0 100644 --- a/lib/recommendations/sections.ts +++ b/lib/recommendations/sections.ts @@ -33,6 +33,7 @@ import { import { getMediaKey } from '@/lib/media'; import type { Translations } from '@/lib/i18n/server'; import type { Language } from '@/lib/i18n/translations'; +import { reportSwallowed } from '@/lib/report'; import type { Movie, TvShow, @@ -225,13 +226,19 @@ export async function buildLibrarySections( ), Promise.all( personSeedEntries.map((entry) => - getCredits(entry.media_id, lang).catch(() => null) + getCredits(entry.media_id, lang).catch((error: unknown) => { + reportSwallowed('recommendations:credits', error); + return null; + }) ) ), (isMovie ? getUpcomingMovies(1, lang) : getOnTheAirTvShows(1, lang) - ).catch((): Movie[] | TvShow[] => []), + ).catch((error: unknown): Movie[] | TvShow[] => { + reportSwallowed('recommendations:fresh-titles', error); + return []; + }), ]); const seedCandidates = seeds.map(({ weight }, index) => ({ From a2140e495952aa9e4a41b959bbdc09bb1aad1dfc Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 00:44:51 +0200 Subject: [PATCH 49/98] =?UTF-8?q?fix(tmdb):=20un=20404=20sur=20recommendat?= =?UTF-8?q?ions/similar=20n'est=20plus=20logg=C3=A9=20en=20erreur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/tmdb/errors.ts documente déjà le 404 de ces endpoints comme « routine rather than exceptional » et expose isTMDBNotFound() pour ça — le helper n'était branché nulle part. Deux séries mortes chez TMDB dans une watchlist suffisaient donc à remplir le terminal d'« Error » à chaque rendu du dashboard, en donnant l'impression d'une panne. Le comportement ne change pas (rangée vide, réponse en cache 1 h sans retry) : seul le bruit disparaît. Les échecs non-404 restent loggés. --- lib/tmdb/movies.ts | 7 +++++-- lib/tmdb/tv.ts | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/tmdb/movies.ts b/lib/tmdb/movies.ts index d46e38d4..732bdce7 100644 --- a/lib/tmdb/movies.ts +++ b/lib/tmdb/movies.ts @@ -1,3 +1,4 @@ +import { isTMDBNotFound } from '@/lib/tmdb/errors'; import type { Movie, MovieDetails, @@ -227,7 +228,8 @@ export async function getMovieRecommendations( ); return results; } catch (error) { - reportSwallowed('tmdb/movies:recommendations', error); + if (!isTMDBNotFound(error)) + reportSwallowed('tmdb/movies:recommendations', error); return []; } } @@ -249,7 +251,8 @@ export async function getSimilarMovies( ); return results; } catch (error) { - reportSwallowed('tmdb/movies:similar', error); + if (!isTMDBNotFound(error)) + reportSwallowed('tmdb/movies:similar', error); return []; } } diff --git a/lib/tmdb/tv.ts b/lib/tmdb/tv.ts index 9e76e3a5..d39f3c24 100644 --- a/lib/tmdb/tv.ts +++ b/lib/tmdb/tv.ts @@ -1,3 +1,4 @@ +import { isTMDBNotFound } from '@/lib/tmdb/errors'; import type { TvShow, TvShowDetails, @@ -236,7 +237,8 @@ export async function getTvShowRecommendations( ); return results; } catch (error) { - reportSwallowed('tmdb/tv:recommendations', error); + if (!isTMDBNotFound(error)) + reportSwallowed('tmdb/tv:recommendations', error); return []; } } @@ -258,7 +260,7 @@ export async function getSimilarTvShows( ); return results; } catch (error) { - reportSwallowed('tmdb/tv:similar', error); + if (!isTMDBNotFound(error)) reportSwallowed('tmdb/tv:similar', error); return []; } } From 865b59f5e417b121fc7d1f74ff7fac925c7f20d5 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 01:48:20 +0200 Subject: [PATCH 50/98] chore(format): Prettier apply --- .../(protected)/profile/[username]/page.tsx | 5 +---- app/[lang]/(protected)/settings/page.tsx | 20 +++++++++---------- lib/data/watchlist.ts | 6 +----- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/app/[lang]/(protected)/profile/[username]/page.tsx b/app/[lang]/(protected)/profile/[username]/page.tsx index 61410b10..16e01081 100644 --- a/app/[lang]/(protected)/profile/[username]/page.tsx +++ b/app/[lang]/(protected)/profile/[username]/page.tsx @@ -12,10 +12,7 @@ import { ProfileTabsSkeleton } from '@/components/profile/ProfileTabsSkeleton'; import { ProfileHeroSkeleton } from '@/components/profile/ProfileHeroSkeleton'; import { FriendshipButton } from '@/components/profile/FriendshipButton'; import { ProfileOptionsMenu } from '@/components/profile/ProfileOptionsMenu'; -import { - getProfileByUsername, - getPrivacySettings, -} from '@/lib/data/profile'; +import { getProfileByUsername, getPrivacySettings } from '@/lib/data/profile'; import { getUserReviews, getUserReviewRatings } from '@/lib/data/reviews'; import { getUserPlaylists } from '@/lib/data/playlists'; import { diff --git a/app/[lang]/(protected)/settings/page.tsx b/app/[lang]/(protected)/settings/page.tsx index 9ab48042..045cd840 100644 --- a/app/[lang]/(protected)/settings/page.tsx +++ b/app/[lang]/(protected)/settings/page.tsx @@ -53,16 +53,16 @@ async function SettingsSection({ selectedProviderIds, notificationPreferences, ] = await Promise.all([ - supabase - .from('user_profiles') - .select(USER_PROFILE_COLUMNS) - .eq('user_id', user.id) - .maybeSingle(), - supabase - .from('privacy_settings') - .select(PRIVACY_COLUMNS) - .eq('user_id', user.id) - .maybeSingle(), + supabase + .from('user_profiles') + .select(USER_PROFILE_COLUMNS) + .eq('user_id', user.id) + .maybeSingle(), + supabase + .from('privacy_settings') + .select(PRIVACY_COLUMNS) + .eq('user_id', user.id) + .maybeSingle(), getUserRegion(lang), getMyStreamingProviders(), getNotificationPreferences(), diff --git a/lib/data/watchlist.ts b/lib/data/watchlist.ts index 8ab51013..42906792 100644 --- a/lib/data/watchlist.ts +++ b/lib/data/watchlist.ts @@ -10,11 +10,7 @@ import { getMyDismissals, getMyStreamingProviders, } from '@/lib/data/recommendations'; -import type { - MediaItem, - MediaType, - WatchlistEntry, -} from '@/types/tmdb'; +import type { MediaItem, MediaType, WatchlistEntry } from '@/types/tmdb'; /** Every watchlist row of the authenticated user, newest first; empty when signed out. */ export async function getUserWatchlist(): Promise { From 81900d4ed23c40e8b167f59f1faa276aa4971eea Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 03:52:01 +0200 Subject: [PATCH 51/98] =?UTF-8?q?feat(ui):=20les=20contr=C3=B4les=20pos?= =?UTF-8?q?=C3=A9s=20sur=20les=20affiches=20passent=20en=20verre=20translu?= =?UTF-8?q?cide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les badges et boutons superposés aux posters (statut, note, marquer vu, abandonner, croix « pas intéressé », retrait de playlist) masquaient l'image derrière un fond opaque. - active `.glass-overlay`, la recette déjà présente dans globals.css mais inutilisée, et lui ajoute `.glass-overlay-button` dont le survol densifie le verre au lieu de le teinter - monte la recette à 65 % de `--surface` : en dessous, une note en marron du thème clair posée sur une affiche noire devenait inconfortable - réactive le flou du bouton d'ajout des cartes, qui était désactivé - remplace trois couleurs figées illisibles en thème clair (`text-white/70`, `text-white/55`, `bg-black/70`) --- app/globals.css | 18 ++++++++++++++++-- .../dashboard/ContinueWatchingSection.tsx | 7 +++---- components/dashboard/ForYouSection.tsx | 2 +- components/media/card/MediaCard.tsx | 5 ++--- components/media/tv/AbandonShowMenu.tsx | 6 +++--- components/media/tv/EpisodeCard.tsx | 4 ++-- components/profile/PlaylistGrid.tsx | 6 +++--- components/profile/PlaylistsSection.tsx | 4 ++-- 8 files changed, 32 insertions(+), 20 deletions(-) diff --git a/app/globals.css b/app/globals.css index bfbbe4fe..cfed1722 100644 --- a/app/globals.css +++ b/app/globals.css @@ -464,15 +464,29 @@ a { } /* Glass layered over media / dark backdrops (cards, buttons on posters). */ - .glass-overlay { + .glass-overlay, + .glass-overlay-button { --glass-filter: blur(40px) saturate(150%); - background-color: color-mix(in srgb, var(--surface) 55%, transparent); + background-color: color-mix(in srgb, var(--surface) 65%, transparent); -webkit-backdrop-filter: var(--glass-filter); backdrop-filter: var(--glass-filter); border: 1px solid var(--border); border-top-color: var(--glass-border-hover); } + /* Interactive variant: hover densifies the glass instead of tinting it. */ + .glass-overlay-button { + transition: + background-color var(--duration-fast) var(--ease-apple), + border-color var(--duration-fast) var(--ease-apple), + color var(--duration-fast) var(--ease-apple); + } + + .glass-overlay-button:hover { + background-color: color-mix(in srgb, var(--surface) 80%, transparent); + border-color: var(--glass-border-hover); + } + .top-nav-safe-area { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); diff --git a/components/dashboard/ContinueWatchingSection.tsx b/components/dashboard/ContinueWatchingSection.tsx index cb41d5b7..5c54b5c6 100644 --- a/components/dashboard/ContinueWatchingSection.tsx +++ b/components/dashboard/ContinueWatchingSection.tsx @@ -163,7 +163,7 @@ function ContinueWatchingCard({ )} {episodeCode && ( - + {episodeCode} )} @@ -201,8 +201,7 @@ function ContinueWatchingCard({ disabled={loading} aria-label={t.movie.markEpisodeWatched} className={cn( - 'flex min-h-10 cursor-pointer items-center gap-2 rounded-md border border-border/10 border-t-border/20 bg-surface px-3 py-2 text-xs font-medium text-muted shadow-card-sm transition-colors', - 'hover:border-border hover:bg-surface-2 hover:text-text', + 'flex min-h-10 cursor-pointer items-center gap-2 rounded-md glass-overlay-button px-3 py-2 text-xs font-medium text-text shadow-card-sm', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', 'disabled:cursor-not-allowed disabled:opacity-50' )} @@ -217,7 +216,7 @@ function ContinueWatchingCard({ : t.movie.markEpisodeWatched} ) : ( - + diff --git a/components/dashboard/ForYouSection.tsx b/components/dashboard/ForYouSection.tsx index 56646f6d..7cd74380 100644 --- a/components/dashboard/ForYouSection.tsx +++ b/components/dashboard/ForYouSection.tsx @@ -77,7 +77,7 @@ export function ForYouSection({ title, items }: ForYouSectionProps) { }} aria-label={t.pages.dashboard.notInterested} title={t.pages.dashboard.notInterested} - className="flex h-8 w-8 items-center justify-center rounded-full bg-poster-overlay-heavy border border-white/10 text-white/70 shadow-card-sm transition-colors cursor-pointer hover:text-white hover:bg-red/40 hover:border-red/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" + className="flex h-8 w-8 items-center justify-center rounded-full glass-overlay-button text-text/70 shadow-card-sm cursor-pointer hover:text-white hover:bg-red/70 hover:border-red/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" > diff --git a/components/media/card/MediaCard.tsx b/components/media/card/MediaCard.tsx index 335851d4..4169dd56 100644 --- a/components/media/card/MediaCard.tsx +++ b/components/media/card/MediaCard.tsx @@ -122,7 +122,7 @@ export function MediaCard({ )} {compact && badgeEntry && ( -
+
{badgeEntry.status === 'watched' ? ( -
+
)} diff --git a/components/media/tv/AbandonShowMenu.tsx b/components/media/tv/AbandonShowMenu.tsx index 6080f637..7a98548f 100644 --- a/components/media/tv/AbandonShowMenu.tsx +++ b/components/media/tv/AbandonShowMenu.tsx @@ -52,9 +52,9 @@ export function AbandonShowMenu({ disabled={loading} aria-label={t.movie.moreActions} className={cn( - 'flex h-8 w-8 items-center justify-center rounded-full bg-surface-2 text-text', - 'border border-border/10 transition-colors cursor-pointer', - 'hover:bg-surface-2/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', + 'flex h-8 w-8 items-center justify-center rounded-full glass-overlay-button text-text', + 'shadow-card-sm cursor-pointer', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', 'disabled:opacity-50 disabled:cursor-not-allowed', className )} diff --git a/components/media/tv/EpisodeCard.tsx b/components/media/tv/EpisodeCard.tsx index e8a6d94c..ebdef07b 100644 --- a/components/media/tv/EpisodeCard.tsx +++ b/components/media/tv/EpisodeCard.tsx @@ -90,12 +90,12 @@ export function EpisodeCard({ {noImage}
)} -
+
E{episode.episode_number.toString().padStart(2, '0')}
diff --git a/components/profile/PlaylistGrid.tsx b/components/profile/PlaylistGrid.tsx index 6115a976..70fe9ad3 100644 --- a/components/profile/PlaylistGrid.tsx +++ b/components/profile/PlaylistGrid.tsx @@ -111,18 +111,18 @@ export function PlaylistGrid({ className={cn( 'absolute top-2 right-2 z-20', 'w-6 h-6 rounded-full flex items-center justify-center', - 'bg-poster-overlay-heavy border border-white/10 shadow-sm', + 'glass-overlay-button shadow-card-sm text-text', 'transition-all duration-(--duration-instant)', 'sm:opacity-0 sm:scale-75 sm:group-hover:opacity-100 sm:group-hover:scale-100', isRemoving ? 'opacity-100 scale-100' - : 'cursor-pointer hover:bg-red/20 hover:border-red/30' + : 'cursor-pointer hover:bg-red/70 hover:border-red/50 hover:text-white' )} > {isRemoving ? ( ) : ( - + )} )} diff --git a/components/profile/PlaylistsSection.tsx b/components/profile/PlaylistsSection.tsx index 3e41001b..aacadddb 100644 --- a/components/profile/PlaylistsSection.tsx +++ b/components/profile/PlaylistsSection.tsx @@ -203,7 +203,7 @@ function PlaylistCard({ e.stopPropagation(); openEditDialog(); }} - className="p-2 rounded-lg bg-black/70 border border-glass-border text-white/55 hover:text-white hover:bg-black/85 transition-colors" + className="p-2 rounded-lg glass-overlay-button text-text/60 hover:text-text" aria-label={t.profile.editPlaylist} > @@ -213,7 +213,7 @@ function PlaylistCard({ e.stopPropagation(); setConfirmDelete(true); }} - className="p-2 rounded-lg bg-black/70 border border-glass-border text-white/55 hover:text-red hover:bg-red/20 hover:border-red/30 transition-colors" + className="p-2 rounded-lg glass-overlay-button text-text/60 hover:text-white hover:bg-red/70 hover:border-red/50" aria-label={t.profile.deletePlaylist} > From e7997ce21ee8700732e590e0b7c3b583f90a532f Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 04:07:59 +0200 Subject: [PATCH 52/98] =?UTF-8?q?feat(prod):=20durcit=20les=20chemins=20se?= =?UTF-8?q?nsibles=20et=20rend=20l'observabilit=C3=A9=20exploitable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reprise triée de la branche audit/prod-hardening, dont la base avait 120 commits de retard : seuls les changements encore absents de dev sont repris, réécrits sur son état actuel. Sécurité - `lib/supabase/server.ts` passe en `server-only` : un import depuis un composant client casse désormais le build au lieu d'embarquer la clé service_role dans le bundle - les liens de confirmation, magic link et reset partent de `BASE_URL` et non plus d'un `Host` reconstruit depuis les en-têtes, qu'un client peut forger - `sanitizeRedirectPath` rejette l'antislash, que les navigateurs normalisent en slash — `/\evil.com` sortait du site - la recherche `@pseudo` filtre sur le jeu de caractères des pseudos, ce qui retire le `%` interprété comme joker par `ilike` - `getFriendsWithProfiles` valide l'UUID reçu avant toute requête Observabilité - `withSentryConfig` : source maps uploadées et tunnel same-origin `/monitoring`, sans quoi les traces Bugsink restent illisibles et les événements tombent sous les bloqueurs de pub - `onRouterTransitionStart` rattache enfin une erreur client à sa route - démarrage bruyant quand une variable optionnelle manque, au lieu d'une fonctionnalité éteinte en silence Divers - `formatDate` / `formatShortDate` ne rendent plus « Invalid Date » - la revalidation du layout après changement de pseudo ou d'avatar est différée, pour ne plus renvoyer tout le payload RSC avec la réponse de mutation Écartés : `reportDegraded` (jamais appelée) et les error boundaries, que dev couvre déjà — et mieux, son garde `!error.digest` évitant le double envoi. --- app/[lang]/(protected)/settings/actions.ts | 5 +++-- app/actions/_helpers.ts | 9 +++++++++ app/api/search/route.ts | 2 +- app/auth/actions.ts | 17 +++-------------- instrumentation-client.ts | 3 +++ instrumentation.ts | 17 +++++++++++++++++ lib/data/friends.ts | 3 +++ lib/format.ts | 12 +++++++++--- lib/supabase/server.ts | 1 + lib/validators.ts | 7 ++++++- next.config.ts | 20 +++++++++++++++++++- proxy.ts | 1 + 12 files changed, 75 insertions(+), 22 deletions(-) diff --git a/app/[lang]/(protected)/settings/actions.ts b/app/[lang]/(protected)/settings/actions.ts index d70c03c2..8545ccd1 100644 --- a/app/[lang]/(protected)/settings/actions.ts +++ b/app/[lang]/(protected)/settings/actions.ts @@ -2,6 +2,7 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; +import { revalidateLayoutAfterResponse } from '@/app/actions/_helpers'; import { createClient, createAdminClient } from '@/lib/supabase/server'; import { isOAuthOnly } from '@/lib/supabase/auth-helpers'; import { getTranslations, getServerLanguage } from '@/lib/i18n/server'; @@ -119,7 +120,7 @@ export async function updateProfile(prevState: unknown, formData: FormData) { return { error: t.settings.usernameTaken, success: false }; if (syncError) return { error: syncError, success: false }; - revalidatePath('/', 'layout'); + revalidateLayoutAfterResponse(); return { error: undefined, success: true, @@ -214,7 +215,7 @@ export async function updateAvatar(prevState: unknown, formData: FormData) { return { error: error.message, success: false }; } - revalidatePath('/', 'layout'); + revalidateLayoutAfterResponse(); return { error: undefined, success: true, diff --git a/app/actions/_helpers.ts b/app/actions/_helpers.ts index 68b0f7b8..e7fa6b8c 100644 --- a/app/actions/_helpers.ts +++ b/app/actions/_helpers.ts @@ -23,6 +23,15 @@ export function revalidateLocalizedAfterResponse(paths: readonly string[]) { }); } +/** + * Deferred whole-layout revalidation, for mutations that change what the shell renders + * (username, avatar). Synchronously it would ship the caller's full RSC payload back with + * the mutation response — the exact cost `revalidateLocalizedAfterResponse` exists to avoid. + */ +export function revalidateLayoutAfterResponse() { + after(() => revalidatePath('/', 'layout')); +} + /** * Drops the cached Open Graph metadata of a playlist after the response. * Without it a renamed — or newly private — playlist keeps advertising its old name for the diff --git a/app/api/search/route.ts b/app/api/search/route.ts index 034f073e..ec7be83d 100644 --- a/app/api/search/route.ts +++ b/app/api/search/route.ts @@ -52,7 +52,7 @@ export async function GET(req: NextRequest) { } if (query.startsWith('@')) { - const username = query.slice(1); + const username = query.slice(1).replace(/[^a-zA-Z0-9_]/g, ''); if (!username) return NextResponse.json( { users: [] }, diff --git a/app/auth/actions.ts b/app/auth/actions.ts index 270b8e21..0217e63b 100644 --- a/app/auth/actions.ts +++ b/app/auth/actions.ts @@ -2,7 +2,6 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; -import { headers } from 'next/headers'; import { createClient, createAdminClient } from '@/lib/supabase/server'; import { BASE_URL } from '@/lib/metadata'; import { getServerLanguage, getTranslations } from '@/lib/i18n/server'; @@ -35,13 +34,6 @@ function mapAuthError(message: string, t: AuthTranslations): string { return message; } -async function getOrigin(): Promise { - const h = await headers(); - const proto = h.get('x-forwarded-proto') ?? 'https'; - const host = h.get('x-forwarded-host') ?? h.get('host'); - return host ? `${proto}://${host}` : BASE_URL; -} - export async function login(prevState: unknown, formData: FormData) { const t = await getTranslations(); const email = validateEmail(formData.get('email')); @@ -86,12 +78,11 @@ export async function signup(prevState: unknown, formData: FormData) { if (existingProfile) return { error: t.settings.usernameTaken }; - const origin = await getOrigin(); const { data, error } = await supabase.auth.signUp({ email, password, options: { - emailRedirectTo: `${origin}/auth/confirm?next=/dashboard`, + emailRedirectTo: `${BASE_URL}/auth/confirm?next=/dashboard`, data: { full_name: username, username, @@ -150,12 +141,11 @@ export async function requestMagicLink( if (!validEmail) return { error: t.settings.missingFields }; const supabase = await createClient(); - const origin = await getOrigin(); const { error } = await supabase.auth.signInWithOtp({ email: validEmail, options: { shouldCreateUser: false, - emailRedirectTo: `${origin}/auth/confirm?next=/dashboard`, + emailRedirectTo: `${BASE_URL}/auth/confirm?next=/dashboard`, }, }); @@ -178,9 +168,8 @@ export async function requestPasswordReset( if (!email) return { error: t.settings.missingFields }; const supabase = await createClient(); - const origin = await getOrigin(); const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: `${origin}/auth/confirm?next=/auth/update-password`, + redirectTo: `${BASE_URL}/auth/confirm?next=/auth/update-password`, }); if (error) return { error: error.message }; diff --git a/instrumentation-client.ts b/instrumentation-client.ts index 49d282d1..70003e6e 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -5,3 +5,6 @@ Sentry.init({ tracesSampleRate: 0, debug: false, }); + +/** Required by the App Router SDK: without it a client error carries no originating-route context. */ +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; diff --git a/instrumentation.ts b/instrumentation.ts index 212cd8e6..478330f6 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -16,6 +16,23 @@ export async function register() { ); } + const degradesSilently = [ + 'NEXT_PUBLIC_SENTRY_DSN', + 'SENTRY_DSN', + 'WATCHMODE_API_KEY', + 'NEXT_PUBLIC_VAPID_PUBLIC_KEY', + 'VAPID_PRIVATE_KEY', + 'VAPID_SUBJECT', + ]; + + const absent = degradesSilently.filter((key) => !process.env[key]); + + if (absent.length > 0) { + console.warn( + `[instrumentation] Feature disabled, no runtime error will be raised: ${absent.join(', ')}` + ); + } + if (process.env.NEXT_RUNTIME === 'nodejs') { await import('./sentry.server.config'); } diff --git a/lib/data/friends.ts b/lib/data/friends.ts index eb81b994..5d5a5724 100644 --- a/lib/data/friends.ts +++ b/lib/data/friends.ts @@ -6,6 +6,7 @@ import { getPrivacySettings } from '@/lib/data/profile'; import { FRIENDSHIP_COLUMNS } from '@/lib/supabase/columns'; import { fetchAllRows } from '@/lib/supabase/pagination'; import { resolveAvatarUrl } from '@/lib/avatar'; +import { validateUUID } from '@/lib/validators'; import type { Friendship, FriendEntry, @@ -93,6 +94,8 @@ export async function getFriendsWithProfiles( ): Promise { const { userId: viewerId } = await getAuthenticatedUser(); + if (validateUUID(userId) === null) throw new Error('Invalid user ID'); + if (viewerId !== userId) { const { friends_visibility } = await getPrivacySettings(userId); if (friends_visibility === 'private') return []; diff --git a/lib/format.ts b/lib/format.ts index 2197b9d2..e0021419 100644 --- a/lib/format.ts +++ b/lib/format.ts @@ -24,15 +24,21 @@ export function formatDate( day: 'numeric', }; - return new Date(dateString).toLocaleDateString(locale, { + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) return null; + + return date.toLocaleDateString(locale, { ...(options ?? defaultOptions), timeZone: DISPLAY_TIME_ZONE, }); } -/** Short localized date (e.g. "15 Jul 2023") from an ISO string. */ +/** Short localized date (e.g. "15 Jul 2023") from an ISO string; empty when unparseable. */ export function formatShortDate(dateString: string, locale: string): string { - return new Date(dateString).toLocaleDateString(locale, { + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) return ''; + + return date.toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric', diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts index 579349dd..be94de08 100644 --- a/lib/supabase/server.ts +++ b/lib/supabase/server.ts @@ -1,3 +1,4 @@ +import 'server-only'; import { createServerClient } from '@supabase/ssr'; import { createClient as createSupabaseAdminClient } from '@supabase/supabase-js'; import { cookies } from 'next/headers'; diff --git a/lib/validators.ts b/lib/validators.ts index 391532c4..fe40761f 100644 --- a/lib/validators.ts +++ b/lib/validators.ts @@ -58,7 +58,12 @@ export function sanitizeRedirectPath( fallback: string ): string { if (!path) return fallback; - if (!path.startsWith('/') || path.startsWith('//') || path.includes('://')) + if ( + !path.startsWith('/') || + path.startsWith('//') || + path.includes('://') || + path.includes('\\') + ) return fallback; return path; } diff --git a/next.config.ts b/next.config.ts index ce5b6b7f..a17179f6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,5 @@ import { networkInterfaces } from 'node:os'; +import { withSentryConfig } from '@sentry/nextjs'; import type { NextConfig } from 'next'; import withSerwist from '@serwist/next'; @@ -125,7 +126,7 @@ const nextConfig: NextConfig = { const offlineRevision = crypto.randomUUID(); -export default withSerwist({ +const withPWA = withSerwist({ swSrc: 'app/service-worker.ts', swDest: 'public/sw.js', disable: isDev, @@ -135,3 +136,20 @@ export default withSerwist({ { url: '/fr/offline', revision: offlineRevision }, ], })(nextConfig); + +/** + * Source maps and the same-origin tunnel. Without the wrapper every Bugsink stack trace reads + * `chunk-a3f2.js:1:48291`, and events sent straight to the third-party DSN are dropped by + * ad blockers. Upload is skipped when SENTRY_AUTH_TOKEN is unset, so local builds stay offline. + */ +export default withSentryConfig(withPWA, { + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + sentryUrl: process.env.SENTRY_URL, + authToken: process.env.SENTRY_AUTH_TOKEN, + tunnelRoute: '/monitoring', + widenClientFileUpload: true, + webpack: { treeshake: { removeDebugLogging: true } }, + silent: !process.env.CI, + sourcemaps: { deleteSourcemapsAfterUpload: true }, +}); diff --git a/proxy.ts b/proxy.ts index 9ad4685e..8312e03c 100644 --- a/proxy.ts +++ b/proxy.ts @@ -8,6 +8,7 @@ function isBypassedPath(pathname: string): boolean { return ( pathname.startsWith('/api') || pathname.startsWith('/auth') || + pathname.startsWith('/monitoring') || pathname === '/og' ); } From ecd1c9adcbee5d30ff1c27edfcbfac1038443076 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 04:23:09 +0200 Subject: [PATCH 53/98] =?UTF-8?q?fix(providers):=20les=20logos=20=C2=AB=20?= =?UTF-8?q?O=C3=B9=20regarder=20=C2=BB=20redeviennent=20des=20liens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Watchmode est la seule source qui fournit une URL profonde par plateforme, et son quota mensuel s'épuise vite : passé le premier 429, le client ouvre un cooldown et `getWatchmodeProviders` rend `null`. On retombe alors sur TMDB, qui liste bien les plateformes mais sans URL par plateforme — `web_url` étant vide, aucun `` n'était produit et les 29 logos de la page devenaient de simples images, sur mobile comme sur desktop. - repli sur `providers.link`, la page JustWatch du titre que TMDB renvoie déjà et que le composant ignorait, quand l'URL profonde manque - cible tactile portée de 40 à 48 px via un pseudo-élément, sans décaler la grille — l'espacement des logos reste suffisant pour éviter tout recouvrement - état `active:` pour redonner un retour visuel au doigt, que le `-webkit-tap-highlight-color: transparent` global supprime Vérifié en émulation iPhone : 29 liens atteignables contre 0 avant. --- components/media/detail/WatchProviders.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/components/media/detail/WatchProviders.tsx b/components/media/detail/WatchProviders.tsx index 09cc0005..9b623abe 100644 --- a/components/media/detail/WatchProviders.tsx +++ b/components/media/detail/WatchProviders.tsx @@ -52,12 +52,14 @@ function ProviderLogo({ showPrice, logoMap, appStoreMap, + fallbackUrl, }: { p: WatchProvider; region: string; showPrice?: boolean; logoMap: Map; appStoreMap: Map; + fallbackUrl?: string; }) { const key = normalizeName(p.provider_name); const logoSrc = @@ -104,14 +106,22 @@ function ProviderLogo({
); - if (p.web_url) { + // Watchmode fournit l'URL profonde vers la plateforme, mais son quota est vite épuisé ; + // on retombe alors sur les données TMDB, dépourvues d'URL par plateforme. Sans ce repli + // vers la page JustWatch du titre, les logos cessaient d'être des liens du tout. + const href = p.web_url || fallbackUrl; + + if (href) { + // Le logo ne fait que 40 px : le pseudo-élément porte la cible tactile à 48 px sans + // décaler la grille. `active:` redonne au doigt le retour visuel que le + // `-webkit-tap-highlight-color: transparent` global supprime. return ( {inner} @@ -129,6 +139,7 @@ function ProviderGroup({ showPrice, logoMap, appStoreMap, + fallbackUrl, }: { label: string; providers: WatchProvider[]; @@ -136,6 +147,7 @@ function ProviderGroup({ showPrice?: boolean; logoMap: Map; appStoreMap: Map; + fallbackUrl?: string; }) { return (
@@ -151,6 +163,7 @@ function ProviderGroup({ showPrice={showPrice} logoMap={logoMap} appStoreMap={appStoreMap} + fallbackUrl={fallbackUrl} /> ))}
@@ -184,6 +197,7 @@ export async function WatchProviders({ providers }: WatchProvidersProps) { region={region} logoMap={logoMap} appStoreMap={appStoreMap} + fallbackUrl={providers.link} /> )} {providers.rent && providers.rent.length > 0 && ( @@ -194,6 +208,7 @@ export async function WatchProviders({ providers }: WatchProvidersProps) { showPrice logoMap={logoMap} appStoreMap={appStoreMap} + fallbackUrl={providers.link} /> )} {providers.buy && providers.buy.length > 0 && ( @@ -204,6 +219,7 @@ export async function WatchProviders({ providers }: WatchProvidersProps) { showPrice logoMap={logoMap} appStoreMap={appStoreMap} + fallbackUrl={providers.link} /> )}
From 9c8adbb14dadd0ff47c576b40b05ff40e5ab9f02 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 04:32:58 +0200 Subject: [PATCH 54/98] =?UTF-8?q?fix(tactile):=20supprime=20un=20tap=20des?= =?UTF-8?q?tructeur=20et=20=C3=A9largit=20deux=20cibles=20trop=20petites?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le bouton de suppression d'une notification portait `group-hover:opacity-100` alors que le `group` était posé sur le lien voisin, pas sur un ancêtre : la règle générée (`:is(:where(.group):hover *)`) ne l'a donc jamais atteint. Il restait invisible en toutes circonstances — survol desktop compris — tout en captant les clics. Viser le coin haut-droit d'une notification pour l'ouvrir la supprimait. - `group` déplacé sur le conteneur qui englobe réellement le bouton - le bouton s'affiche d'emblée sous 640 px et ne se cache qu'au-delà, où le survol existe pour le révéler - cible tactile de la croix « pas intéressé » portée de 32 à 48 px - même traitement pour le menu « … » d'une série, mais d'un cran seulement : dans « Continuer à regarder » son voisin n'est qu'à 6 px, qu'un `-inset-2` aurait recouvert (mesuré : débord 4 px, aucun chevauchement) --- components/dashboard/ForYouSection.tsx | 2 +- components/media/tv/AbandonShowMenu.tsx | 5 ++++- components/notifications/NotificationItem.tsx | 9 ++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/components/dashboard/ForYouSection.tsx b/components/dashboard/ForYouSection.tsx index 7cd74380..29209736 100644 --- a/components/dashboard/ForYouSection.tsx +++ b/components/dashboard/ForYouSection.tsx @@ -77,7 +77,7 @@ export function ForYouSection({ title, items }: ForYouSectionProps) { }} aria-label={t.pages.dashboard.notInterested} title={t.pages.dashboard.notInterested} - className="flex h-8 w-8 items-center justify-center rounded-full glass-overlay-button text-text/70 shadow-card-sm cursor-pointer hover:text-white hover:bg-red/70 hover:border-red/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" + className="relative flex h-8 w-8 items-center justify-center rounded-full glass-overlay-button text-text/70 shadow-card-sm cursor-pointer hover:text-white hover:bg-red/70 hover:border-red/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary before:absolute before:-inset-2 before:content-['']" > diff --git a/components/media/tv/AbandonShowMenu.tsx b/components/media/tv/AbandonShowMenu.tsx index 7a98548f..40915788 100644 --- a/components/media/tv/AbandonShowMenu.tsx +++ b/components/media/tv/AbandonShowMenu.tsx @@ -52,8 +52,11 @@ export function AbandonShowMenu({ disabled={loading} aria-label={t.movie.moreActions} className={cn( - 'flex h-8 w-8 items-center justify-center rounded-full glass-overlay-button text-text', + 'relative flex h-8 w-8 items-center justify-center rounded-full glass-overlay-button text-text', 'shadow-card-sm cursor-pointer', + // Cible tactile élargie, mais d'un cran seulement : dans « Continuer à + // regarder » le voisin n'est qu'à 6 px, qu'un `-inset-2` recouvrirait. + "before:absolute before:-inset-1 before:content-['']", 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', 'disabled:opacity-50 disabled:cursor-not-allowed', className diff --git a/components/notifications/NotificationItem.tsx b/components/notifications/NotificationItem.tsx index 8d0b428e..55678630 100644 --- a/components/notifications/NotificationItem.tsx +++ b/components/notifications/NotificationItem.tsx @@ -68,12 +68,12 @@ export function NotificationItem({ ); const itemClassName = cn( - 'group flex items-start gap-3 rounded-lg px-3 py-2.5 transition-colors', + 'flex items-start gap-3 rounded-lg px-3 py-2.5 transition-colors', 'hover:bg-surface-2 active:bg-surface-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary' ); return ( -
+
{notification.url ? ( {inner}
)} {onDelete && ( -
+ // Visible d'emblée au doigt : `hover:` est borné à @media (hover:hover), donc au + // tactile le bouton restait invisible tout en captant le tap — viser la + // notification pour l'ouvrir la supprimait. +
onDelete(notification.id)} ariaLabel={t.notifications.delete} From 30cb8a1f697bf82e2e46a4cb9246110adb863943 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 05:26:21 +0200 Subject: [PATCH 55/98] feat(providers): rapproche les offres d'un titre des plateformes de l'utilisateur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `lib/provider-identity.ts` : sort de `lib/tmdb/watch-providers.ts` la normalisation de nom et les alias, qui tiraient `next/headers` via le client TMDB et rendaient tout appelant intestable - `lib/watch-now.ts` : `matchMyProviders` croise le flatrate d'un titre avec les services de l'utilisateur, et retient le lien le plus profond disponible - rapprochement par nom et non par id : quand les offres viennent de Watchmode, les `source_id` vivent dans un autre espace que les id TMDB de la sélection utilisateur - 9 tests couvrent alias (Max/HBO Max), variantes suffixées, repli sans lien profond, déduplication et non-appariement de deux noms courts distincts --- lib/provider-identity.ts | 52 ++++++++++++++ lib/tmdb/watch-providers.ts | 24 ++----- lib/watch-now.ts | 64 ++++++++++++++++++ tests/unit/watch-now.test.ts | 128 +++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 20 deletions(-) create mode 100644 lib/provider-identity.ts create mode 100644 lib/watch-now.ts create mode 100644 tests/unit/watch-now.test.ts diff --git a/lib/provider-identity.ts b/lib/provider-identity.ts new file mode 100644 index 00000000..d17c90bb --- /dev/null +++ b/lib/provider-identity.ts @@ -0,0 +1,52 @@ +const NAME_ALIASES: Record = { + amazon: 'amazonprimevideo', + amazonprime: 'amazonprimevideo', + hbomax: 'max', + hbo: 'max', + paramountplus: 'paramountplus', + paramount: 'paramountplus', + peacockpremium: 'peacockpremium', + peacock: 'peacockpremium', + nowtv: 'nowtv', + now: 'nowtv', + disney: 'disneyplus', + disneyplus: 'disneyplus', +}; + +const PREFIX_MATCH_MIN_LENGTH = 5; + +export function normalizeName(name: string): string { + return name.toLowerCase().replace(/[^a-z0-9]/g, ''); +} + +export function providerAlias(key: string): string | undefined { + return NAME_ALIASES[key]; +} + +/** + * Comparable identity of a provider name, alias-folded so one service keeps a single key + * across sources — TMDB says "HBO Max" where Watchmode says "Max". + */ +export function providerNameKey(name: string): string { + const key = normalizeName(name); + return NAME_ALIASES[key] ?? key; +} + +/** + * Whether two provider names designate the same service. Matching is name-based on purpose: + * a title's offers may come from Watchmode, whose numeric ids live in a different space than + * the TMDB ids the user's selection is stored with. + */ +export function isSameProvider(a: string, b: string): boolean { + const keyA = providerNameKey(a); + const keyB = providerNameKey(b); + + if (keyA === keyB) return true; + if ( + keyA.length < PREFIX_MATCH_MIN_LENGTH || + keyB.length < PREFIX_MATCH_MIN_LENGTH + ) + return false; + + return keyA.startsWith(keyB) || keyB.startsWith(keyA); +} diff --git a/lib/tmdb/watch-providers.ts b/lib/tmdb/watch-providers.ts index 1bf5ed4b..f1b51677 100644 --- a/lib/tmdb/watch-providers.ts +++ b/lib/tmdb/watch-providers.ts @@ -1,32 +1,16 @@ import { fetchTMDB } from './client'; +import { normalizeName, providerAlias } from '@/lib/provider-identity'; import type { Language } from '@/lib/i18n/translations'; import { reportSwallowed } from '@/lib/report'; +export { normalizeName } from '@/lib/provider-identity'; + interface TmdbProviderItem { logo_path: string; provider_id: number; provider_name: string; } -const NAME_ALIASES: Record = { - amazon: 'amazonprimevideo', - amazonprime: 'amazonprimevideo', - hbomax: 'max', - hbo: 'max', - paramountplus: 'paramountplus', - paramount: 'paramountplus', - peacockpremium: 'peacockpremium', - peacock: 'peacockpremium', - nowtv: 'nowtv', - now: 'nowtv', - disney: 'disneyplus', - disneyplus: 'disneyplus', -}; - -export function normalizeName(name: string): string { - return name.toLowerCase().replace(/[^a-z0-9]/g, ''); -} - async function fetchProviderList( mediaType: 'movie' | 'tv', region: string, @@ -84,7 +68,7 @@ export function resolveProviderLogo( const exact = logoMap.get(key); if (exact) return `https://image.tmdb.org/t/p/w92${exact}`; - const alias = NAME_ALIASES[key]; + const alias = providerAlias(key); if (alias) { const aliased = logoMap.get(alias); if (aliased) return `https://image.tmdb.org/t/p/w92${aliased}`; diff --git a/lib/watch-now.ts b/lib/watch-now.ts new file mode 100644 index 00000000..9feb1aa6 --- /dev/null +++ b/lib/watch-now.ts @@ -0,0 +1,64 @@ +import { isSameProvider, providerNameKey } from '@/lib/provider-identity'; +import type { WatchProvider } from '@/types/tmdb'; + +export interface WatchNowOption { + providerId: number; + providerName: string; + logoPath: string; + href: string; +} + +export type WatchNowVariant = 'banner' | 'bar'; + +/** + * The user's own streaming services that carry this title, with the deepest link available. + * Subscription offers only: renting a title is not "watching it on your platform". + * + * @param flatrate - Subscription offers of the title in the user's region. + * @param mine - Providers the user selected in settings, TMDB-sourced (name + logo). + * @param fallbackHref - Title page used when the offer carries no per-provider deep link. + */ +export function matchMyProviders( + flatrate: WatchProvider[] | undefined, + mine: WatchProvider[], + fallbackHref: string +): WatchNowOption[] { + if (!flatrate?.length || mine.length === 0) return []; + + const options = new Map(); + + for (const offer of flatrate) { + const owned = mine.find((p) => + isSameProvider(offer.provider_name, p.provider_name) + ); + if (!owned) continue; + + const href = offer.web_url || fallbackHref; + if (!href) continue; + + const key = providerNameKey(owned.provider_name); + if (options.has(key)) continue; + + options.set(key, { + providerId: owned.provider_id, + providerName: owned.provider_name, + logoPath: owned.logo_path, + href, + }); + } + + return [...options.values()]; +} + +const WATCH_NOW_BASE = + 'flex items-center justify-center gap-2 shrink-0 font-semibold text-sm text-white bg-primary hover:bg-primary-hover border border-transparent shadow-card-sm transition-all duration-(--duration-fast) ease-apple focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'; + +const WATCH_NOW_SHAPE: Record = { + banner: 'w-full sm:w-auto min-h-11 px-4 py-2.5 rounded-lg', + bar: 'h-12 w-12 lg:h-auto lg:w-auto lg:min-h-11 lg:px-4 lg:py-2.5 rounded-full lg:rounded-lg', +}; + +/** Shared skin of the play control, so the plain link and the menu trigger stay identical. */ +export function watchNowClass(variant: WatchNowVariant): string { + return `${WATCH_NOW_BASE} ${WATCH_NOW_SHAPE[variant]}`; +} diff --git a/tests/unit/watch-now.test.ts b/tests/unit/watch-now.test.ts new file mode 100644 index 00000000..c6f404b6 --- /dev/null +++ b/tests/unit/watch-now.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it } from 'vitest'; +import { matchMyProviders } from '@/lib/watch-now'; +import type { WatchProvider } from '@/types/tmdb'; + +const JUSTWATCH = 'https://justwatch.com/fr/film/dune'; + +function provider( + name: string, + overrides: Partial = {} +): WatchProvider { + return { + provider_id: 8, + provider_name: name, + logo_path: '/netflix.jpg', + display_priority: 0, + ...overrides, + }; +} + +describe('matchMyProviders', () => { + it('keeps only the services the user subscribes to', () => { + const options = matchMyProviders( + [ + provider('Netflix', { web_url: 'https://netflix.com/title/1' }), + provider('Disney Plus', { provider_id: 337 }), + ], + [provider('Netflix')], + JUSTWATCH + ); + + expect(options).toEqual([ + { + providerId: 8, + providerName: 'Netflix', + logoPath: '/netflix.jpg', + href: 'https://netflix.com/title/1', + }, + ]); + }); + + it('matches on name when the offer comes from Watchmode, whose ids differ from TMDB', () => { + const options = matchMyProviders( + [ + provider('Netflix', { + provider_id: 203, + logo_path: '', + logo_url: 'https://watchmode.com/netflix.png', + web_url: 'https://netflix.com/title/1', + }), + ], + [provider('Netflix', { provider_id: 8 })], + JUSTWATCH + ); + + expect(options).toHaveLength(1); + expect(options[0].providerId).toBe(8); + expect(options[0].logoPath).toBe('/netflix.jpg'); + }); + + it('folds aliases so one service is not missed across sources', () => { + const options = matchMyProviders( + [provider('Max', { provider_id: 1899 })], + [provider('HBO Max', { provider_id: 384 })], + JUSTWATCH + ); + + expect(options.map((o) => o.providerName)).toEqual(['HBO Max']); + }); + + it('matches a suffixed variant of the same service', () => { + const options = matchMyProviders( + [provider('Netflix Standard with Ads', { provider_id: 1796 })], + [provider('Netflix', { provider_id: 8 })], + JUSTWATCH + ); + + expect(options).toHaveLength(1); + }); + + it('does not confuse two short unrelated names', () => { + expect( + matchMyProviders( + [provider('OCS', { provider_id: 56 })], + [provider('OCS Go', { provider_id: 57 })], + JUSTWATCH + ) + ).toHaveLength(0); + }); + + it('falls back to the title page when the offer carries no deep link', () => { + const options = matchMyProviders( + [provider('Netflix')], + [provider('Netflix')], + JUSTWATCH + ); + + expect(options[0].href).toBe(JUSTWATCH); + }); + + it('drops an offer that has no link at all', () => { + expect( + matchMyProviders([provider('Netflix')], [provider('Netflix')], '') + ).toHaveLength(0); + }); + + it('deduplicates a service listed twice by the offers', () => { + const options = matchMyProviders( + [ + provider('Netflix', { web_url: 'https://netflix.com/title/1' }), + provider('netflix', { web_url: 'https://netflix.com/title/2' }), + ], + [provider('Netflix')], + JUSTWATCH + ); + + expect(options).toHaveLength(1); + expect(options[0].href).toBe('https://netflix.com/title/1'); + }); + + it('returns nothing without offers or without a user selection', () => { + expect( + matchMyProviders(undefined, [provider('Netflix')], JUSTWATCH) + ).toHaveLength(0); + expect( + matchMyProviders([provider('Netflix')], [], JUSTWATCH) + ).toHaveLength(0); + }); +}); From 64c749315ff3dd37f70203e0a48dafe9fb7f5525 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 05:26:30 +0200 Subject: [PATCH 56/98] feat(ui): bouton lecture vers la plateforme de streaming de l'utilisateur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `WatchNowSlot` : lien direct « Regarder sur X » quand un seul des services de l'utilisateur diffuse le titre, menu de choix au-delà — rien du tout sinon - `WatchNowPublisher` résout les options côté serveur, `WatchNowSeed` les publie dans `lib/watch-now-store.ts` : bannière et barre collante lisent la même résolution - store plutôt qu'un second `` par emplacement : la barre se re-parente dans un portal navbar en mobile, un trou de streaming supplémentaire y déchire le DOM - le menu Radix est en import dynamique, il ne charge que pour les rares titres disponibles sur plusieurs de ses plateformes - abonnement uniquement : proposer une location sous un bouton « Regarder » tromperait --- components/media/detail/WatchNowMenu.tsx | 73 +++++++++++++++++++ components/media/detail/WatchNowPublisher.tsx | 37 ++++++++++ components/media/detail/WatchNowSeed.tsx | 15 ++++ components/media/detail/WatchNowSlot.tsx | 46 ++++++++++++ lib/i18n/messages/movie.ts | 6 ++ lib/watch-now-store.ts | 45 ++++++++++++ 6 files changed, 222 insertions(+) create mode 100644 components/media/detail/WatchNowMenu.tsx create mode 100644 components/media/detail/WatchNowPublisher.tsx create mode 100644 components/media/detail/WatchNowSeed.tsx create mode 100644 components/media/detail/WatchNowSlot.tsx create mode 100644 lib/watch-now-store.ts diff --git a/components/media/detail/WatchNowMenu.tsx b/components/media/detail/WatchNowMenu.tsx new file mode 100644 index 00000000..efb5937f --- /dev/null +++ b/components/media/detail/WatchNowMenu.tsx @@ -0,0 +1,73 @@ +'use client'; + +import Image from 'next/image'; +import { ChevronDown, Play } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuItem, +} from '@/components/ui/dropdown-menu'; +import { useTranslation } from '@/lib/i18n/context'; +import { getImageUrl } from '@/lib/tmdb/images'; +import { cn } from '@/lib/utils'; +import { + watchNowClass, + type WatchNowOption, + type WatchNowVariant, +} from '@/lib/watch-now'; + +interface WatchNowMenuProps { + options: WatchNowOption[]; + variant: WatchNowVariant; +} + +/** Platform picker used when several of the user's services carry the title. */ +export function WatchNowMenu({ options, variant }: WatchNowMenuProps) { + const { t } = useTranslation(); + const labelClass = variant === 'bar' ? 'hidden lg:inline' : undefined; + + return ( + + + + {t.movie.watchNow} + + + + + {t.movie.chooseProvider} + + {options.map((option) => ( + + + + + {option.providerName} + + + + ))} + + + ); +} diff --git a/components/media/detail/WatchNowPublisher.tsx b/components/media/detail/WatchNowPublisher.tsx new file mode 100644 index 00000000..e73af32d --- /dev/null +++ b/components/media/detail/WatchNowPublisher.tsx @@ -0,0 +1,37 @@ +import { WatchNowSeed } from '@/components/media/detail/WatchNowSeed'; +import { getCachedStreamingProviders } from '@/lib/data/watchlist'; +import { getAvailableProviders } from '@/lib/tmdb/providers'; +import { matchMyProviders } from '@/lib/watch-now'; +import type { WatchProvidersRegion } from '@/types/tmdb'; + +interface WatchNowPublisherProps { + providers: WatchProvidersRegion | null; + region: string; +} + +/** + * Resolves which of the user's own services carry this title and hands them to the play + * controls. Lives inside the "where to watch" section so the offers are fetched once, and so + * no extra streaming boundary is opened in the banner or the sticky bar. + */ +export async function WatchNowPublisher({ + providers, + region, +}: WatchNowPublisherProps) { + if (!providers?.flatrate?.length) return null; + + const myProviderIds = await getCachedStreamingProviders(); + if (myProviderIds.length === 0) return null; + + const available = await getAvailableProviders(region); + const mine = new Set(myProviderIds); + + const options = matchMyProviders( + providers.flatrate, + available.filter((provider) => mine.has(provider.provider_id)), + providers.link + ); + if (options.length === 0) return null; + + return ; +} diff --git a/components/media/detail/WatchNowSeed.tsx b/components/media/detail/WatchNowSeed.tsx new file mode 100644 index 00000000..0eba262c --- /dev/null +++ b/components/media/detail/WatchNowSeed.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { useEffect } from 'react'; +import { watchNowStore } from '@/lib/watch-now-store'; +import type { WatchNowOption } from '@/lib/watch-now'; + +/** Hands the server-resolved play options to every `WatchNowSlot` of the page. */ +export function WatchNowSeed({ options }: { options: WatchNowOption[] }) { + useEffect(() => { + watchNowStore.seed(options); + return () => watchNowStore.release(options); + }, [options]); + + return null; +} diff --git a/components/media/detail/WatchNowSlot.tsx b/components/media/detail/WatchNowSlot.tsx new file mode 100644 index 00000000..9c5fe36f --- /dev/null +++ b/components/media/detail/WatchNowSlot.tsx @@ -0,0 +1,46 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import { Play } from 'lucide-react'; +import { useTranslation } from '@/lib/i18n/context'; +import { useWatchNowOptions } from '@/lib/watch-now-store'; +import { watchNowClass, type WatchNowVariant } from '@/lib/watch-now'; + +const WatchNowMenu = dynamic(() => + import('@/components/media/detail/WatchNowMenu').then((m) => m.WatchNowMenu) +); + +/** + * Play control for the title being viewed: a direct link when a single one of the user's + * services carries it, a platform picker otherwise. Renders nothing until `WatchNowPublisher` + * has resolved the offers, and stays empty when none of them match. + */ +export function WatchNowSlot({ variant }: { variant: WatchNowVariant }) { + const { t } = useTranslation(); + const options = useWatchNowOptions(); + + if (options.length === 0) return null; + if (options.length > 1) { + return ; + } + + const [only] = options; + const label = t.movie.watchOn.replace('${provider}', only.providerName); + + return ( + + + + {label} + + + ); +} diff --git a/lib/i18n/messages/movie.ts b/lib/i18n/messages/movie.ts index 0e7c2c59..ea7d065c 100644 --- a/lib/i18n/messages/movie.ts +++ b/lib/i18n/messages/movie.ts @@ -158,6 +158,9 @@ const fr = { rent: 'Location', buy: 'Achat', noProviders: 'Non disponible dans ta région', + watchNow: 'Regarder', + watchOn: 'Regarder sur ${provider}', + chooseProvider: 'Où veux-tu le regarder ?', }; const en = { @@ -320,6 +323,9 @@ const en = { rent: 'Rent', buy: 'Buy', noProviders: 'Not available in your region', + watchNow: 'Watch', + watchOn: 'Watch on ${provider}', + chooseProvider: 'Where do you want to watch it?', } satisfies typeof fr; export const movie = { fr, en }; diff --git a/lib/watch-now-store.ts b/lib/watch-now-store.ts new file mode 100644 index 00000000..5f2bdff4 --- /dev/null +++ b/lib/watch-now-store.ts @@ -0,0 +1,45 @@ +import { useSyncExternalStore } from 'react'; +import type { WatchNowOption } from '@/lib/watch-now'; + +const EMPTY: WatchNowOption[] = []; + +let current: WatchNowOption[] = EMPTY; +const listeners = new Set<() => void>(); + +function emit() { + for (const listener of listeners) listener(); +} + +/** + * Play options of the detail page being viewed, resolved once by the banner and read by the + * sticky bar — which cannot stream its own copy without tearing the navbar portal it lives in. + */ +export const watchNowStore = { + seed(options: WatchNowOption[]) { + if (current === options) return; + current = options; + emit(); + }, + release(options: WatchNowOption[]) { + if (current !== options) return; + current = EMPTY; + emit(); + }, + get(): WatchNowOption[] { + return current; + }, + subscribe(listener: () => void) { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, +}; + +export function useWatchNowOptions(): WatchNowOption[] { + return useSyncExternalStore( + watchNowStore.subscribe, + watchNowStore.get, + () => EMPTY + ); +} From 308c885c0077f0e5b82bf3e712320b24f7d08fc4 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 05:26:38 +0200 Subject: [PATCH 57/98] =?UTF-8?q?feat(fiches):=20expose=20le=20bouton=20le?= =?UTF-8?q?cture=20sur=20film,=20s=C3=A9rie=20et=20saison?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bannière et barre collante des trois pages reçoivent `WatchNowSlot` - `WatchProviders` héberge le publisher : la section « Où regarder » charge déjà ces offres dans une frontière de streaming éprouvée, aucun fetch ni Suspense en plus - `SeasonBanner` gagne un slot `watchNowButton`, il n'en avait aucun - vérifié connecté : Breaking Bad → Netflix, Game of Thrones → HBO Max, Inception → menu Prime Video + HBO Max ; thèmes clair et sombre relus --- app/[lang]/movie/[id]/page.tsx | 24 ++++++++++++++----- app/[lang]/tv/[id]/page.tsx | 11 ++++++--- .../tv/[id]/season/[seasonNumber]/page.tsx | 3 +++ components/media/detail/WatchProviders.tsx | 2 ++ components/media/tv/SeasonBanner.tsx | 5 +++- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/app/[lang]/movie/[id]/page.tsx b/app/[lang]/movie/[id]/page.tsx index 1e28ddab..dcc5f548 100644 --- a/app/[lang]/movie/[id]/page.tsx +++ b/app/[lang]/movie/[id]/page.tsx @@ -19,6 +19,7 @@ import { MediaDetailLayout } from '@/components/media/detail/MediaDetailLayout'; import { SagaSection } from '@/components/media/detail/SagaSection'; import { SimilarSection } from '@/components/media/detail/SimilarSection'; import { WatchProviders } from '@/components/media/detail/WatchProviders'; +import { WatchNowSlot } from '@/components/media/detail/WatchNowSlot'; import { MediaTrailers } from '@/components/media/detail/MediaTrailers'; import { DetailSectionSkeleton, @@ -214,17 +215,28 @@ export default async function MoviePage(props: MoviePageProps) { } actions={ - }> - - + <> + + } + > + + + } /> ); const actionsBar = ( - }> - - + <> + + }> + + + ); return ( diff --git a/app/[lang]/tv/[id]/page.tsx b/app/[lang]/tv/[id]/page.tsx index 6668668e..da39ae50 100644 --- a/app/[lang]/tv/[id]/page.tsx +++ b/app/[lang]/tv/[id]/page.tsx @@ -18,6 +18,7 @@ import { TvWatchActions } from '@/components/media/detail/TvWatchActions'; import { MediaDetailLayout } from '@/components/media/detail/MediaDetailLayout'; import { SimilarSection } from '@/components/media/detail/SimilarSection'; import { WatchProviders } from '@/components/media/detail/WatchProviders'; +import { WatchNowSlot } from '@/components/media/detail/WatchNowSlot'; import { MediaTrailers } from '@/components/media/detail/MediaTrailers'; import { DetailSectionSkeleton, @@ -282,6 +283,7 @@ export default async function TvShowPage(props: TvPageProps) { } actions={
+
}> - - + <> + + }> + + + ); const seasonsSection = diff --git a/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx b/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx index 08d0a2a9..98c35ac6 100644 --- a/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx +++ b/app/[lang]/tv/[id]/season/[seasonNumber]/page.tsx @@ -21,6 +21,7 @@ import { MediaActionsBar } from '@/components/media/detail/MediaActionsBar'; import { SectionHeading } from '@/components/ui/SectionHeading'; import { CommunityRating } from '@/components/media/detail/CommunityRating'; import { WatchProviders } from '@/components/media/detail/WatchProviders'; +import { WatchNowSlot } from '@/components/media/detail/WatchNowSlot'; import { DetailSectionSkeleton } from '@/components/media/detail/MediaDetailSkeleton'; import { SeasonEpisodesList } from '@/components/media/tv/SeasonEpisodesList'; import { localizedAlternates } from '@/lib/metadata'; @@ -173,9 +174,11 @@ export default async function SeasonPage(props: SeasonPageProps) { totalEpisodes={totalEpisodes} genres={tvDetails.genres} rating={seasonRating} + watchNowButton={} /> + + {td.whereToWatch} {!providers || diff --git a/components/media/tv/SeasonBanner.tsx b/components/media/tv/SeasonBanner.tsx index 530ad160..0a13f284 100644 --- a/components/media/tv/SeasonBanner.tsx +++ b/components/media/tv/SeasonBanner.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, type ReactNode } from 'react'; import Image from 'next/image'; import Link from 'next/link'; import { ArrowLeft, Calendar, Layers } from 'lucide-react'; @@ -30,6 +30,7 @@ interface SeasonBannerProps { totalEpisodes: number; genres: { id: number; name: string }[]; rating: { avg: number; count: number } | null; + watchNowButton?: ReactNode; } /** @@ -49,6 +50,7 @@ export function SeasonBanner({ totalEpisodes, genres, rating, + watchNowButton, }: SeasonBannerProps) { const { t, lang } = useTranslation(); const locale = getLocale(lang); @@ -160,6 +162,7 @@ export function SeasonBanner({ ref={bottomRef} className="flex flex-col sm:flex-row sm:items-center gap-3 mt-2" > + {watchNowButton} Date: Mon, 27 Jul 2026 16:24:32 +0200 Subject: [PATCH 58/98] =?UTF-8?q?fix(e2e):=20n'assimile=20plus=20la=20t?= =?UTF-8?q?=C3=A9l=C3=A9m=C3=A9trie=20Sentry=20=C3=A0=20une=20action=20ser?= =?UTF-8?q?veur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - le test tactile comptait tout POST comme Server Action, or Sentry en émet 6 vers sentry.silexio.be pendant le chargement de la fiche - il échouait donc sur un tap parfaitement innocent : aucun POST ne partait vers localhost, le comportement testé était correct depuis le début - le filtre se limite désormais à l'origine du site, via la fixture `baseURL` - suite E2E de nouveau verte : 46 passés, 1 sauté, 0 échec --- tests/e2e/media-card-tap.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/e2e/media-card-tap.spec.ts b/tests/e2e/media-card-tap.spec.ts index 7a0d95e8..d56ce4f5 100644 --- a/tests/e2e/media-card-tap.spec.ts +++ b/tests/e2e/media-card-tap.spec.ts @@ -9,10 +9,16 @@ test.use({ test.describe('MediaCard on touch devices', () => { test('tapping the bottom of a card navigates instead of firing a watchlist action', async ({ page, + baseURL, }) => { + // Scoped to our own origin: Sentry ships its envelopes over POST too, and counting + // those made the assertion fail on a perfectly innocent tap. + const origin = new URL(baseURL ?? 'http://localhost:3000').origin; const serverActions: string[] = []; page.on('request', (request) => { - if (request.method() === 'POST') serverActions.push(request.url()); + const url = request.url(); + if (request.method() === 'POST' && url.startsWith(origin)) + serverActions.push(url); }); await page.goto('/en/movie/550'); From b3ac5c5bd0fb4c8bb1e599fdc9c702222c76dd24 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 17:30:30 +0200 Subject: [PATCH 59/98] =?UTF-8?q?fix(watch-now):=20durcit=20le=20lien=20de?= =?UTF-8?q?=20lecture=20et=20isole=20la=20section=20=C2=AB=20O=C3=B9=20reg?= =?UTF-8?q?arder=20=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `web_url` vient de Watchmode et atterrissait tel quel dans un `href` : un schéma `javascript:` s'y exécutait au clic. Seuls `http(s)` passent désormais, sinon on retombe sur la page du titre, et rien ne s'affiche si aucun lien n'est sain - le publisher ajoutait une dépendance Supabase à la section « Où regarder », qui n'en avait aucune : un incident d'auth emportait toute la section. La résolution est isolée dans `resolveMyOptions`, échec silencieux via `reportSwallowed` - le try/catch n'entoure que les appels réseau, jamais le JSX : React ne rend pas le composant sur place, une erreur de rendu n'y serait pas attrapée - 3 tests couvrent `javascript:`, `data:`, les URL protocol-relative et `nflx://` --- components/media/detail/WatchNowPublisher.tsx | 41 +++++++++++++------ lib/watch-now.ts | 11 ++++- tests/unit/watch-now.test.ts | 30 ++++++++++++++ 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/components/media/detail/WatchNowPublisher.tsx b/components/media/detail/WatchNowPublisher.tsx index e73af32d..71d14a38 100644 --- a/components/media/detail/WatchNowPublisher.tsx +++ b/components/media/detail/WatchNowPublisher.tsx @@ -1,7 +1,8 @@ import { WatchNowSeed } from '@/components/media/detail/WatchNowSeed'; import { getCachedStreamingProviders } from '@/lib/data/watchlist'; import { getAvailableProviders } from '@/lib/tmdb/providers'; -import { matchMyProviders } from '@/lib/watch-now'; +import { reportSwallowed } from '@/lib/report'; +import { matchMyProviders, type WatchNowOption } from '@/lib/watch-now'; import type { WatchProvidersRegion } from '@/types/tmdb'; interface WatchNowPublisherProps { @@ -9,6 +10,32 @@ interface WatchNowPublisherProps { region: string; } +/** + * Hosting the play options inside "where to watch" must never make that section depend on the + * session being readable, so a failure here degrades to no play button instead of no section. + */ +async function resolveMyOptions( + providers: WatchProvidersRegion, + region: string +): Promise { + try { + const myProviderIds = await getCachedStreamingProviders(); + if (myProviderIds.length === 0) return []; + + const available = await getAvailableProviders(region); + const mine = new Set(myProviderIds); + + return matchMyProviders( + providers.flatrate, + available.filter((provider) => mine.has(provider.provider_id)), + providers.link + ); + } catch (error) { + reportSwallowed('watch-now:publisher', error); + return []; + } +} + /** * Resolves which of the user's own services carry this title and hands them to the play * controls. Lives inside the "where to watch" section so the offers are fetched once, and so @@ -20,17 +47,7 @@ export async function WatchNowPublisher({ }: WatchNowPublisherProps) { if (!providers?.flatrate?.length) return null; - const myProviderIds = await getCachedStreamingProviders(); - if (myProviderIds.length === 0) return null; - - const available = await getAvailableProviders(region); - const mine = new Set(myProviderIds); - - const options = matchMyProviders( - providers.flatrate, - available.filter((provider) => mine.has(provider.provider_id)), - providers.link - ); + const options = await resolveMyOptions(providers, region); if (options.length === 0) return null; return ; diff --git a/lib/watch-now.ts b/lib/watch-now.ts index 9feb1aa6..f61357bb 100644 --- a/lib/watch-now.ts +++ b/lib/watch-now.ts @@ -10,6 +10,14 @@ export interface WatchNowOption { export type WatchNowVariant = 'banner' | 'bar'; +/** + * Offer links come from third parties and land in an `href`, where a `javascript:` scheme + * would execute on click. Only plain web links get through. + */ +function webLinkOrNull(url: string | undefined): string | null { + return url && /^https?:\/\//i.test(url) ? url : null; +} + /** * The user's own streaming services that carry this title, with the deepest link available. * Subscription offers only: renting a title is not "watching it on your platform". @@ -33,7 +41,8 @@ export function matchMyProviders( ); if (!owned) continue; - const href = offer.web_url || fallbackHref; + const href = + webLinkOrNull(offer.web_url) ?? webLinkOrNull(fallbackHref); if (!href) continue; const key = providerNameKey(owned.provider_name); diff --git a/tests/unit/watch-now.test.ts b/tests/unit/watch-now.test.ts index c6f404b6..5a59b2ce 100644 --- a/tests/unit/watch-now.test.ts +++ b/tests/unit/watch-now.test.ts @@ -103,6 +103,36 @@ describe('matchMyProviders', () => { ).toHaveLength(0); }); + it('refuses a javascript: link and falls back to the title page', () => { + const options = matchMyProviders( + [provider('Netflix', { web_url: 'javascript:alert(1)' })], + [provider('Netflix')], + JUSTWATCH + ); + + expect(options[0].href).toBe(JUSTWATCH); + }); + + it('drops the offer when every candidate link has a dangerous scheme', () => { + expect( + matchMyProviders( + [provider('Netflix', { web_url: 'javascript:alert(1)' })], + [provider('Netflix')], + 'data:text/html,' + ) + ).toHaveLength(0); + }); + + it('keeps a protocol-relative or app-scheme link out of the href', () => { + expect( + matchMyProviders( + [provider('Netflix', { web_url: 'nflx://title/1' })], + [provider('Netflix')], + '//evil.example/x' + ) + ).toHaveLength(0); + }); + it('deduplicates a service listed twice by the offers', () => { const options = matchMyProviders( [ From 0c3182b45d0ef1b4944da33d85e3400a1d5bb719 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 17:34:48 +0200 Subject: [PATCH 60/98] =?UTF-8?q?fix(providers):=20assainit=20aussi=20les?= =?UTF-8?q?=20liens=20des=20logos=20=C2=AB=20O=C3=B9=20regarder=20=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - le durcissement précédent ne couvrait que le bouton lecture ; les logos de la section consomment le même `web_url` Watchmode et l'injectaient tel quel dans un `href`, donc la faille `javascript:` restait ouverte juste à côté - le garde sort dans `lib/safe-link.ts`, partagé par les deux chemins plutôt que dupliqué — et réutilisable pour les futurs flux partenaires - 3 tests directs sur le garde : schémas exécutables, protocol-relative, relatifs --- components/media/detail/WatchProviders.tsx | 3 +- lib/safe-link.ts | 8 +++++ lib/watch-now.ts | 9 +---- tests/unit/safe-link.test.ts | 38 ++++++++++++++++++++++ 4 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 lib/safe-link.ts create mode 100644 tests/unit/safe-link.test.ts diff --git a/components/media/detail/WatchProviders.tsx b/components/media/detail/WatchProviders.tsx index 70bab954..cc42bf15 100644 --- a/components/media/detail/WatchProviders.tsx +++ b/components/media/detail/WatchProviders.tsx @@ -8,6 +8,7 @@ import { normalizeName, } from '@/lib/tmdb/watch-providers'; import { getAppStoreIconMap } from '@/lib/watchmode/app-store'; +import { webLinkOrNull } from '@/lib/safe-link'; import { WatchNowPublisher } from '@/components/media/detail/WatchNowPublisher'; import type { WatchProvidersRegion, WatchProvider } from '@/types/tmdb'; @@ -110,7 +111,7 @@ function ProviderLogo({ // Watchmode fournit l'URL profonde vers la plateforme, mais son quota est vite épuisé ; // on retombe alors sur les données TMDB, dépourvues d'URL par plateforme. Sans ce repli // vers la page JustWatch du titre, les logos cessaient d'être des liens du tout. - const href = p.web_url || fallbackUrl; + const href = webLinkOrNull(p.web_url) ?? webLinkOrNull(fallbackUrl); if (href) { // Le logo ne fait que 40 px : le pseudo-élément porte la cible tactile à 48 px sans diff --git a/lib/safe-link.ts b/lib/safe-link.ts new file mode 100644 index 00000000..dd6ada5c --- /dev/null +++ b/lib/safe-link.ts @@ -0,0 +1,8 @@ +/** + * A third-party URL cleared for use in an `href`, or null. + * Watch providers, and later any partner feed, hand us links we did not author: a + * `javascript:` or `data:` scheme placed in an anchor executes on click. + */ +export function webLinkOrNull(url: string | undefined): string | null { + return url && /^https?:\/\//i.test(url) ? url : null; +} diff --git a/lib/watch-now.ts b/lib/watch-now.ts index f61357bb..8d51bb52 100644 --- a/lib/watch-now.ts +++ b/lib/watch-now.ts @@ -1,4 +1,5 @@ import { isSameProvider, providerNameKey } from '@/lib/provider-identity'; +import { webLinkOrNull } from '@/lib/safe-link'; import type { WatchProvider } from '@/types/tmdb'; export interface WatchNowOption { @@ -10,14 +11,6 @@ export interface WatchNowOption { export type WatchNowVariant = 'banner' | 'bar'; -/** - * Offer links come from third parties and land in an `href`, where a `javascript:` scheme - * would execute on click. Only plain web links get through. - */ -function webLinkOrNull(url: string | undefined): string | null { - return url && /^https?:\/\//i.test(url) ? url : null; -} - /** * The user's own streaming services that carry this title, with the deepest link available. * Subscription offers only: renting a title is not "watching it on your platform". diff --git a/tests/unit/safe-link.test.ts b/tests/unit/safe-link.test.ts new file mode 100644 index 00000000..7211e722 --- /dev/null +++ b/tests/unit/safe-link.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; +import { webLinkOrNull } from '@/lib/safe-link'; + +describe('webLinkOrNull', () => { + it('lets plain web links through untouched', () => { + for (const url of [ + 'https://www.netflix.com/title/70143836', + 'http://example.com/x?a=1#b', + 'HTTPS://EXAMPLE.COM/X', + ]) { + expect(webLinkOrNull(url)).toBe(url); + } + }); + + it('rejects schemes that execute when placed in an href', () => { + for (const url of [ + 'javascript:alert(1)', + 'JaVaScRiPt:alert(1)', + 'data:text/html,', + 'vbscript:msgbox(1)', + ]) { + expect(webLinkOrNull(url)).toBeNull(); + } + }); + + it('rejects anything that is not an absolute web URL', () => { + for (const url of [ + '//evil.example/x', + 'nflx://title/1', + '/relative/path', + 'ftp://example.com/f', + '', + undefined, + ]) { + expect(webLinkOrNull(url)).toBeNull(); + } + }); +}); From d63c5e9e9ff0a1cc4527ab23f931d407330c1838 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Mon, 27 Jul 2026 23:52:25 +0200 Subject: [PATCH 61/98] fix(i18n): un segment de langue inconnu ne fait plus planter le rendu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `generateMetadata` reçoit le segment `[lang]` brut et s'exécute AVANT que le layout puisse le `notFound()` : les 26 routes `[lang]` faisaient donc confiance à l'URL - `getTranslations` renvoyait `undefined` pour une langue inconnue, et le premier `t.xxx` levait — constaté en vrai sur `/sw.js` (TypeError reading 'metadata') - le garde tombe dans le resolver plutôt que dans 26 fichiers ; le layout continue de répondre 404, seul le crash disparaît - retire 4 alias de plateforme qui se mappaient sur eux-mêmes, donc sans effet (`providerNameKey` fait déjà `?? key`), et verrouille le comportement par 4 tests --- lib/i18n/server.ts | 12 +++++-- lib/provider-identity.ts | 4 --- tests/unit/provider-identity.test.ts | 51 ++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 tests/unit/provider-identity.test.ts diff --git a/lib/i18n/server.ts b/lib/i18n/server.ts index bc8c8c17..0e5102df 100644 --- a/lib/i18n/server.ts +++ b/lib/i18n/server.ts @@ -29,9 +29,17 @@ export const getServerLanguage = cache(async (): Promise => { return DEFAULT_LANGUAGE; }); -/** Returns the translation object for the given language, or the request-resolved one when omitted. */ +/** + * Returns the translation object for the given language, or the request-resolved one when the + * argument is missing or not a supported language. + * + * The guard is not decorative: `generateMetadata` receives the raw `[lang]` URL segment and runs + * before the layout can `notFound()` an unknown one, so any unmatched single-segment path — a + * missing static file, a bot probing `/config.json` — reached this with garbage and crashed the + * render on `undefined`. + */ export const getTranslations = cache(async (lang?: Language) => { - const resolved = lang ?? (await getServerLanguage()); + const resolved = isLanguage(lang) ? lang : await getServerLanguage(); return translations[resolved]; }); diff --git a/lib/provider-identity.ts b/lib/provider-identity.ts index d17c90bb..b77dfb5d 100644 --- a/lib/provider-identity.ts +++ b/lib/provider-identity.ts @@ -3,14 +3,10 @@ const NAME_ALIASES: Record = { amazonprime: 'amazonprimevideo', hbomax: 'max', hbo: 'max', - paramountplus: 'paramountplus', paramount: 'paramountplus', - peacockpremium: 'peacockpremium', peacock: 'peacockpremium', - nowtv: 'nowtv', now: 'nowtv', disney: 'disneyplus', - disneyplus: 'disneyplus', }; const PREFIX_MATCH_MIN_LENGTH = 5; diff --git a/tests/unit/provider-identity.test.ts b/tests/unit/provider-identity.test.ts new file mode 100644 index 00000000..ccc48f19 --- /dev/null +++ b/tests/unit/provider-identity.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest'; +import { + isSameProvider, + normalizeName, + providerNameKey, +} from '@/lib/provider-identity'; + +describe('providerNameKey', () => { + it('folds the spellings a service is known by onto one key', () => { + expect(providerNameKey('HBO Max')).toBe('max'); + expect(providerNameKey('Max')).toBe('max'); + expect(providerNameKey('Amazon Prime')).toBe('amazonprimevideo'); + expect(providerNameKey('Amazon Prime Video')).toBe('amazonprimevideo'); + }); + + it('resolves a short spelling and its full name to the same key', () => { + for (const [short, full] of [ + ['Paramount', 'Paramount Plus'], + ['Peacock', 'Peacock Premium'], + ['Now', 'Now TV'], + ['Disney', 'Disney Plus'], + ]) { + expect(providerNameKey(short)).toBe(providerNameKey(full)); + } + }); + + it('leaves an unaliased name at its normalized form', () => { + expect(providerNameKey('Netflix')).toBe('netflix'); + expect(providerNameKey('Crunchyroll')).toBe('crunchyroll'); + }); +}); + +describe('normalizeName', () => { + it('strips case and every non-alphanumeric character', () => { + expect(normalizeName('Apple TV+')).toBe('appletv'); + expect(normalizeName(' Rakuten-TV ')).toBe('rakutentv'); + }); +}); + +describe('isSameProvider', () => { + it('matches across spellings and suffixed variants', () => { + expect(isSameProvider('HBO Max', 'Max')).toBe(true); + expect(isSameProvider('Netflix Standard with Ads', 'Netflix')).toBe( + true + ); + }); + + it('refuses to match two short unrelated names', () => { + expect(isSameProvider('OCS', 'OCS Go')).toBe(false); + }); +}); From 71c0c0aad42bb44415447d112038e1aa28608ccd Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Tue, 28 Jul 2026 00:23:41 +0200 Subject: [PATCH 62/98] =?UTF-8?q?fix(notifications):=20affiche=20la=20vrai?= =?UTF-8?q?e=20photo=20de=20profil=20de=20l'exp=C3=A9diteur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `NotificationItem` et `NotificationToast` appelaient `UserAvatar` sans `picture`, d'où les initiales génériques quel que soit l'expéditeur - la requête ne lisait pas `sender_id` et n'allait jamais chercher l'avatar ; elle le résout maintenant en une seconde requête groupée sur `user_profiles`, comme le fait déjà `lib/data/friends.ts` — PostgREST ne peut pas joindre, il n'y a pas de FK - le temps réel ne reçoit que la ligne brute : l'avatar y est résolu côté client, et seulement pour les notifications d'ami, les autres affichant l'affiche du média - `rowToAppNotification` prend l'avatar en second paramètre optionnel, donc les appelants qui ne peuvent rien résoudre restent corrects - 2 tests couvrent l'avatar porté et le repli null --- components/notifications/NotificationItem.tsx | 1 + .../notifications/NotificationToast.tsx | 1 + .../notifications/NotificationsProvider.tsx | 104 +++++++++++------- lib/data/notifications.ts | 37 ++++++- lib/notifications.ts | 39 ++++--- tests/unit/notifications-mapper.test.ts | 32 +++++- types/notifications.ts | 1 + 7 files changed, 158 insertions(+), 57 deletions(-) diff --git a/components/notifications/NotificationItem.tsx b/components/notifications/NotificationItem.tsx index 55678630..c20822f1 100644 --- a/components/notifications/NotificationItem.tsx +++ b/components/notifications/NotificationItem.tsx @@ -31,6 +31,7 @@ export function NotificationItem({ const visual = isFriend ? ( [0] & { + sender_id?: string; +}; + export function NotificationsProvider({ userId, initialUnreadCount, @@ -67,6 +73,50 @@ export function NotificationsProvider({ useEffect(() => { const supabase = createClient(); + + function showToast(n: AppNotification) { + const url = n.url; + toast.custom((id) => ( + { + toast.dismiss(id); + setUnreadCount((c) => Math.max(0, c - 1)); + void markNotificationRead(n.id).catch( + (error) => + reportSwallowed( + 'notifications:markRead', + error + ) + ); + router.push(localizedHref(lang, url)); + } + : undefined + } + /> + )); + } + + /** + * The realtime payload carries `sender_id`, never the picture, so the avatar costs one + * extra read — spent only on the notifications that actually show a face. + */ + async function resolveSenderAvatar( + row: NotificationRow + ): Promise { + if (!row.type.startsWith('friend') || !row.sender_id) return null; + const { data } = await supabase + .from('user_profiles') + .select('avatar_url') + .eq('user_id', row.sender_id) + .maybeSingle(); + return resolveAvatarUrl(data?.avatar_url, null); + } + const channel = supabase .channel(`notifications:${userId}`) .on( @@ -79,45 +129,21 @@ export function NotificationsProvider({ }, (payload) => { setUnreadCount((c) => c + 1); - const n = rowToAppNotification( - payload.new as Parameters< - typeof rowToAppNotification - >[0] - ); - if (n.type === 'friend_request') promptStore.requestPush(); - - const url = n.url; - toast.custom((id) => ( - { - toast.dismiss(id); - setUnreadCount((c) => - Math.max(0, c - 1) - ); - void markNotificationRead( - n.id - ).catch((error) => - reportSwallowed( - 'notifications:markRead', - error - ) - ); - router.push( - localizedHref(lang, url) - ); - } - : undefined - } - /> - )); + const row = payload.new as NotificationRow; + if (row.type === 'friend_request') + promptStore.requestPush(); + + void resolveSenderAvatar(row) + .catch((error) => { + reportSwallowed( + 'notifications:senderAvatar', + error + ); + return null; + }) + .then((avatarUrl) => + showToast(rowToAppNotification(row, avatarUrl)) + ); } ) .on( diff --git a/lib/data/notifications.ts b/lib/data/notifications.ts index 5885dc02..c566b2fe 100644 --- a/lib/data/notifications.ts +++ b/lib/data/notifications.ts @@ -1,7 +1,9 @@ import 'server-only'; import { getAuthenticatedUser } from '@/lib/supabase/auth-helpers'; +import { resolveAvatarUrl } from '@/lib/avatar'; import { rowToAppNotification } from '@/lib/notifications'; +import type { SupabaseClient } from '@supabase/supabase-js'; import { DEFAULT_NOTIFICATION_PREFERENCES, type AppNotification, @@ -9,7 +11,30 @@ import { } from '@/types/notifications'; const NOTIFICATION_COLUMNS = - 'id, type, sender_username, media_id, media_type, media_title, poster_path, season_number, episode_number, url, read_at, created_at'; + 'id, type, sender_id, sender_username, media_id, media_type, media_title, poster_path, season_number, episode_number, url, read_at, created_at'; + +/** + * Senders' avatars keyed by user id. Fetched separately rather than embedded: `sender_id` has + * no declared foreign key to `user_profiles`, so PostgREST cannot join the two. + */ +async function getSenderAvatars( + supabase: SupabaseClient, + senderIds: string[] +): Promise> { + if (senderIds.length === 0) return new Map(); + + const { data } = await supabase + .from('user_profiles') + .select('user_id, avatar_url') + .in('user_id', senderIds); + + const avatars = new Map(); + for (const profile of data ?? []) { + const url = resolveAvatarUrl(profile.avatar_url, null); + if (url) avatars.set(profile.user_id, url); + } + return avatars; +} /** Returns the authenticated user's notifications, newest first. */ export async function getNotifications(limit = 30): Promise { @@ -21,7 +46,15 @@ export async function getNotifications(limit = 30): Promise { .order('created_at', { ascending: false }) .limit(limit); if (error) throw new Error(error.message); - return (data ?? []).map(rowToAppNotification); + + const rows = data ?? []; + const avatars = await getSenderAvatars(supabase, [ + ...new Set(rows.map((row) => row.sender_id).filter(Boolean)), + ]); + + return rows.map((row) => + rowToAppNotification(row, avatars.get(row.sender_id) ?? null) + ); } /** Returns the count of unread notifications for the authenticated user. */ diff --git a/lib/notifications.ts b/lib/notifications.ts index 988a8b98..72a27d26 100644 --- a/lib/notifications.ts +++ b/lib/notifications.ts @@ -14,25 +14,34 @@ export function notificationMessage( .replace('{episode}', String(n.episodeNumber ?? '')); } -/** Maps a Supabase notifications row to the camelCase app shape. */ -export function rowToAppNotification(row: { - id: string; - type: string; - sender_username: string | null; - media_id: number | null; - media_type: string | null; - media_title: string | null; - poster_path: string | null; - season_number: number | null; - episode_number: number | null; - url: string | null; - read_at: string | null; - created_at: string; -}): AppNotification { +/** + * Maps a Supabase notifications row to the camelCase app shape. + * + * @param senderAvatarUrl - Looked up by the caller: the row carries `sender_id`, never the + * picture, and the realtime payload cannot join, so each caller passes what it could resolve. + */ +export function rowToAppNotification( + row: { + id: string; + type: string; + sender_username: string | null; + media_id: number | null; + media_type: string | null; + media_title: string | null; + poster_path: string | null; + season_number: number | null; + episode_number: number | null; + url: string | null; + read_at: string | null; + created_at: string; + }, + senderAvatarUrl: string | null = null +): AppNotification { return { id: row.id, type: row.type as NotificationType, senderUsername: row.sender_username, + senderAvatarUrl, mediaId: row.media_id, mediaType: row.media_type as 'movie' | 'tv' | null, mediaTitle: row.media_title, diff --git a/tests/unit/notifications-mapper.test.ts b/tests/unit/notifications-mapper.test.ts index 3a37fa54..14acd4b7 100644 --- a/tests/unit/notifications-mapper.test.ts +++ b/tests/unit/notifications-mapper.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { notificationMessage } from '@/lib/notifications'; +import { notificationMessage, rowToAppNotification } from '@/lib/notifications'; import type { AppNotification } from '@/types/notifications'; const T = { @@ -13,6 +13,7 @@ const base: AppNotification = { id: '1', type: 'friend_request', senderUsername: 'neo', + senderAvatarUrl: null, mediaId: null, mediaType: null, mediaTitle: null, @@ -41,3 +42,32 @@ describe('notificationMessage', () => { expect(notificationMessage(n, T)).toBe('Nouvel épisode de Dark : S2E5'); }); }); + +const row = { + id: '1', + type: 'friend_request', + sender_username: 'neo', + media_id: null, + media_type: null, + media_title: null, + poster_path: null, + season_number: null, + episode_number: null, + url: '/profile/neo', + read_at: null, + created_at: '2026-07-28T00:00:00.000Z', +}; + +describe('rowToAppNotification', () => { + it("porte l'avatar de l'expéditeur quand l'appelant a pu le résoudre", () => { + const avatar = 'https://example.com/neo.jpg'; + expect(rowToAppNotification(row, avatar).senderAvatarUrl).toBe(avatar); + }); + + it("retombe sur null quand l'avatar est inconnu, sans casser le mapping", () => { + const n = rowToAppNotification(row); + expect(n.senderAvatarUrl).toBeNull(); + expect(n.senderUsername).toBe('neo'); + expect(n.url).toBe('/profile/neo'); + }); +}); diff --git a/types/notifications.ts b/types/notifications.ts index d702a5ef..a1fdfea1 100644 --- a/types/notifications.ts +++ b/types/notifications.ts @@ -11,6 +11,7 @@ export interface AppNotification { id: string; type: NotificationType; senderUsername: string | null; + senderAvatarUrl: string | null; mediaId: number | null; mediaType: 'movie' | 'tv' | null; mediaTitle: string | null; From 7d6a0a381aba909bde00a3ab311912760b55e2a6 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Tue, 28 Jul 2026 00:38:31 +0200 Subject: [PATCH 63/98] fix(a11y): rouvre le pincement pour zoomer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `maximumScale: 1` + `userScalable: false` verrouillaient le zoom : échec du critère WCAG 1.4.4, mesuré par Lighthouse comme le seul défaut d'accessibilité des pages - la seule raison de verrouiller serait le zoom iOS au focus d'un champ ; il ne se déclenche pas ici, `input` et `textarea` sont en `text-base` (16 px) sur mobile et ne passent en `text-sm` qu'à partir de 768 px --- app/[lang]/layout.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx index 77b72593..9fa2bb1b 100644 --- a/app/[lang]/layout.tsx +++ b/app/[lang]/layout.tsx @@ -36,11 +36,12 @@ const display = Bebas_Neue({ variable: '--font-display', }); +// Le pincement pour zoomer reste ouvert : le verrouiller échoue au critère WCAG 1.4.4 et +// prive les malvoyants d'agrandir. Le zoom iOS au focus, seule raison de le verrouiller, ne +// se déclenche pas ici — les champs sont en `text-base` (16 px) sur mobile. export const viewport: Viewport = { width: 'device-width', initialScale: 1, - maximumScale: 1, - userScalable: false, viewportFit: 'cover', interactiveWidget: 'resizes-content', colorScheme: 'dark light', From d7cee58734fc95ec3c8c033ab474af69e1e0e899 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Tue, 28 Jul 2026 02:39:43 +0200 Subject: [PATCH 64/98] fix(csp): Autoriser le chargement des images TMDB via fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute `image.tmdb.org` à la directive `connect-src` de la Content Security Policy. Ceci corrige une erreur `FETCH API cannot load...` qui empêchait l'affichage des affiches de films/séries sur les navigateurs basés sur Chromium (Chrome, Brave) lorsque les images sont récupérées via JavaScript. --- next.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.ts b/next.config.ts index a17179f6..47055642 100644 --- a/next.config.ts +++ b/next.config.ts @@ -30,7 +30,7 @@ const cspDirectives = [ "img-src 'self' data: blob: https://image.tmdb.org https://i.ytimg.com https://lh3.googleusercontent.com https://api.dicebear.com https://*.supabase.co https://cdn.watchmode.com https://*.mzstatic.com", "font-src 'self' data: https://fonts.gstatic.com", 'frame-src https://www.youtube.com https://www.youtube-nocookie.com', - `connect-src 'self' https://*.supabase.co https://api.themoviedb.org https://www.youtube.com https://sentry.silexio.be${isDev ? ' ws: wss:' : ' wss:'}`, + `connect-src 'self' https://*.supabase.co https://api.themoviedb.org https://image.tmdb.org https://api.watchmode.com https://www.youtube.com https://sentry.silexio.be${isDev ? ' ws: wss:' : ' wss:'}`, "object-src 'none'", "base-uri 'self'", "form-action 'self'", From d39fa90e77a3fc0224cadb652b048e3ce93e9995 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Tue, 28 Jul 2026 04:41:27 +0200 Subject: [PATCH 65/98] =?UTF-8?q?fix(ui):=20mat=C3=A9riaux=20verre=20lisib?= =?UTF-8?q?les=20sans=20backdrop-filter=20+=20miniature=20de=20trailer=20r?= =?UTF-8?q?=C3=A9siliente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un fond translucide sans backdrop-filter laisse défiler la page au travers : Chrome sur GPU blocklisté ou accélération coupée, WebView Android ancienne, Firefox où la propriété est désactivée. glass-bar, glass-popover et glass-overlay se posent sur du média ou du texte arbitraire et repassent en surface opaque. La miniature de bande-annonce dépendait d'un domaine tiers unique : quand i.ytimg.com est filtré côté client, la carte tombait sur un aplat vide. Elle bascule maintenant sur le backdrop TMDB, puis sur le placeholder local. Le commentaire sur l'ordre des préfixes disait l'inverse de ce que fait lightningcss, qui dérive -webkit-backdrop-filter des cibles de build. --- app/[lang]/movie/[id]/page.tsx | 10 ++++++++-- app/[lang]/tv/[id]/page.tsx | 10 ++++++++-- app/globals.css | 21 +++++++++++++++++++-- components/media/detail/MediaTrailers.tsx | 14 ++++++++++++-- types/components.ts | 1 + 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/app/[lang]/movie/[id]/page.tsx b/app/[lang]/movie/[id]/page.tsx index dcc5f548..54134df9 100644 --- a/app/[lang]/movie/[id]/page.tsx +++ b/app/[lang]/movie/[id]/page.tsx @@ -72,14 +72,16 @@ async function MovieProvidersSection({ async function MovieTrailersSection({ movieId, lang, + backdropPath, }: { movieId: number; lang: Language; + backdropPath: string | null; }) { const videos = await getMovieVideos(movieId, lang); const trailers = await filterAvailableVideos(filterTrailers(videos)); if (trailers.length === 0) return null; - return ; + return ; } async function MovieUserActions({ @@ -276,7 +278,11 @@ export default async function MoviePage(props: MoviePageProps) { } trailers={ }> - + } relatedSections={ diff --git a/app/[lang]/tv/[id]/page.tsx b/app/[lang]/tv/[id]/page.tsx index da39ae50..91a757e6 100644 --- a/app/[lang]/tv/[id]/page.tsx +++ b/app/[lang]/tv/[id]/page.tsx @@ -81,14 +81,16 @@ async function TvProvidersSection({ async function TvTrailersSection({ tvId, lang, + backdropPath, }: { tvId: number; lang: Language; + backdropPath: string | null; }) { const videos = await getTvShowVideos(tvId, lang); const trailers = await filterAvailableVideos(filterTrailers(videos)); if (trailers.length === 0) return null; - return ; + return ; } async function TvUserActions({ @@ -381,7 +383,11 @@ export default async function TvShowPage(props: TvPageProps) { } trailers={ }> - + } cast={credits.cast} diff --git a/app/globals.css b/app/globals.css index cfed1722..2c7076f9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -434,8 +434,9 @@ a { } @layer components { - /* -webkit- MUST come before the standard property: lightningcss merges the - pair and keeps only the LAST form (Chrome 2026 dropped the -webkit- alias). */ + /* lightningcss derives -webkit-backdrop-filter from the build targets on its own. + Written by hand it MUST come before the standard property: the pair is merged + and only the LAST form survives. */ .glass-surface { --glass-filter: blur(24px) saturate(150%); background-color: var(--glass-bg-hover); @@ -487,6 +488,22 @@ a { border-color: var(--glass-border-hover); } + /* Without backdrop-filter — Chrome on a blocklisted GPU or with acceleration off, + older Android WebViews, Firefox where the property is disabled — a translucent + fill lets the page scroll through. These materials sit over arbitrary media or + text, so they fall back to an opaque surface rather than stay unreadable. */ + @supports not ( + (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) + ) { + .glass-bar, + .glass-popover, + .glass-overlay, + .glass-overlay-button, + .glass-overlay-button:hover { + background-color: var(--surface); + } + } + .top-nav-safe-area { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); diff --git a/components/media/detail/MediaTrailers.tsx b/components/media/detail/MediaTrailers.tsx index 4bf6d18d..4b476e2e 100644 --- a/components/media/detail/MediaTrailers.tsx +++ b/components/media/detail/MediaTrailers.tsx @@ -5,9 +5,10 @@ import Image from 'next/image'; import { Play } from 'lucide-react'; import { useTranslation } from '@/lib/i18n/context'; import { SectionHeading } from '@/components/ui/SectionHeading'; +import { getImageUrl } from '@/lib/tmdb/images'; import type { MediaTrailersProps } from '@/types/components'; -export function MediaTrailers({ trailers }: MediaTrailersProps) { +export function MediaTrailers({ trailers, backdropPath }: MediaTrailersProps) { const { t } = useTranslation(); const [failedKeys, setFailedKeys] = useState>(new Set()); @@ -32,6 +33,7 @@ export function MediaTrailers({ trailers }: MediaTrailersProps) { videoKey={trailer.key} title={trailer.name} site={trailer.site} + backdropPath={backdropPath} onError={handleIframeError} unsupportedLabel={t.movie.unsupportedFormat} /> @@ -45,16 +47,19 @@ function TrailerEmbed({ videoKey, title, site, + backdropPath, onError, unsupportedLabel, }: { videoKey: string; title: string; site: string; + backdropPath: string | null; onError: (key: string) => void; unsupportedLabel: string; }) { const [isPlaying, setIsPlaying] = useState(false); + const [thumbnailBlocked, setThumbnailBlocked] = useState(false); if (site !== 'YouTube') { return ( @@ -85,10 +90,15 @@ function TrailerEmbed({ className="group/trailer relative block w-full h-full cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none" > {title} setThumbnailBlocked(true)} className="object-cover transition-transform duration-(--duration-base) group-hover/trailer:scale-105" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw" /> diff --git a/types/components.ts b/types/components.ts index 64165540..2bbb22b3 100644 --- a/types/components.ts +++ b/types/components.ts @@ -62,6 +62,7 @@ export interface MediaDescriptionProps { export interface MediaTrailersProps { trailers: Video[]; + backdropPath: string | null; } export interface MediaCastProps { From aed8c03d2be93671de5214600c2f31ac4a65b8ad Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Tue, 28 Jul 2026 05:15:30 +0200 Subject: [PATCH 66/98] =?UTF-8?q?fix(hydration):=20tol=C3=A8re=20les=20att?= =?UTF-8?q?ributs=20que=20les=20extensions=20posent=20sur=20le=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ColorZilla écrit `cz-shortcut-listen`, Grammarly `data-gr-*`, avant que React n'hydrate : chaque attribut lève un mismatch qui pollue la console. `suppressHydrationWarning` était sur seul, or il ne se propage pas — il ne couvre que l'élément qui le porte. Portée limitée au body lui-même : les mismatchs de ses enfants restent signalés. --- app/[lang]/layout.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx index 9fa2bb1b..124c2cc5 100644 --- a/app/[lang]/layout.tsx +++ b/app/[lang]/layout.tsx @@ -352,7 +352,12 @@ export default async function RootLayout({ }} /> + {/* Les extensions posent leurs attributs sur le body avant que React n'hydrate + (`cz-shortcut-listen` de ColorZilla, `data-gr-*` de Grammarly…) et chacune + lève un mismatch d'hydratation. `suppressHydrationWarning` ne se propage pas + depuis : il ne couvre que l'élément qui le porte. */} From 5b3809070c6e05a2158e282352107547c2c4b188 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Tue, 28 Jul 2026 06:11:45 +0200 Subject: [PATCH 67/98] =?UTF-8?q?fix(ui):=20=C3=A9l=C3=A9ments=20masqu?= =?UTF-8?q?=C3=A9s=20sur=20Chromium=20150=20=E2=80=94=20hidden=20+=20varia?= =?UTF-8?q?nte=20responsive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sur Chromium 150 (Chrome, Brave), une règle `display: none` hors media query n'est plus surchargée par une règle de display placée après elle dans une media query : `.hidden` bat `.lg:grid` alors qu'il la précède dans la feuille. `.block` et `.flex`, déclarés avant `.hidden`, cèdent eux correctement la place — seul `display: none` résiste. Safari et les Chromium plus anciens ne sont pas touchés, d'où un bug invisible en test. La navbar desktop en était la victime la plus visible : bloc mobile masqué par `lg:hidden`, bloc desktop jamais rétabli par `lg:grid`, donc une barre de 1px. Les 14 autres occurrences du motif étaient cassées de la même façon (labels de boutons, icônes d'onglets, backdrop cinématique, bandeaux de saison). Le motif passe partout en ` max-:hidden` : la valeur de base est affichée et c'est le `display: none` qui vit dans la media query — sens que Chromium honore toujours (`flex lg:hidden` fonctionnait déjà). Équivalent à l'ancien à chaque breakpoint, sans dépendre de l'ordre de la cascade. --- components/library/LibraryTabs.tsx | 2 +- components/media/detail/CinematicBackdrop.tsx | 2 +- components/media/detail/MediaBanner.tsx | 2 +- components/media/detail/MediaDetailSkeleton.tsx | 2 +- components/media/detail/WatchButton.tsx | 2 +- components/media/detail/WatchNowMenu.tsx | 2 +- components/media/detail/WatchNowSlot.tsx | 2 +- components/media/tv/SeasonBanner.tsx | 2 +- components/media/tv/SeasonWatchButton.tsx | 2 +- components/media/tv/TvWatchSummary.tsx | 2 +- components/navigation/NavbarClient.tsx | 4 ++-- components/profile/PlaylistPageSkeleton.tsx | 2 +- components/settings/SettingsNav.tsx | 2 +- components/shared/HorizontalScroll.tsx | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/components/library/LibraryTabs.tsx b/components/library/LibraryTabs.tsx index 515f95ed..deecf772 100644 --- a/components/library/LibraryTabs.tsx +++ b/components/library/LibraryTabs.tsx @@ -165,7 +165,7 @@ export function LibraryTabs({ : 'text-muted hover:text-text' )} > - + {tab.label} - + diff --git a/components/media/detail/MediaBanner.tsx b/components/media/detail/MediaBanner.tsx index aeac1308..48eadef9 100644 --- a/components/media/detail/MediaBanner.tsx +++ b/components/media/detail/MediaBanner.tsx @@ -67,7 +67,7 @@ export function MediaBanner({
-
+
{reviewDialog} diff --git a/components/media/detail/WatchNowMenu.tsx b/components/media/detail/WatchNowMenu.tsx index efb5937f..2ce00d5a 100644 --- a/components/media/detail/WatchNowMenu.tsx +++ b/components/media/detail/WatchNowMenu.tsx @@ -26,7 +26,7 @@ interface WatchNowMenuProps { /** Platform picker used when several of the user's services carry the title. */ export function WatchNowMenu({ options, variant }: WatchNowMenuProps) { const { t } = useTranslation(); - const labelClass = variant === 'bar' ? 'hidden lg:inline' : undefined; + const labelClass = variant === 'bar' ? 'inline max-lg:hidden' : undefined; return ( diff --git a/components/media/detail/WatchNowSlot.tsx b/components/media/detail/WatchNowSlot.tsx index 9c5fe36f..22349e7f 100644 --- a/components/media/detail/WatchNowSlot.tsx +++ b/components/media/detail/WatchNowSlot.tsx @@ -37,7 +37,7 @@ export function WatchNowSlot({ variant }: { variant: WatchNowVariant }) { > {label} diff --git a/components/media/tv/SeasonBanner.tsx b/components/media/tv/SeasonBanner.tsx index 0a13f284..bf8f5f12 100644 --- a/components/media/tv/SeasonBanner.tsx +++ b/components/media/tv/SeasonBanner.tsx @@ -87,7 +87,7 @@ export function SeasonBanner({
-
+
- {stateLabel} + {stateLabel} ); } diff --git a/components/media/tv/TvWatchSummary.tsx b/components/media/tv/TvWatchSummary.tsx index 83ffd9d3..14f2b9f3 100644 --- a/components/media/tv/TvWatchSummary.tsx +++ b/components/media/tv/TvWatchSummary.tsx @@ -24,7 +24,7 @@ export function TvWatchSummary({ const overallPercent = Math.round((totalWatched / totalEpisodes) * 100); return ( -
+
{totalWatched}/{totalEpisodes} {t.movie.episodes} diff --git a/components/navigation/NavbarClient.tsx b/components/navigation/NavbarClient.tsx index fc0bff01..6a2fc169 100644 --- a/components/navigation/NavbarClient.tsx +++ b/components/navigation/NavbarClient.tsx @@ -147,7 +147,7 @@ export function NavbarClient({
{/* Desktop: full navigation */} -
+
{logo}
@@ -297,7 +297,7 @@ export function NavbarClient({ {/* Desktop only: media sub-bar that expands on scroll */}
-
+
{Array.from({ length: 5 }).map((_, i) => (
{tab.icon} - {tab.label} + {tab.label} ))} diff --git a/components/shared/HorizontalScroll.tsx b/components/shared/HorizontalScroll.tsx index f505e874..4ccc8b88 100644 --- a/components/shared/HorizontalScroll.tsx +++ b/components/shared/HorizontalScroll.tsx @@ -35,7 +35,7 @@ export function HorizontalScroll({ {title && (
{title} -
+
+ ) + )} + + {expanded && ( +
    + {people.map((person, index) => { + const isActive = index === activeIndex; + return ( +
  • + +
  • + ); + })} +
+ )} +
+ ); +} diff --git a/components/media/list/MediaListControls.tsx b/components/media/list/MediaListControls.tsx index 39c1c5d6..38cb41f6 100644 --- a/components/media/list/MediaListControls.tsx +++ b/components/media/list/MediaListControls.tsx @@ -5,8 +5,6 @@ import { ArrowUpNarrowWide, ChevronDown, ListFilter, - Loader2, - Search, X, } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -23,6 +21,7 @@ import { } from '@/components/ui/dropdown-menu'; import { SORT_KEYS, type SortKey } from '@/lib/media-list/controls'; import type { UseMediaListControls } from '@/hooks/useMediaListControls'; +import { ActorFilterInput } from './ActorFilterInput'; interface MediaListControlsProps { controls: UseMediaListControls; @@ -47,6 +46,7 @@ export function MediaListControls({ toggleDir, toggleGenre, setActorQuery, + selectActor, clearActor, clearAll, hasActiveFilters, @@ -141,31 +141,13 @@ export function MediaListControls({ )} -
- - setActorQuery(e.target.value)} - placeholder={t.lists.actorPlaceholder} - aria-label={t.lists.filterByActor} - className="w-full h-11 pl-9 pr-9 rounded-full bg-surface-2 border border-border text-sm text-text placeholder:text-muted outline-none transition-colors duration-(--duration-fast) focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/40" - /> - {isActorLoading ? ( - - ) : ( - state.actorQuery !== '' && ( - - ) - )} -
+
{hasActiveFilters && ( diff --git a/hooks/useMediaListControls.ts b/hooks/useMediaListControls.ts index 1dda3dbe..f4e583ab 100644 --- a/hooks/useMediaListControls.ts +++ b/hooks/useMediaListControls.ts @@ -7,11 +7,13 @@ import { useState, useSyncExternalStore, } from 'react'; -import type { MediaItem } from '@/types/tmdb'; +import type { MediaItem, PersonSuggestion } from '@/types/tmdb'; import { applyListControls, availableGenres, DEFAULT_LIST_CONTROLS, + withActorQuery, + withSelectedActor, type ListControlsState, type SortKey, } from '@/lib/media-list/controls'; @@ -34,6 +36,8 @@ function parseState(raw: string | null): ListControlsState { ...DEFAULT_LIST_CONTROLS, ...parsed, genreIds: Array.isArray(parsed.genreIds) ? parsed.genreIds : [], + actorId: + typeof parsed.actorId === 'number' ? parsed.actorId : null, }; } catch { return DEFAULT_LIST_CONTROLS; @@ -75,6 +79,7 @@ export interface UseMediaListControls { toggleGenre: (id: number) => void; clearGenres: () => void; setActorQuery: (query: string) => void; + selectActor: (person: PersonSuggestion) => void; clearActor: () => void; clearAll: () => void; hasActiveFilters: boolean; @@ -84,6 +89,7 @@ export interface UseMediaListControls { * Drives a media list's sort/filter controls: genre + sort run synchronously in memory, * the actor filter resolves on demand via a debounced server action, and the whole state * persists to `localStorage` under `storageKey` (SSR-safe via useSyncExternalStore). + * An actor picked from the autocomplete matches on its TMDB ID and skips the debounce. * Returns the processed items plus setters. */ export function useMediaListControls( @@ -116,7 +122,9 @@ export function useMediaListControls( const [actorKeys, setActorKeys] = useState | null>(null); const [actorLoading, setActorLoading] = useState(false); - const actorActive = state.actorQuery.trim().length >= ACTOR_MIN_LENGTH; + const actorActive = + state.actorId !== null || + state.actorQuery.trim().length >= ACTOR_MIN_LENGTH; const actorInput = useMemo( () => @@ -129,28 +137,32 @@ export function useMediaListControls( useEffect(() => { const query = state.actorQuery.trim(); - if (query.length < ACTOR_MIN_LENGTH) return; + const personId = state.actorId; + if (personId === null && query.length < ACTOR_MIN_LENGTH) return; let cancelled = false; - const timer = setTimeout(() => { - setActorLoading(true); - filterListByActor(actorInput, query) - .then((keys) => { - if (!cancelled) setActorKeys(new Set(keys)); - }) - .catch(() => { - if (!cancelled) setActorKeys(new Set()); - }) - .finally(() => { - if (!cancelled) setActorLoading(false); - }); - }, ACTOR_DEBOUNCE_MS); + const timer = setTimeout( + () => { + setActorLoading(true); + filterListByActor(actorInput, query, personId ?? undefined) + .then((keys) => { + if (!cancelled) setActorKeys(new Set(keys)); + }) + .catch(() => { + if (!cancelled) setActorKeys(new Set()); + }) + .finally(() => { + if (!cancelled) setActorLoading(false); + }); + }, + personId === null ? ACTOR_DEBOUNCE_MS : 0 + ); return () => { cancelled = true; clearTimeout(timer); }; - }, [state.actorQuery, actorInput]); + }, [state.actorQuery, state.actorId, actorInput]); const effectiveActorKeys = actorActive ? actorKeys : null; @@ -195,17 +207,24 @@ export function useMediaListControls( ); const setActorQuery = useCallback( - (query: string) => setState((prev) => ({ ...prev, actorQuery: query })), + (query: string) => setState((prev) => withActorQuery(prev, query)), + [setState] + ); + + const selectActor = useCallback( + (person: PersonSuggestion) => + setState((prev) => withSelectedActor(prev, person)), [setState] ); const clearActor = useCallback( - () => setState((prev) => ({ ...prev, actorQuery: '' })), + () => setState((prev) => withActorQuery(prev, '')), [setState] ); const clearAll = useCallback( - () => setState((prev) => ({ ...prev, genreIds: [], actorQuery: '' })), + () => + setState((prev) => ({ ...withActorQuery(prev, ''), genreIds: [] })), [setState] ); @@ -222,6 +241,7 @@ export function useMediaListControls( toggleGenre, clearGenres, setActorQuery, + selectActor, clearActor, clearAll, hasActiveFilters, diff --git a/hooks/usePersonSuggestions.ts b/hooks/usePersonSuggestions.ts new file mode 100644 index 00000000..61f00bdc --- /dev/null +++ b/hooks/usePersonSuggestions.ts @@ -0,0 +1,62 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useTranslation } from '@/lib/i18n/context'; +import type { PersonSuggestion } from '@/types/tmdb'; + +const DEBOUNCE_MS = 300; +const MIN_QUERY_LENGTH = 2; + +/** + * Fetches live person suggestions from `/api/search?type=person`, debounced 300ms. + * Pass an empty query to keep the hook idle (e.g. right after a suggestion was picked). + */ +export function usePersonSuggestions(query: string) { + const { lang } = useTranslation(); + const [people, setPeople] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [trackedQuery, setTrackedQuery] = useState(query); + + if (trackedQuery !== query) { + setTrackedQuery(query); + if (query.trim().length < MIN_QUERY_LENGTH) { + setPeople([]); + setIsLoading(false); + } + } + + useEffect(() => { + if (query.trim().length < MIN_QUERY_LENGTH) return; + + // eslint-disable-next-line react-hooks/set-state-in-effect + setIsLoading(true); + + const controller = new AbortController(); + const timer = setTimeout(async () => { + try { + const response = await fetch( + `/api/search?type=person&query=${encodeURIComponent(query)}&lang=${lang}`, + { signal: controller.signal } + ); + if (!response.ok) + throw new Error(`Search failed: ${response.status}`); + const data: { people?: PersonSuggestion[] } = + await response.json(); + setPeople(Array.isArray(data.people) ? data.people : []); + } catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') + return; + setPeople([]); + } finally { + if (!controller.signal.aborted) setIsLoading(false); + } + }, DEBOUNCE_MS); + + return () => { + clearTimeout(timer); + controller.abort(); + }; + }, [query, lang]); + + return { people, isLoading }; +} diff --git a/lib/i18n/messages/lists.ts b/lib/i18n/messages/lists.ts index 91517381..ea3067c5 100644 --- a/lib/i18n/messages/lists.ts +++ b/lib/i18n/messages/lists.ts @@ -11,6 +11,7 @@ const fr = { filterByActor: 'Filtrer par acteur', actor: 'Acteur', actorPlaceholder: 'Rechercher un acteur', + actorSuggestions: 'Suggestions d’acteurs', clearActor: 'Effacer la recherche', activeFilters: 'Filtres actifs', removeFilter: 'Retirer le filtre', @@ -31,6 +32,7 @@ const en = { filterByActor: 'Filter by actor', actor: 'Actor', actorPlaceholder: 'Search by actor', + actorSuggestions: 'Actor suggestions', clearActor: 'Clear search', activeFilters: 'Active filters', removeFilter: 'Remove filter', diff --git a/lib/media-list/controls.ts b/lib/media-list/controls.ts index 45624d70..44f3e7e2 100644 --- a/lib/media-list/controls.ts +++ b/lib/media-list/controls.ts @@ -1,4 +1,4 @@ -import type { MediaItem } from '@/types/tmdb'; +import type { MediaItem, PersonSuggestion } from '@/types/tmdb'; import { getMediaKey } from '@/lib/media'; export type SortKey = 'added' | 'year' | 'title' | 'rating'; @@ -9,6 +9,7 @@ export interface ListControlsState { sortDir: SortDir; genreIds: number[]; actorQuery: string; + actorId: number | null; } export const SORT_KEYS: readonly SortKey[] = [ @@ -23,8 +24,25 @@ export const DEFAULT_LIST_CONTROLS: ListControlsState = { sortDir: 'desc', genreIds: [], actorQuery: '', + actorId: null, }; +/** Sets a free-text actor query, releasing any person previously locked by ID. */ +export function withActorQuery( + state: ListControlsState, + actorQuery: string +): ListControlsState { + return { ...state, actorQuery, actorId: null }; +} + +/** Locks the actor filter onto an exact TMDB person, immune to spelling and homonyms. */ +export function withSelectedActor( + state: ListControlsState, + person: PersonSuggestion +): ListControlsState { + return { ...state, actorQuery: person.name, actorId: person.id }; +} + function releaseYear(item: MediaItem): number | null { const year = Number.parseInt(item.release_date.slice(0, 4), 10); return Number.isFinite(year) ? year : null; diff --git a/lib/tmdb/index.ts b/lib/tmdb/index.ts index 559f938a..9796bc61 100644 --- a/lib/tmdb/index.ts +++ b/lib/tmdb/index.ts @@ -32,7 +32,12 @@ export { getSeasonDetails, getTvShowWatchProviders, } from './tv'; -export { searchMulti, movieToMediaItem, tvShowToMediaItem } from './search'; +export { + searchMulti, + searchPeople, + movieToMediaItem, + tvShowToMediaItem, +} from './search'; export { selectHeroImage, getImageUrl } from './images'; export { getCrewDetails, getCrewMovieCredits, getCrewTvCredits } from './crew'; export { getListMediaMetadata } from './list-metadata'; diff --git a/lib/tmdb/search.ts b/lib/tmdb/search.ts index 80ca056f..32030010 100644 --- a/lib/tmdb/search.ts +++ b/lib/tmdb/search.ts @@ -1,4 +1,9 @@ -import type { MediaItem, Movie, TvShow } from '@/types/tmdb'; +import type { + MediaItem, + Movie, + PersonSuggestion, + TvShow, +} from '@/types/tmdb'; import { fetchTMDB } from './client'; import type { Language } from '@/lib/i18n/translations'; @@ -50,6 +55,51 @@ export async function searchMulti( .map(tmdbMultiResultToMediaItem); } +interface TMDBPersonResult { + id: number; + name: string; + profile_path: string | null; + known_for?: Array<{ title?: string; name?: string }>; +} + +const KNOWN_FOR_MAX_TITLES = 2; + +/** + * Searches people via TMDB's person endpoint, for the actor filter autocomplete. + * Results keep TMDB's popularity order. + * + * @param query - Search string. + * @param lang - Language to search in; defaults to the request-resolved one. + * @returns Normalized person suggestions. + */ +export async function searchPeople( + query: string, + lang?: Language +): Promise { + const { results } = await fetchTMDB<{ results: TMDBPersonResult[] }>( + '/search/person', + { query }, + { lang } + ); + + return results.map(tmdbPersonResultToSuggestion); +} + +function tmdbPersonResultToSuggestion( + result: TMDBPersonResult +): PersonSuggestion { + return { + id: result.id, + name: result.name, + profile_path: result.profile_path, + knownFor: (result.known_for ?? []) + .map((entry) => entry.title ?? entry.name ?? '') + .filter((title) => title !== '') + .slice(0, KNOWN_FOR_MAX_TITLES) + .join(' · '), + }; +} + /** * Normalizes a TMDB `Movie` object into the app's unified `MediaItem` shape. * diff --git a/tests/unit/media-list-controls.test.ts b/tests/unit/media-list-controls.test.ts index b337c90b..dd70de71 100644 --- a/tests/unit/media-list-controls.test.ts +++ b/tests/unit/media-list-controls.test.ts @@ -3,6 +3,8 @@ import { applyListControls, availableGenres, DEFAULT_LIST_CONTROLS, + withActorQuery, + withSelectedActor, type ListControlsState, } from '@/lib/media-list/controls'; import type { MediaItem } from '@/types/tmdb'; @@ -139,6 +141,44 @@ describe('applyListControls — actor match set', () => { }); }); +describe('actor selection state', () => { + const keanu = { + id: 6384, + name: 'Keanu Reeves', + profile_path: null, + knownFor: 'The Matrix', + }; + + it('locks the query on the picked person name and id', () => { + const result = withSelectedActor(DEFAULT_LIST_CONTROLS, keanu); + expect(result.actorQuery).toBe('Keanu Reeves'); + expect(result.actorId).toBe(6384); + }); + + it('releases the locked id as soon as the query is typed again', () => { + const locked = withSelectedActor(DEFAULT_LIST_CONTROLS, keanu); + const typed = withActorQuery(locked, 'Keanu R'); + expect(typed.actorQuery).toBe('Keanu R'); + expect(typed.actorId).toBeNull(); + }); + + it('clears both query and id when emptied', () => { + const locked = withSelectedActor(DEFAULT_LIST_CONTROLS, keanu); + expect(withActorQuery(locked, '')).toMatchObject({ + actorQuery: '', + actorId: null, + }); + }); + + it('leaves sort and genre filters untouched', () => { + const base = state({ sortKey: 'title', genreIds: [878] }); + expect(withSelectedActor(base, keanu)).toMatchObject({ + sortKey: 'title', + genreIds: [878], + }); + }); +}); + describe('availableGenres', () => { it('returns distinct named genres present in the list, sorted by name', () => { const names: Record = { diff --git a/types/tmdb.ts b/types/tmdb.ts index 18784036..01aed205 100644 --- a/types/tmdb.ts +++ b/types/tmdb.ts @@ -147,6 +147,13 @@ export interface Crew { profile_path: string | null; } +export interface PersonSuggestion { + id: number; + name: string; + profile_path: string | null; + knownFor: string; +} + export interface CreatedBy { id: number; credit_id: string; From bf6b7f96d5f2c08199221c47c867be4a202649bd Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Wed, 29 Jul 2026 03:00:00 +0200 Subject: [PATCH 74/98] =?UTF-8?q?fix(avatar):=20r=C3=A9tablit=20les=20phot?= =?UTF-8?q?os=20de=20compte=20Google=20et=20fiabilise=20les=20avatars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les photos Google ne s'affichaient plus nulle part. Ce n'est pas la config des images — `lh3.googleusercontent.com` figure déjà dans `remotePatterns` et dans la CSP `img-src` — c'est le `Referer` : notre en-tête `Referrer-Policy` envoie l'origine avec chaque requête et Google coupe la connexion sur les avatars hotlinkés (`transferSize: 0`, aucune réponse HTTP). Mesuré sur la même URL, mêmes conditions : 0/6 chargements avec la politique par défaut, 6/6 avec `no-referrer`. En curl la requête passait, faute d'en-têtes `Sec-Fetch-*`, d'où l'illusion d'une URL saine. Les cinq rendus d'avatar passent donc en `referrerPolicy="no-referrer"`, sans effet sur Supabase, DiceBear et les blobs locaux qui ne filtrent pas là-dessus. Aucun d'eux n'avait de `onError` : une URL morte laissait un carré vide au lieu des initiales. `useBrokenImage` mémorise la source qui a échoué — la clé est la source elle-même, donc un avatar remplacé est réessayé au lieu de rester cassé. Enfin la photo du provider est copiée dans le bucket `avatars` à la première connexion OAuth, derrière `after()` pour ne rien ajouter à la latence du login : le rendu ne dépend plus d'un CDN tiers et l'IP des visiteurs ne part plus chez Google à chaque affichage. Un avatar déjà uploadé n'est jamais écrasé. `user_metadata` étant modifiable par l'utilisateur, l'URL source est filtrée avant le fetch — https et hôte `lh.googleusercontent.com` uniquement, sans quoi la copie serait un SSRF ouvert. --- app/auth/callback/route.ts | 10 ++- components/media/reviews/ReviewsList.tsx | 6 +- components/profile/FriendsSection.tsx | 54 +++++++++++----- components/profile/ProfileHero.tsx | 6 +- components/settings/ProfileSettings.tsx | 7 ++- components/shared/UserAvatar.tsx | 15 +++-- hooks/useBrokenImage.ts | 16 +++++ lib/avatar-mirror.ts | 80 ++++++++++++++++++++++++ lib/validators.ts | 32 ++++++++++ tests/unit/validators.test.ts | 73 +++++++++++++++++++++ 10 files changed, 274 insertions(+), 25 deletions(-) create mode 100644 hooks/useBrokenImage.ts create mode 100644 lib/avatar-mirror.ts diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts index b5e1830c..4d8141b2 100644 --- a/app/auth/callback/route.ts +++ b/app/auth/callback/route.ts @@ -1,5 +1,6 @@ -import { NextResponse } from 'next/server'; +import { NextResponse, after } from 'next/server'; import { createClient } from '@/lib/supabase/server'; +import { mirrorOAuthAvatar } from '@/lib/avatar-mirror'; import { sanitizeRedirectPath } from '@/lib/validators'; import { getServerLanguage } from '@/lib/i18n/server'; import { localizedHref } from '@/lib/i18n/utils'; @@ -26,6 +27,13 @@ export async function GET(request: Request) { await supabase.auth.exchangeCodeForSession(code); if (!error && data.session) { + const { id, user_metadata } = data.session.user; + after(() => + mirrorOAuthAvatar( + id, + user_metadata?.avatar_url ?? user_metadata?.picture + ) + ); return NextResponse.redirect( `${baseUrl}${localizedHref(lang, next)}` ); diff --git a/components/media/reviews/ReviewsList.tsx b/components/media/reviews/ReviewsList.tsx index 14cbf828..456cac4d 100644 --- a/components/media/reviews/ReviewsList.tsx +++ b/components/media/reviews/ReviewsList.tsx @@ -15,6 +15,7 @@ import { useTranslation } from '@/lib/i18n/context'; import { localizedHref } from '@/lib/i18n/utils'; import type { Language } from '@/lib/i18n/translations'; import type { PublicReview } from '@/types/profile'; +import { useBrokenImage } from '@/hooks/useBrokenImage'; function relativeDate(dateStr: string, lang: string): string { const days = Math.floor( @@ -35,15 +36,18 @@ function Avatar({ avatarUrl: string | null; }) { const initial = (username[0] ?? '?').toUpperCase(); + const broken = useBrokenImage(avatarUrl); return (
- {avatarUrl ? ( + {avatarUrl && !broken.isBroken ? ( {username} ) : ( diff --git a/components/profile/FriendsSection.tsx b/components/profile/FriendsSection.tsx index 1bf28a7f..b45c9e2a 100644 --- a/components/profile/FriendsSection.tsx +++ b/components/profile/FriendsSection.tsx @@ -7,6 +7,7 @@ import { useTranslation } from '@/lib/i18n/context'; import { localizedHref } from '@/lib/i18n/utils'; import { PrivacyBlock } from '@/components/profile/PrivacyBlock'; import { EmptyState } from '@/components/ui/EmptyState'; +import { useBrokenImage } from '@/hooks/useBrokenImage'; interface FriendsSectionProps { friends: FriendEntry[]; @@ -38,22 +39,11 @@ export function FriendsSection({ prefetch={false} className="flex flex-col items-center gap-2 p-3 rounded-lg bg-surface border border-border-subtle shadow-card-sm hover:bg-surface-2 transition duration-(--duration-fast) ease-apple active:scale-[0.97]" > - {avatarUrl ? ( - {displayName} - ) : ( -
- - {initials} - -
- )} +

{displayName} @@ -68,3 +58,35 @@ export function FriendsSection({

); } + +function FriendAvatar({ + avatarUrl, + displayName, + initials, +}: { + avatarUrl: string | null | undefined; + displayName: string; + initials: string; +}) { + const broken = useBrokenImage(avatarUrl); + + if (!avatarUrl || broken.isBroken) + return ( +
+ {initials} +
+ ); + + return ( + {displayName} + ); +} diff --git a/components/profile/ProfileHero.tsx b/components/profile/ProfileHero.tsx index 01e6bdc5..e6e3e98c 100644 --- a/components/profile/ProfileHero.tsx +++ b/components/profile/ProfileHero.tsx @@ -11,6 +11,7 @@ import { Grain } from '@/components/effects/Grain'; import type { UserProfile } from '@/types/profile'; import { useTranslation } from '@/lib/i18n/context'; import { localizedHref } from '@/lib/i18n/utils'; +import { useBrokenImage } from '@/hooks/useBrokenImage'; interface IconProps { className?: string; @@ -91,6 +92,7 @@ export function ProfileHero({ const { t, lang } = useTranslation(); const displayName = fullName || profile.username; const initials = displayName.slice(0, 2).toUpperCase(); + const broken = useBrokenImage(avatarUrl); const socialLinks = [ profile.instagram && { @@ -138,14 +140,16 @@ export function ProfileHero({
- {avatarUrl ? ( + {avatarUrl && !broken.isBroken ? ( {`${displayName} ) : (
diff --git a/components/settings/ProfileSettings.tsx b/components/settings/ProfileSettings.tsx index c71efe8b..1efb3a2a 100644 --- a/components/settings/ProfileSettings.tsx +++ b/components/settings/ProfileSettings.tsx @@ -24,6 +24,7 @@ import { import { Upload } from 'lucide-react'; import { User } from '@supabase/supabase-js'; import { useTranslation } from '@/lib/i18n/context'; +import { useBrokenImage } from '@/hooks/useBrokenImage'; const initialState = { error: undefined, @@ -54,6 +55,7 @@ export function ProfileSettings({ const currentAvatarUrl = profileAvatarUrl || user?.user_metadata?.avatar_url || ''; const [avatarPreview, setAvatarPreview] = useState(currentAvatarUrl); + const brokenAvatar = useBrokenImage(avatarPreview); const [fullNameStr, setFullNameStr] = useState( user?.user_metadata?.full_name || '' ); @@ -102,14 +104,17 @@ export function ProfileSettings({
- {avatarPreview ? ( + {avatarPreview && + !brokenAvatar.isBroken ? ( {t.common.userAvatar} ) : (
diff --git a/components/shared/UserAvatar.tsx b/components/shared/UserAvatar.tsx index 4965392f..47ee3a9f 100644 --- a/components/shared/UserAvatar.tsx +++ b/components/shared/UserAvatar.tsx @@ -2,6 +2,7 @@ import Image from 'next/image'; import { useTranslation } from '@/lib/i18n/context'; +import { useBrokenImage } from '@/hooks/useBrokenImage'; interface UserAvatarProps { picture?: string; fullName?: string; @@ -20,17 +21,20 @@ export function UserAvatar({ loading = 'lazy', }: UserAvatarProps) { const { t } = useTranslation(); + const broken = useBrokenImage(picture); const goldColor = 'd6b25e'; - const avatarUrl = picture - ? picture - : `https://api.dicebear.com/9.x/initials/svg?seed=${ - fullName || email?.split('@')[0] || 'user' - }&size=${size}&backgroundType=gradientLinear&backgroundColor=${goldColor}&fontWeight=600&fontFamily=Tahoma&chars=1`; + const avatarUrl = + picture && !broken.isBroken + ? picture + : `https://api.dicebear.com/9.x/initials/svg?seed=${ + fullName || email?.split('@')[0] || 'user' + }&size=${size}&backgroundType=gradientLinear&backgroundColor=${goldColor}&fontWeight=600&fontFamily=Tahoma&chars=1`; return ( {t.common.userAvatar} ); } diff --git a/hooks/useBrokenImage.ts b/hooks/useBrokenImage.ts new file mode 100644 index 00000000..dd6121cf --- /dev/null +++ b/hooks/useBrokenImage.ts @@ -0,0 +1,16 @@ +'use client'; + +import { useState } from 'react'; + +/** + * Tracks which image source failed to load so a dead URL falls back to a local placeholder. + * Keying on the source itself means a later `src` retries instead of staying broken. + */ +export function useBrokenImage(src: string | null | undefined) { + const [brokenSrc, setBrokenSrc] = useState(null); + + return { + isBroken: Boolean(src) && brokenSrc === src, + onError: () => setBrokenSrc(src ?? null), + }; +} diff --git a/lib/avatar-mirror.ts b/lib/avatar-mirror.ts new file mode 100644 index 00000000..3d6f6a67 --- /dev/null +++ b/lib/avatar-mirror.ts @@ -0,0 +1,80 @@ +import 'server-only'; + +import { createAdminClient } from '@/lib/supabase/server'; +import { reportSwallowed } from '@/lib/report'; +import { + avatarExtensionForMime, + isImportableAvatarUrl, + isStoredAvatarUrl, +} from '@/lib/validators'; + +const FETCH_TIMEOUT_MS = 5_000; +const MAX_REMOTE_AVATAR_BYTES = 2 * 1024 * 1024; + +/** + * Copies an OAuth provider avatar into the `avatars` bucket the first time a user signs in, + * so the app stops hotlinking a third-party CDN — Google refuses requests carrying our + * `Referer` and sees every visitor's IP. A user-uploaded avatar is never overwritten. + * Best effort: any failure leaves the provider URL in place. + */ +export async function mirrorOAuthAvatar( + userId: string, + sourceUrl: unknown +): Promise { + if (!isImportableAvatarUrl(sourceUrl)) return; + + try { + const admin = createAdminClient(); + const { data: profile } = await admin + .from('user_profiles') + .select('avatar_url') + .eq('user_id', userId) + .maybeSingle(); + + if (isStoredAvatarUrl(profile?.avatar_url)) return; + + const response = await fetch(sourceUrl, { + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + }); + if (!response.ok) { + reportSwallowed( + 'auth:avatar-mirror', + `provider responded ${response.status}` + ); + return; + } + + const extension = avatarExtensionForMime( + response.headers.get('content-type') ?? '' + ); + if (!extension) return; + + const body = await response.arrayBuffer(); + if (body.byteLength === 0 || body.byteLength > MAX_REMOTE_AVATAR_BYTES) + return; + + const fileName = `${userId}-${Date.now()}.${extension}`; + const { error: uploadError } = await admin.storage + .from('avatars') + .upload(fileName, body, { + contentType: response.headers.get('content-type') ?? undefined, + upsert: true, + }); + if (uploadError) { + reportSwallowed('auth:avatar-mirror', uploadError); + return; + } + + const { data: publicUrl } = admin.storage + .from('avatars') + .getPublicUrl(fileName); + + const { error: updateError } = await admin + .from('user_profiles') + .update({ avatar_url: publicUrl.publicUrl }) + .eq('user_id', userId); + if (updateError) reportSwallowed('auth:avatar-mirror', updateError); + } catch (error) { + reportSwallowed('auth:avatar-mirror', error); + } +} diff --git a/lib/validators.ts b/lib/validators.ts index fe40761f..4896fcdd 100644 --- a/lib/validators.ts +++ b/lib/validators.ts @@ -130,6 +130,38 @@ export function validateAvatarFile( return { valid: true, ext }; } +const OAUTH_AVATAR_HOST = /^lh\d+\.googleusercontent\.com$/; +const STORED_AVATAR_MARKER = '/storage/v1/object/public/avatars/'; + +/** + * Guards the server-side avatar copy against SSRF: `user_metadata` is user-writable, + * so only https URLs served by a known OAuth provider CDN may be fetched. + */ +export function isImportableAvatarUrl(url: unknown): url is string { + if (typeof url !== 'string') return false; + try { + const parsed = new URL(url); + return ( + parsed.protocol === 'https:' && + OAUTH_AVATAR_HOST.test(parsed.hostname) + ); + } catch { + return false; + } +} + +/** True when the avatar already lives in our own bucket and must not be overwritten. */ +export function isStoredAvatarUrl(url: unknown): url is string { + return typeof url === 'string' && url.includes(STORED_AVATAR_MARKER); +} + +/** Maps a fetched avatar's content type to the extension used for its stored file. */ +export function avatarExtensionForMime(contentType: string): string | null { + const mime = contentType.split(';')[0].trim().toLowerCase(); + if (!(ALLOWED_AVATAR_MIMES as readonly string[]).includes(mime)) return null; + return mime === 'image/jpeg' ? 'jpg' : mime.slice('image/'.length); +} + const VALID_NOTIFICATION_TYPES = new Set(NOTIFICATION_TYPES); export function isValidNotificationType( diff --git a/tests/unit/validators.test.ts b/tests/unit/validators.test.ts index 206529c8..78b61cae 100644 --- a/tests/unit/validators.test.ts +++ b/tests/unit/validators.test.ts @@ -9,6 +9,9 @@ import { validateAvatarFile, sanitizeRedirectPath, validateReviewContent, + isImportableAvatarUrl, + isStoredAvatarUrl, + avatarExtensionForMime, } from '@/lib/validators'; import { MAX_REVIEW_LENGTH } from '@/types/profile'; @@ -241,3 +244,73 @@ describe('validateUUID', () => { expect(validateUUID({})).toBeNull(); }); }); + +describe('isImportableAvatarUrl', () => { + it('accepts Google OAuth avatar hosts over https', () => { + expect( + isImportableAvatarUrl( + 'https://lh3.googleusercontent.com/a/ACg8ocI=s96-c' + ) + ).toBe(true); + expect( + isImportableAvatarUrl('https://lh6.googleusercontent.com/a/x=s96-c') + ).toBe(true); + }); + + it('rejects any other host, blocking SSRF through user-writable metadata', () => { + expect( + isImportableAvatarUrl('https://evil.com/lh3.googleusercontent.com') + ).toBe(false); + expect(isImportableAvatarUrl('http://169.254.169.254/latest/meta-data')).toBe( + false + ); + expect(isImportableAvatarUrl('https://googleusercontent.com/a/x')).toBe( + false + ); + expect( + isImportableAvatarUrl( + 'https://lh3.googleusercontent.com.evil.com/a/x' + ) + ).toBe(false); + }); + + it('rejects plain http and non-URL input', () => { + expect( + isImportableAvatarUrl('http://lh3.googleusercontent.com/a/x') + ).toBe(false); + expect(isImportableAvatarUrl('not a url')).toBe(false); + expect(isImportableAvatarUrl(null)).toBe(false); + expect(isImportableAvatarUrl(42)).toBe(false); + }); +}); + +describe('isStoredAvatarUrl', () => { + it('detects avatars already served from our own bucket', () => { + expect( + isStoredAvatarUrl( + 'https://xyz.supabase.co/storage/v1/object/public/avatars/user-1.png?t=1' + ) + ).toBe(true); + }); + + it('rejects provider URLs and non-strings', () => { + expect( + isStoredAvatarUrl('https://lh3.googleusercontent.com/a/x=s96-c') + ).toBe(false); + expect(isStoredAvatarUrl(null)).toBe(false); + }); +}); + +describe('avatarExtensionForMime', () => { + it('maps allowed image types, ignoring charset and case', () => { + expect(avatarExtensionForMime('image/jpeg')).toBe('jpg'); + expect(avatarExtensionForMime('IMAGE/PNG; charset=binary')).toBe('png'); + expect(avatarExtensionForMime('image/webp')).toBe('webp'); + }); + + it('rejects types outside the avatar allowlist', () => { + expect(avatarExtensionForMime('image/svg+xml')).toBeNull(); + expect(avatarExtensionForMime('text/html')).toBeNull(); + expect(avatarExtensionForMime('')).toBeNull(); + }); +}); From 27b222f6ec21ee022a49d40d3f95d412ece3b40a Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Wed, 29 Jul 2026 03:38:43 +0200 Subject: [PATCH 75/98] chore(format): Prettier apply --- hooks/useMediaListControls.ts | 3 +-- lib/tmdb/search.ts | 7 +------ lib/validators.ts | 3 ++- tests/unit/validators.test.ts | 6 +++--- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/hooks/useMediaListControls.ts b/hooks/useMediaListControls.ts index f4e583ab..4028777c 100644 --- a/hooks/useMediaListControls.ts +++ b/hooks/useMediaListControls.ts @@ -36,8 +36,7 @@ function parseState(raw: string | null): ListControlsState { ...DEFAULT_LIST_CONTROLS, ...parsed, genreIds: Array.isArray(parsed.genreIds) ? parsed.genreIds : [], - actorId: - typeof parsed.actorId === 'number' ? parsed.actorId : null, + actorId: typeof parsed.actorId === 'number' ? parsed.actorId : null, }; } catch { return DEFAULT_LIST_CONTROLS; diff --git a/lib/tmdb/search.ts b/lib/tmdb/search.ts index 32030010..3981942f 100644 --- a/lib/tmdb/search.ts +++ b/lib/tmdb/search.ts @@ -1,9 +1,4 @@ -import type { - MediaItem, - Movie, - PersonSuggestion, - TvShow, -} from '@/types/tmdb'; +import type { MediaItem, Movie, PersonSuggestion, TvShow } from '@/types/tmdb'; import { fetchTMDB } from './client'; import type { Language } from '@/lib/i18n/translations'; diff --git a/lib/validators.ts b/lib/validators.ts index 4896fcdd..d3259f99 100644 --- a/lib/validators.ts +++ b/lib/validators.ts @@ -158,7 +158,8 @@ export function isStoredAvatarUrl(url: unknown): url is string { /** Maps a fetched avatar's content type to the extension used for its stored file. */ export function avatarExtensionForMime(contentType: string): string | null { const mime = contentType.split(';')[0].trim().toLowerCase(); - if (!(ALLOWED_AVATAR_MIMES as readonly string[]).includes(mime)) return null; + if (!(ALLOWED_AVATAR_MIMES as readonly string[]).includes(mime)) + return null; return mime === 'image/jpeg' ? 'jpg' : mime.slice('image/'.length); } diff --git a/tests/unit/validators.test.ts b/tests/unit/validators.test.ts index 78b61cae..d5c417c5 100644 --- a/tests/unit/validators.test.ts +++ b/tests/unit/validators.test.ts @@ -261,9 +261,9 @@ describe('isImportableAvatarUrl', () => { expect( isImportableAvatarUrl('https://evil.com/lh3.googleusercontent.com') ).toBe(false); - expect(isImportableAvatarUrl('http://169.254.169.254/latest/meta-data')).toBe( - false - ); + expect( + isImportableAvatarUrl('http://169.254.169.254/latest/meta-data') + ).toBe(false); expect(isImportableAvatarUrl('https://googleusercontent.com/a/x')).toBe( false ); From 13d93feb375f957af73f3cc707912174edf01ce1 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Fri, 31 Jul 2026 07:10:34 +0200 Subject: [PATCH 76/98] chore(k8s): optimize autoscaling and ressources for stability --- k8s/app.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/k8s/app.yaml b/k8s/app.yaml index 3e85a57f..ed4ccd0c 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -11,7 +11,7 @@ metadata: labels: app: reelmark spec: - replicas: 1 + replicas: 2 strategy: type: RollingUpdate rollingUpdate: @@ -42,16 +42,16 @@ spec: command: ['sh', '-c', 'sleep 10'] resources: requests: - cpu: '100m' - memory: '256Mi' - limits: - cpu: '500m' + cpu: '250m' memory: '512Mi' + limits: + cpu: '1000m' + memory: '1Gi' readinessProbe: httpGet: path: /api/health port: 3000 - initialDelaySeconds: 5 + initialDelaySeconds: 45 periodSeconds: 10 livenessProbe: httpGet: @@ -83,12 +83,12 @@ spec: apiVersion: apps/v1 kind: Deployment name: reelmark - minReplicas: 1 - maxReplicas: 5 + minReplicas: 2 + maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization - averageUtilization: 70 + averageUtilization: 75 From bdd5d3a6375413783e4f5390ad6cdbae25763f7d Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 06:51:15 +0200 Subject: [PATCH 77/98] =?UTF-8?q?fix(k8s):=20fiabilise=20la=20disponibilit?= =?UTF-8?q?=C3=A9=20des=20pods=20et=20le=20dimensionnement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PodDisruptionBudget maxUnavailable: 1 — correct quelle que soit la taille du ReplicaSet, contrairement à minAvailable qui autoriserait l'éviction de 9 pods sur 10 - topologySpreadConstraints en ScheduleAnyway : répartit sans jamais bloquer le scheduling quand le pool n'a qu'un nœud disponible - requests 500m/1Gi (au lieu de 250m/512Mi) : le HPA cible un pourcentage de la request, 75% de 250m déclenchait un scale-up permanent qui entraînait le node pool dans le même va-et-vient - ephemeral-storage borné : un pod ne peut plus déclencher un DiskPressure qui évincerait tous les pods du nœud (20 Go seulement) - NODE_OPTIONS borne le heap V8 à 768 Mo sous la limite de 1 Gi - startupProbe (60 s) remplace les initialDelaySeconds : la liveness ne peut plus tuer un pod avant qu'il soit prêt - spec.replicas retiré — le champ appartient au HPA - image en placeholder __IMAGE_TAG__ : un apply brut échoue visiblement au lieu de reverter le cluster sur :latest - ingress affinity-mode: balanced — persistent gardait un client collé à un pod disparu Risque : le premier apply peut brièvement ramener le Deployment à 1 pod si la migration vers server-side apply ne préserve pas le champ replicas. Vérifier `kubectl -n reelmark get hpa` après le premier déploiement. --- k8s/app.yaml | 45 +++++++++++++++++++++++++++++++++++++-------- k8s/ingress.yaml | 2 +- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/k8s/app.yaml b/k8s/app.yaml index ed4ccd0c..ccfb7e9f 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -11,7 +11,7 @@ metadata: labels: app: reelmark spec: - replicas: 2 + revisionHistoryLimit: 3 strategy: type: RollingUpdate rollingUpdate: @@ -26,13 +26,25 @@ spec: app: reelmark spec: terminationGracePeriodSeconds: 30 + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: reelmark imagePullSecrets: - name: ghcr-pull containers: - name: reelmark - image: ghcr.io/thesawkit/reelmark:latest + image: ghcr.io/thesawkit/reelmark:__IMAGE_TAG__ ports: - containerPort: 3000 + # Node dimensionne son heap sur la mémoire de la machine, pas sur la limite + # du conteneur : sans cette borne un pic de rendu SSR se fait OOMKill à 1Gi. + env: + - name: NODE_OPTIONS + value: '--max-old-space-size=768' envFrom: - secretRef: name: reelmark-secrets @@ -42,22 +54,28 @@ spec: command: ['sh', '-c', 'sleep 10'] resources: requests: - cpu: '250m' - memory: '512Mi' + cpu: '500m' + memory: '1Gi' + ephemeral-storage: '1Gi' limits: - cpu: '1000m' + cpu: '1500m' memory: '1Gi' + ephemeral-storage: '2Gi' + startupProbe: + httpGet: + path: /api/health + port: 3000 + periodSeconds: 3 + failureThreshold: 20 readinessProbe: httpGet: path: /api/health port: 3000 - initialDelaySeconds: 45 periodSeconds: 10 livenessProbe: httpGet: path: /api/health port: 3000 - initialDelaySeconds: 15 periodSeconds: 20 --- apiVersion: v1 @@ -73,6 +91,17 @@ spec: - port: 80 targetPort: 3000 --- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: reelmark + namespace: reelmark +spec: + maxUnavailable: 1 + selector: + matchLabels: + app: reelmark +--- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: @@ -91,4 +120,4 @@ spec: name: cpu target: type: Utilization - averageUtilization: 75 + averageUtilization: 70 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml index 9d489ab1..81628a46 100644 --- a/k8s/ingress.yaml +++ b/k8s/ingress.yaml @@ -9,7 +9,7 @@ metadata: nginx.ingress.kubernetes.io/proxy-buffers-number: '8' nginx.ingress.kubernetes.io/proxy-busy-buffers-size: '32k' nginx.ingress.kubernetes.io/affinity: 'cookie' - nginx.ingress.kubernetes.io/affinity-mode: 'persistent' + nginx.ingress.kubernetes.io/affinity-mode: 'balanced' nginx.ingress.kubernetes.io/session-cookie-name: 'reelmark-affinity' nginx.ingress.kubernetes.io/session-cookie-max-age: '172800' nginx.ingress.kubernetes.io/session-cookie-samesite: 'Lax' From 661f8128eccf043257c6360fd41372e8fa6d82e3 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 06:51:27 +0200 Subject: [PATCH 78/98] =?UTF-8?q?feat(ci):=20d=C3=A9ploiement=20d=C3=A9cla?= =?UTF-8?q?ratif=20de=20l'app=20et=20de=20l'edge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy.yml passe de `kubectl set image` à `kubectl apply --server-side` : les resources, probes, PDB et HPA partent en prod avec le commit qui les contient. Le mode server-side est requis, pas cosmétique — en client-side l'absence de spec.replicas dans le manifeste le remettrait à son défaut (1 pod) à chaque déploiement au lieu de le laisser au HPA - infra.yml (nouveau) gère ingress-nginx via Helm et k8s/ingress.yaml, sur changement des fichiers edge ou en workflow_dispatch — l'edge ne bouge pas à chaque push applicatif - ingress-nginx passe à 2 replicas avec anti-affinité `required` sur hostname : les deux pods ne peuvent pas partager un nœud, ce qui interdit au cluster autoscaler Infomaniak (1→10, non configurable) de redescendre à 1 nœud. Le plancher de 2 nœuds devient structurel — c'est le correctif des coupures Cloudflare 1016/523 et des 404 « default backend » - chart pinné à 4.15.1 : une version flottante ferait passer l'edge en majeure sans revue au premier déclenchement - concurrency group sur les deux workflows, permissions minimales sur infra --- .github/workflows/deploy.yml | 12 +++++--- .github/workflows/infra.yml | 58 +++++++++++++++++++++++++++++++++++ k8s/ingress-nginx-values.yaml | 20 ++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/infra.yml create mode 100644 k8s/ingress-nginx-values.yaml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a95c19a9..f4f256e8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,6 +9,10 @@ env: IMAGE: ghcr.io/thesawkit/reelmark FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true +concurrency: + group: deploy-app + cancel-in-progress: false + jobs: build-deploy: runs-on: ubuntu-latest @@ -54,8 +58,8 @@ jobs: echo "$KUBECONFIG_B64" | base64 -d > ~/.kube/config chmod 600 ~/.kube/config - - name: Roll out new image + - name: Deploy manifests run: | - kubectl -n reelmark set image deployment/reelmark \ - reelmark=${{ env.IMAGE }}:${{ github.sha }} - kubectl -n reelmark rollout status deployment/reelmark --timeout=180s + sed "s|__IMAGE_TAG__|${{ github.sha }}|g" k8s/app.yaml \ + | kubectl apply --server-side --force-conflicts -f - + kubectl -n reelmark rollout status deployment/reelmark --timeout=300s diff --git a/.github/workflows/infra.yml b/.github/workflows/infra.yml new file mode 100644 index 00000000..ef0a30bc --- /dev/null +++ b/.github/workflows/infra.yml @@ -0,0 +1,58 @@ +name: Infra — ingress-nginx + +on: + push: + branches: [main] + paths: + - 'k8s/ingress-nginx-values.yaml' + - 'k8s/ingress.yaml' + - '.github/workflows/infra.yml' + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + CHART_VERSION: '4.15.1' + +concurrency: + group: infra-ingress + cancel-in-progress: false + +jobs: + ingress: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - uses: azure/setup-helm@v4 + + - uses: azure/setup-kubectl@v4 + + - name: Configure kubeconfig + env: + KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }} + run: | + mkdir -p ~/.kube + echo "$KUBECONFIG_B64" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + # --atomic rollback l'edge si l'upgrade échoue. Le timeout large couvre le + # provisioning d'un nœud par le cluster autoscaler quand l'anti-affinité laisse + # le second replica en Pending. + - name: Upgrade ingress-nginx + run: | + helm upgrade --install ingress-nginx ingress-nginx \ + --repo https://kubernetes.github.io/ingress-nginx \ + --version "$CHART_VERSION" \ + --namespace ingress-nginx --create-namespace \ + --values k8s/ingress-nginx-values.yaml \ + --atomic --timeout 10m + + - name: Apply application Ingress + run: kubectl apply --server-side --force-conflicts -f k8s/ingress.yaml + + - name: Report edge state + run: | + kubectl -n ingress-nginx get pods -o wide + kubectl -n ingress-nginx get svc ingress-nginx-controller -o wide diff --git a/k8s/ingress-nginx-values.yaml b/k8s/ingress-nginx-values.yaml new file mode 100644 index 00000000..2d06c0ae --- /dev/null +++ b/k8s/ingress-nginx-values.yaml @@ -0,0 +1,20 @@ +# Values Helm du chart ingress-nginx. Appliquées par .github/workflows/infra.yml. +controller: + replicaCount: 2 + minAvailable: 1 + # `required` et non `preferred` : les deux replicas ne peuvent PAS partager un nœud, ce qui + # interdit au cluster autoscaler de redescendre le node pool à 1 — le plancher de 2 nœuds + # devient structurel au lieu de dépendre d'un réglage du Manager Infomaniak. + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/component: controller + resources: + requests: + cpu: '100m' + memory: '128Mi' From 1db413b7889b564a3ae65ad4ebbbcfcd95eb0fdd Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 06:51:36 +0200 Subject: [PATCH 79/98] =?UTF-8?q?fix(docker):=20=C3=A9choue=20le=20build?= =?UTF-8?q?=20si=20un=20build-arg=20client=20est=20vide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un secret GitHub vide produisait un bundle client avec des valeurs undefined pour Supabase, sans aucune erreur au build — l'app partait en prod cassée. Même garde que celle déjà en place sur tmdb_token, sur les trois variables sans lesquelles le client est inutilisable. .dockerignore exclut docs, context-ai et tsconfig.tsbuildinfo (645 Ko) qui invalidaient le cache de build à chaque modification. --- .dockerignore | 3 +++ Dockerfile | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/.dockerignore b/.dockerignore index 56896024..40f2be85 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,9 @@ playwright-report test-results coverage k8s +docs +context-ai +tsconfig.tsbuildinfo *.md .env* Dockerfile diff --git a/Dockerfile b/Dockerfile index 54f01cde..3b095e66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,10 @@ COPY . . # entrées "use cache" : il appelle donc TMDB. Le token passe par un secret BuildKit, jamais # par un ARG — un build-arg resterait lisible dans l'historique de l'image. RUN --mount=type=secret,id=tmdb_token \ + set -e; \ + test -n "$NEXT_PUBLIC_SUPABASE_URL" || { echo "ERREUR: build-arg 'NEXT_PUBLIC_SUPABASE_URL' vide — le bundle client serait inutilisable." >&2; exit 1; }; \ + test -n "$NEXT_PUBLIC_SUPABASE_ANON_KEY" || { echo "ERREUR: build-arg 'NEXT_PUBLIC_SUPABASE_ANON_KEY' vide — le bundle client serait inutilisable." >&2; exit 1; }; \ + test -n "$NEXT_PUBLIC_BASE_URL" || { echo "ERREUR: build-arg 'NEXT_PUBLIC_BASE_URL' vide — le bundle client serait inutilisable." >&2; exit 1; }; \ if [ ! -s /run/secrets/tmdb_token ]; then \ echo "ERREUR: secret de build 'tmdb_token' manquant — le prerender appelle TMDB." >&2; \ exit 1; \ From 8dd4fa4cca7f12175c05ae711bdbe27b5c4f76ad Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 06:51:36 +0200 Subject: [PATCH 80/98] =?UTF-8?q?docs(deployment):=20met=20=C3=A0=20jour?= =?UTF-8?q?=20la=20proc=C3=A9dure=20Infomaniak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plancher de 2 nœuds imposé par l'anti-affinité ingress, l'autoscaling du node pool Infomaniak (1→10) n'étant pas configurable - dimensionnement détaillé pour a2-ram4-disk20-perf1 (2 pods par nœud) - deux workflows, deux rythmes : app à chaque push, edge sur demande - raison du server-side apply - Cache Rule Cloudflare sur /_next/static — sort les assets des pods et fait tomber la charge CPU qui déclenche le HPA - section Scaling corrigée (disait 2→5 à 70%, le manifeste dit 2→10) --- DEPLOYMENT.md | 75 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 0a96297e..37b02392 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -23,7 +23,13 @@ Supabase / TMDB / Bugsink restent externes. Registry d'image : **ghcr.io**. Via le **Manager Infomaniak** (UI) ou l'API/CLI — voir la [doc Kubernetes](https://docs.infomaniak.cloud/tutorials/kubernetes/kubernetes_services/) et le [repo d'exemple Infomaniak](https://github.com/Infomaniak/Public_Cloud_Kubernetes). -- Control plane managé (facturé) + 1 node pool (2–3 instances pour commencer). +- Control plane managé (facturé) + 1 node pool en `a2-ram4-disk20-perf1` (2 vCPU / 4 Go / 20 Go). +- L'autoscaling du node pool Infomaniak va de 1 à 10, non configurable. **Le plancher de 2 nœuds + est imposé côté cluster**, par l'anti-affinité `required` des deux replicas d'`ingress-nginx` + (`k8s/ingress-nginx-values.yaml`) : ils ne peuvent pas partager un nœud, donc le cluster + autoscaler ne peut pas redescendre à 1. Sans ce plancher, `ingress-nginx`, `coredns` et les pods + applicatifs tiennent sur le même nœud et chaque scale-down coupe le service 10–30 s + (Cloudflare 1016/523, puis 404 « default backend » le temps que l'ingress resynchronise). - Récupérer le **kubeconfig** depuis le Manager → `~/.kube/config`. - Vérifier : `kubectl get nodes`. @@ -83,12 +89,24 @@ Sans lui, `kubectl get hpa -n reelmark` affiche `cpu: ` et l'autoscalin ## 3. Ingress + LoadBalancer +Géré par `.github/workflows/infra.yml` (déclenché par un changement de +`k8s/ingress-nginx-values.yaml` ou `k8s/ingress.yaml`, sinon `workflow_dispatch`). Pour le premier +bootstrap, avant que les secrets GitHub existent, la même commande à la main : + ```bash helm upgrade --install ingress-nginx ingress-nginx \ --repo https://kubernetes.github.io/ingress-nginx \ - --namespace ingress-nginx --create-namespace + --version 4.15.1 \ + --namespace ingress-nginx --create-namespace \ + --values k8s/ingress-nginx-values.yaml ``` +La version du chart est pinnée dans `.github/workflows/infra.yml` (`CHART_VERSION`) : une version +flottante ferait passer l'edge en majeure au premier déclenchement du workflow, sans revue. + +Le second replica reste `Pending` tant que le cluster n'a qu'un nœud : c'est le signal attendu, +le cluster autoscaler en provisionne un second dans la foulée. + Le Service `LoadBalancer` provisionne un **LB Octavia** (facturé). Récupérer son IP publique : ```bash @@ -105,6 +123,11 @@ kubectl -n ingress-nginx get svc ingress-nginx-controller -o wide ``` 3. **Images** : activer **Polish** (WebP/AVIF auto) + **Cache** — remplace l'optim in-pod (voir note images plus bas). 4. **WAF** : activer les règles managées de base. +5. **Cache Rule sur les assets** (Rules → Cache Rules) : expression + `starts_with(http.request.uri.path, "/_next/static/")` → _Eligible for cache_, + Edge TTL 1 an. Ces fichiers sont immutables (hachés par build). C'est l'optimisation la plus + rentable du setup : elle sort les assets des pods et fait tomber la charge CPU qui déclenche + le HPA. ## 5. Registry ghcr.io — pull secret @@ -130,17 +153,41 @@ kubectl -n reelmark create secret generic reelmark-secrets \ ## 7. Déployer les manifests -L'image est déjà réglée (`ghcr.io/thesawkit/reelmark`). Déployer : +`k8s/app.yaml` porte le placeholder `__IMAGE_TAG__` au lieu d'un tag figé : le CI y substitue le +SHA du commit. Un `kubectl apply` brut échouerait en `ImagePullBackOff` — c'est voulu, ça vaut +mieux qu'un `:latest` qui reverte silencieusement le cluster sur une image inconnue. ```bash -kubectl apply -f k8s/app.yaml -kubectl apply -f k8s/ingress.yaml +sed "s|__IMAGE_TAG__|$(git rev-parse HEAD)|g" k8s/app.yaml \ + | kubectl apply --server-side --force-conflicts -f - kubectl -n reelmark rollout status deployment/reelmark ``` +`--server-side` n'est pas cosmétique : voir la section 8 pour la raison (`spec.replicas` appartient +au HPA). `k8s/ingress.yaml` n'est pas dans ce bloc — il est déployé par +`.github/workflows/infra.yml`, l'edge ne doit pas bouger à chaque push de code. + ## 8. CI/CD (GitHub Actions) -`.github/workflows/deploy.yml` build l'image, la push sur ghcr.io et roll le déploiement à chaque push sur `main`. +`.github/workflows/deploy.yml` build l'image, la push sur ghcr.io, puis **applique `k8s/app.yaml`** +avec le SHA substitué à chaque push sur `main`. Le déploiement est déclaratif : toute modification +des resources, probes, PDB ou HPA part en prod avec le commit qui la contient — inutile de +réappliquer à la main. + +Corollaire : le Deployment ne déclare **pas** `spec.replicas`. Ce champ appartient au HPA ; le +laisser dans le manifeste ramènerait le nombre de pods à sa valeur écrite à chaque déploiement, +annulant l'autoscaling en pleine charge. L'apply se fait donc en **`--server-side`** : en +client-side, l'absence du champ le remettrait à son défaut (1 pod) à chaque déploiement, alors +qu'en server-side il reste la propriété du field manager `horizontal-pod-autoscaler`. +`--force-conflicts` est nécessaire une fois, pour reprendre les champs encore détenus par +l'ancien manager `kubectl-client-side-apply`. + +Deux workflows, deux rythmes : + +| Workflow | Déclencheur | Portée | +| ------------------------------ | ------------------------------------ | ------------------------------------------ | +| `.github/workflows/deploy.yml` | tout push sur `main` | image + `k8s/app.yaml` | +| `.github/workflows/infra.yml` | changement des fichiers edge, manuel | chart `ingress-nginx` + `k8s/ingress.yaml` | **Secrets GitHub à définir** (Settings → Secrets → Actions) : @@ -168,8 +215,20 @@ curl -I https://reelmark.silexio.be - **Build** : reste `next build --webpack` (Serwist ne supporte pas Turbopack). Le Dockerfile l'exécute via `pnpm build`. Le stage deps copie `pnpm-workspace.yaml` (il contient `overrides: postcss` + `allowBuilds`) — sinon `pnpm install --frozen-lockfile` échoue (`ERR_PNPM_LOCKFILE_CONFIG_MISMATCH`). - **Images** : `next.config.ts` est passé en `images.unoptimized: true` → pods légers, pas de `sharp`. Le resize/format est délégué à **Cloudflare Polish**. Les posters TMDB sont déjà dimensionnés par URL (`w500`, etc.). Pour revenir à l'optim Next in-pod : retirer `unoptimized`, ajouter `sharp` au Dockerfile runner. - **NEXT_PUBLIC_*** : injectés au **build** (inlinés dans le bundle client) ET présents au runtime (SSR). Les secrets serveur sont runtime-only. -- **Probes** : `/api/health` (route `app/api/health/route.ts`). -- **Scaling** : HPA 2→5 pods sur CPU 70%. +- **Probes** : `/api/health` (route `app/api/health/route.ts`). Un `startupProbe` couvre le + démarrage (60 s max) ; readiness et liveness ne s'activent qu'ensuite, donc un boot lent sur un + nœud chargé ne peut plus déclencher un CrashLoopBackOff. +- **Scaling** : HPA 2→10 pods sur CPU 70 % de la request (350m). Le seuil se lit toujours en + pourcentage de `requests.cpu`, jamais de la limite — une request trop basse fait scaler le HPA + en permanence et entraîne le node pool dans le même va-et-vient. +- **Dimensionnement (`a2-ram4-disk20-perf1`)** : ~1 500m CPU et ~2 600 Mi restent à l'app par nœud + une fois kubelet et DaemonSets déduits, soit **2 pods par nœud** (500m + 1Gi de requests). + `memory.limit == memory.request` place les pods en QoS Burstable protégée : ils ne sont pas + candidats à l'éviction avant les BestEffort sous MemoryPressure. `NODE_OPTIONS` borne le heap V8 + à 768 Mo sous la limite de 1 Gi. +- **Disponibilité** : PDB `maxUnavailable: 1` (correct quelle que soit la taille du ReplicaSet, + contrairement à `minAvailable`) + `topologySpreadConstraints` en `ScheduleAnyway` pour répartir + les pods sans jamais bloquer le scheduling quand le pool n'a qu'un nœud disponible. ## Sources From 6b3d7944a1d68f84f4cdb87505a3342c87e99300 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 07:00:23 +0200 Subject: [PATCH 81/98] =?UTF-8?q?fix(k8s):=20applique=20le=20profil=20Pod?= =?UTF-8?q?=20Security=20=C2=AB=20restricted=20=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runAsNonRoot en uid/gid 1001 (l'utilisateur créé par le Dockerfile), seccompProfile RuntimeDefault, allowPrivilegeEscalation: false, toutes les capabilities retirées - readOnlyRootFilesystem: true, avec deux emptyDir bornés sur les seuls chemins que le serveur standalone écrit à l'exécution — /app/.next/cache et /tmp. Le fsGroup: 1001 du pod est ce qui les rend inscriptibles : sans lui un emptyDir reste root:root en 0755 et le conteneur non-root échoue au premier write de cache - automountServiceAccountToken: false — l'app ne parle jamais à l'API Kubernetes, le token monté par défaut est une surface d'attaque offerte - timeoutSeconds explicites sur les trois probes. Le défaut de 1 s suffit à faire redémarrer un pod dont l'event loop est simplement occupé à rendre du SSR, et la charge se reporte alors sur les survivants — la panne en cascade classique de la liveness probe - terminationGracePeriodSeconds 30 → 45. Next termine les requêtes en vol et les callbacks after() avant de sortir sur SIGTERM et demande 10 à 30 s de drain ; toutes les Server Actions du projet déferrent leur revalidation dans after() Réf: kubernetes.io/docs/concepts/security/pod-security-standards nextjs.org/docs/app/guides/self-hosting blog.colinbreck.com/kubernetes-liveness-and-readiness-probes --- k8s/app.yaml | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/k8s/app.yaml b/k8s/app.yaml index ccfb7e9f..01997dd8 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -25,7 +25,19 @@ spec: labels: app: reelmark spec: - terminationGracePeriodSeconds: 30 + # Next termine les requêtes en vol ET les callbacks after() avant de sortir sur + # SIGTERM ; il lui faut un drain de 10 à 30 s. Ici : 10 s de preStop pour laisser + # l'endpoint quitter le Service, puis 35 s de drain. Toutes les Server Actions du + # projet déferrent leur revalidation dans after() — un drain trop court les perd. + terminationGracePeriodSeconds: 45 + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + seccompProfile: + type: RuntimeDefault topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -48,6 +60,19 @@ spec: envFrom: - secretRef: name: reelmark-secrets + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ['ALL'] + # Les deux seuls chemins que le serveur standalone écrit à l'exécution. Le + # `fsGroup` du pod les rend accessibles à l'uid 1001 — sans lui un emptyDir + # reste root:root en 0755 et le conteneur non-root ne peut pas y écrire. + volumeMounts: + - name: next-cache + mountPath: /app/.next/cache + - name: tmp + mountPath: /tmp lifecycle: preStop: exec: @@ -61,22 +86,38 @@ spec: cpu: '1500m' memory: '1Gi' ephemeral-storage: '2Gi' + # `timeoutSeconds` vaut 1 s par défaut : sous un pic de rendu SSR l'event loop + # dépasse ce seuil, trois fois de suite le pod redémarre, la charge se reporte + # sur les survivants et l'incident se propage. Les seuils ci-dessous laissent + # 60 s d'indisponibilité franche avant de conclure à un blocage réel. startupProbe: httpGet: path: /api/health port: 3000 periodSeconds: 3 + timeoutSeconds: 3 failureThreshold: 20 readinessProbe: httpGet: path: /api/health port: 3000 periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 livenessProbe: httpGet: path: /api/health port: 3000 periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + volumes: + - name: next-cache + emptyDir: + sizeLimit: 1Gi + - name: tmp + emptyDir: + sizeLimit: 256Mi --- apiVersion: v1 kind: Service From 013a228980846bc490d4c183525f15d65b26b3af Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 07:00:23 +0200 Subject: [PATCH 82/98] =?UTF-8?q?fix(k8s):=20r=C3=A9tablit=20le=20streamin?= =?UTF-8?q?g=20SSR=20derri=C3=A8re=20nginx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nginx bufferise les réponses upstream par défaut : il attend la réponse complète avant de l'envoyer au client, ce qui annule le streaming des sections des fiches détail. La bannière ne peignait plus avant que les données Supabase soient arrivées, alors que les pages sont précisément construites pour ne bloquer que sur le trio TMDB. Réf: nextjs.org/docs/app/guides/self-hosting#streaming-and-suspense --- k8s/ingress.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml index 81628a46..9593a537 100644 --- a/k8s/ingress.yaml +++ b/k8s/ingress.yaml @@ -4,6 +4,10 @@ metadata: name: reelmark namespace: reelmark annotations: + # nginx bufferise les réponses upstream par défaut : il attend la réponse complète avant + # de l'envoyer, ce qui annule le streaming des sections des fiches détail — + # la page arrive d'un bloc au lieu de peindre la bannière en premier. + nginx.ingress.kubernetes.io/proxy-buffering: 'off' nginx.ingress.kubernetes.io/proxy-body-size: '10m' nginx.ingress.kubernetes.io/proxy-buffer-size: '16k' nginx.ingress.kubernetes.io/proxy-buffers-number: '8' From 182bb2e854d6d25da4dc3848271f55bda5ea1e3a Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 07:00:23 +0200 Subject: [PATCH 83/98] feat(k8s): cloisonne le namespace en default-deny MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trois flux seulement : entrée depuis ingress-nginx sur 3000, DNS vers kube-system, HTTPS sortant hors plages RFC1918. Un pod compromis ne peut donc ni scanner le cluster ni joindre l'API server. Volontairement hors CI : une erreur de périmètre coupe l'app sans message clair. La procédure de pose et de retour arrière est dans DEPLOYMENT.md, avec le point à vérifier au préalable (Bugsink joignable en IP publique). --- k8s/network-policy.yaml | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 k8s/network-policy.yaml diff --git a/k8s/network-policy.yaml b/k8s/network-policy.yaml new file mode 100644 index 00000000..11106436 --- /dev/null +++ b/k8s/network-policy.yaml @@ -0,0 +1,56 @@ +# Cloisonnement réseau du namespace. NON appliqué par la CI — voir DEPLOYMENT.md avant de le +# poser : une erreur de périmètre coupe l'app en silence. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny + namespace: reelmark +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: reelmark + namespace: reelmark +spec: + podSelector: + matchLabels: + app: reelmark + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + ports: + - protocol: TCP + port: 3000 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + # Supabase, TMDB, Watchmode et Bugsink sont tous joignables en HTTPS public. Les plages + # RFC1918 sont exclues : un pod compromis ne peut ni scanner le cluster ni joindre + # l'API server, qui ne lui sert à rien (automountServiceAccountToken est déjà à false). + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + ports: + - protocol: TCP + port: 443 From b4cf38dacc7ef65d0eebde85b39bb71214f1552a Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 07:00:23 +0200 Subject: [PATCH 84/98] =?UTF-8?q?docs(deployment):=20documente=20le=20durc?= =?UTF-8?q?issement=20et=20le=20cloisonnement=20r=C3=A9seau?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - profil restricted, chemins inscriptibles et rôle du fsGroup - drain SIGTERM aligné sur les callbacks after() - seuils de probes et raison du timeoutSeconds explicite - section 7 bis : pose et retrait de la NetworkPolicy - note streaming (proxy-buffering) Sources officielles liées en bas de section. --- DEPLOYMENT.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 37b02392..2294d23f 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -167,6 +167,26 @@ kubectl -n reelmark rollout status deployment/reelmark au HPA). `k8s/ingress.yaml` n'est pas dans ce bloc — il est déployé par `.github/workflows/infra.yml`, l'edge ne doit pas bouger à chaque push de code. +## 7 bis. Cloisonnement réseau (optionnel, à poser à la main) + +`k8s/network-policy.yaml` passe le namespace en default-deny et n'ouvre que trois flux : entrée +depuis `ingress-nginx` sur 3000, DNS vers `kube-system`, et HTTPS sortant **hors plages RFC1918**. +Un pod compromis ne peut donc ni scanner le cluster, ni joindre l'API server. + +Volontairement hors CI : une erreur de périmètre coupe l'app sans message clair. + +```bash +kubectl apply -f k8s/network-policy.yaml +kubectl -n reelmark logs -l app=reelmark --tail=50 # guetter les timeouts Supabase/TMDB +curl -I https://reelmark.silexio.be +kubectl delete -f k8s/network-policy.yaml # retour arrière immédiat +``` + +Prérequis : Cilium applique les NetworkPolicy standard, c'est déjà le CNI du cluster. Le seul +point à vérifier avant de poser la policy est **Bugsink** (`sentry.silexio.be`) : la règle suppose +qu'il est joignable par IP publique. S'il est hébergé sur une IP privée du même Public Cloud, il +faut lui ajouter un `to.ipBlock` dédié, sinon les rapports d'erreur partent en silence. + ## 8. CI/CD (GitHub Actions) `.github/workflows/deploy.yml` build l'image, la push sur ghcr.io, puis **applique `k8s/app.yaml`** @@ -217,7 +237,12 @@ curl -I https://reelmark.silexio.be - **NEXT_PUBLIC_*** : injectés au **build** (inlinés dans le bundle client) ET présents au runtime (SSR). Les secrets serveur sont runtime-only. - **Probes** : `/api/health` (route `app/api/health/route.ts`). Un `startupProbe` couvre le démarrage (60 s max) ; readiness et liveness ne s'activent qu'ensuite, donc un boot lent sur un - nœud chargé ne peut plus déclencher un CrashLoopBackOff. + nœud chargé ne peut plus déclencher un CrashLoopBackOff. L'endpoint ne touche volontairement + aucune dépendance externe : une liveness qui teste Supabase ferait redémarrer tous les pods le + jour où Supabase ralentit. +- **Streaming** : `proxy-buffering: 'off'` sur l'Ingress. nginx bufferise les réponses upstream + par défaut et attend la réponse complète, ce qui annule le streaming des sections `` + des fiches détail — la bannière ne peint plus avant les données Supabase. - **Scaling** : HPA 2→10 pods sur CPU 70 % de la request (350m). Le seuil se lit toujours en pourcentage de `requests.cpu`, jamais de la limite — une request trop basse fait scaler le HPA en permanence et entraîne le node pool dans le même va-et-vient. @@ -229,6 +254,26 @@ curl -I https://reelmark.silexio.be - **Disponibilité** : PDB `maxUnavailable: 1` (correct quelle que soit la taille du ReplicaSet, contrairement à `minAvailable`) + `topologySpreadConstraints` en `ScheduleAnyway` pour répartir les pods sans jamais bloquer le scheduling quand le pool n'a qu'un nœud disponible. +- **Durcissement** : le pod satisfait le profil [Pod Security Standard `restricted`][pss] — + `runAsNonRoot` en uid/gid 1001 (l'utilisateur créé par le Dockerfile), `seccompProfile: +RuntimeDefault`, `allowPrivilegeEscalation: false`, toutes les capabilities retirées, + `readOnlyRootFilesystem: true` et `automountServiceAccountToken: false` (l'app ne parle jamais + à l'API Kubernetes). Deux `emptyDir` bornés couvrent les seuls chemins que le serveur + standalone écrit : `/app/.next/cache` et `/tmp`. Le `fsGroup: 1001` du pod est ce qui les rend + inscriptibles — sans lui un `emptyDir` reste `root:root` en 0755 et le conteneur non-root + échoue au premier write de cache. +- **Arrêt** : Next termine les requêtes en vol **et les callbacks `after()`** avant de sortir sur + SIGTERM, et [demande un drain de 10 à 30 s][self-host]. Toutes les Server Actions du projet + déferrent leur revalidation dans `after()`, d'où `terminationGracePeriodSeconds: 45` (10 s de + `preStop` pour sortir du Service, puis 35 s de drain). +- **Probes** : `timeoutSeconds` vaut 1 s par défaut, ce qui suffit à faire redémarrer un pod dont + l'event loop est simplement occupé à rendre du SSR — le grand classique de la [panne en + cascade provoquée par la liveness probe][breck]. Les seuils retenus tolèrent 60 s + d'indisponibilité franche avant de conclure à un blocage réel. + +[pss]: https://kubernetes.io/docs/concepts/security/pod-security-standards/ +[self-host]: https://nextjs.org/docs/app/guides/self-hosting +[breck]: https://blog.colinbreck.com/kubernetes-liveness-and-readiness-probes-how-to-avoid-shooting-yourself-in-the-foot/ ## Sources From 7a0a297fe4deb6c2efbaae20837a52830a667d63 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 07:13:37 +0200 Subject: [PATCH 85/98] =?UTF-8?q?fix(build):=20prot=C3=A8ge=20les=20d?= =?UTF-8?q?=C3=A9ploiements=20du=20version=20skew?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NEXT_DEPLOYMENT_ID reçoit le SHA du commit. Next suffixe alors les assets en `?dpl=` et compare l'identifiant du client à celui du serveur : en cas d'écart il déclenche un rechargement complet au lieu de demander un chunk que le pod ne sert plus. C'est le correctif de fond des 404 pendant un rolling update — l'affinité par cookie n'en réduisait que la probabilité - NEXT_SERVER_ACTIONS_ENCRYPTION_KEY par secret BuildKit, optionnel avec avertissement. Sans clé fixe, chaque build tire la sienne et une Server Action chiffrée par l'ancien pod échoue sur le nouveau (« Failed to find Server Action ») pendant toute la fenêtre du déploiement Réf: nextjs.org/docs/app/api-reference/config/next-config-js/deploymentId nextjs.org/docs/app/guides/self-hosting#multi-server-deployments --- .github/workflows/deploy.yml | 2 ++ Dockerfile | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f4f256e8..de884c3e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -45,8 +45,10 @@ jobs: NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }} NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }} NEXT_PUBLIC_VAPID_PUBLIC_KEY=${{ secrets.NEXT_PUBLIC_VAPID_PUBLIC_KEY }} + NEXT_DEPLOYMENT_ID=${{ github.sha }} secrets: | tmdb_token=${{ secrets.TMDB_READ_ACCESS_TOKEN }} + actions_encryption_key=${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} - uses: azure/setup-kubectl@v4 diff --git a/Dockerfile b/Dockerfile index 3b095e66..5aa7418e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,11 +16,16 @@ ARG NEXT_PUBLIC_SENTRY_DSN # Inlinée dans le bundle client au build : la fournir au runtime seul ne suffit pas, # pushManager.subscribe() recevrait undefined et le toggle resterait sans effet. ARG NEXT_PUBLIC_VAPID_PUBLIC_KEY +# Protection contre le version skew : les assets sont suffixés `?dpl=`, et un client resté sur +# l'ancien build recharge la page au lieu de demander un chunk qui n'existe plus (404 pendant +# toute la fenêtre d'un rolling update). +ARG NEXT_DEPLOYMENT_ID ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL \ NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY \ NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL \ NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN \ NEXT_PUBLIC_VAPID_PUBLIC_KEY=$NEXT_PUBLIC_VAPID_PUBLIC_KEY \ + NEXT_DEPLOYMENT_ID=$NEXT_DEPLOYMENT_ID \ NEXT_TELEMETRY_DISABLED=1 COPY --from=deps /app/node_modules ./node_modules COPY . . @@ -28,6 +33,7 @@ COPY . . # entrées "use cache" : il appelle donc TMDB. Le token passe par un secret BuildKit, jamais # par un ARG — un build-arg resterait lisible dans l'historique de l'image. RUN --mount=type=secret,id=tmdb_token \ + --mount=type=secret,id=actions_encryption_key \ set -e; \ test -n "$NEXT_PUBLIC_SUPABASE_URL" || { echo "ERREUR: build-arg 'NEXT_PUBLIC_SUPABASE_URL' vide — le bundle client serait inutilisable." >&2; exit 1; }; \ test -n "$NEXT_PUBLIC_SUPABASE_ANON_KEY" || { echo "ERREUR: build-arg 'NEXT_PUBLIC_SUPABASE_ANON_KEY' vide — le bundle client serait inutilisable." >&2; exit 1; }; \ @@ -36,11 +42,18 @@ RUN --mount=type=secret,id=tmdb_token \ echo "ERREUR: secret de build 'tmdb_token' manquant — le prerender appelle TMDB." >&2; \ exit 1; \ fi; \ + if [ -s /run/secrets/actions_encryption_key ]; then \ + export NEXT_SERVER_ACTIONS_ENCRYPTION_KEY="$(cat /run/secrets/actions_encryption_key)"; \ + else \ + echo "ATTENTION: secret 'actions_encryption_key' absent — chaque build tire sa propre clé, et une Server Action chiffrée par un pod échoue sur l'autre pendant un rolling update." >&2; \ + fi; \ TMDB_READ_ACCESS_TOKEN="$(cat /run/secrets/tmdb_token)" pnpm build FROM base AS runner +ARG NEXT_DEPLOYMENT_ID ENV NODE_ENV=production \ NEXT_TELEMETRY_DISABLED=1 \ + NEXT_DEPLOYMENT_ID=$NEXT_DEPLOYMENT_ID \ PORT=3000 \ HOSTNAME=0.0.0.0 RUN addgroup -g 1001 -S nodejs && adduser -u 1001 -S nextjs -G nodejs From ac0596b1069898805ec1ac7bb3bae7afdeb2783d Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 07:13:37 +0200 Subject: [PATCH 86/98] =?UTF-8?q?fix(k8s):=20lisse=20l'autoscaling=20et=20?= =?UTF-8?q?d=C3=A9bloque=20les=20drains?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HPA behavior : sans lui le contrôleur double le nombre de pods toutes les 15 s. Chaque paire de pods réclame un nœud au cluster autoscaler, qui met 1 à 3 min à le provisionner — la montée brutale commande des machines dont le pic n'a plus besoin quand elles arrivent. Fenêtre de 60 s et +2 pods/min à la montée, 50%/min à la descente - PDB unhealthyPodEvictionPolicy: AlwaysAllow — un pod déjà cassé immobilisait le budget et bloquait le drain d'un nœud, alors que l'évincer restaure la disponibilité au lieu de la dégrader --- k8s/app.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/k8s/app.yaml b/k8s/app.yaml index 01997dd8..73d7d8e1 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -139,6 +139,9 @@ metadata: namespace: reelmark spec: maxUnavailable: 1 + # Un pod déjà cassé ne doit pas immobiliser le budget et bloquer le drain d'un nœud : + # l'évincer restaure la disponibilité, il ne la dégrade pas. + unhealthyPodEvictionPolicy: AlwaysAllow selector: matchLabels: app: reelmark @@ -162,3 +165,19 @@ spec: target: type: Utilization averageUtilization: 70 + # Sans `behavior`, le HPA double le nombre de pods toutes les 15 s. Chaque paire de pods + # demande un nœud au cluster autoscaler, qui met 1 à 3 min à le provisionner : la montée + # brutale réclame des machines que le pic n'aura plus besoin quand elles arriveront. + behavior: + scaleUp: + stabilizationWindowSeconds: 60 + policies: + - type: Pods + value: 2 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Percent + value: 50 + periodSeconds: 60 From b75302d63ddea3fe29877a4930b171134c0da2c4 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 07:13:37 +0200 Subject: [PATCH 87/98] =?UTF-8?q?docs(deployment):=20version=20skew,=20cl?= =?UTF-8?q?=C3=A9=20Server=20Actions=20et=20acc=C3=A8s=20direct=20=C3=A0?= =?UTF-8?q?=20l'origine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - génération et rôle de NEXT_SERVER_ACTIONS_ENCRYPTION_KEY - effet de NEXT_DEPLOYMENT_ID sur les 404 de rolling update - section 7 ter : le LB Octavia est joignable sans passer par Cloudflare, donc sans WAF ni rate limiting — et cf-connecting-ip est alors forgeable. Fix par whitelist des plages Cloudflare, avec le prérequis à vérifier d'abord (le SNAT d'Octavia peut masquer l'IP source et faire tout rejeter) --- DEPLOYMENT.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 2294d23f..78873e22 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -187,6 +187,38 @@ point à vérifier avant de poser la policy est **Bugsink** (`sentry.silexio.be` qu'il est joignable par IP publique. S'il est hébergé sur une IP privée du même Public Cloud, il faut lui ajouter un `to.ipBlock` dédié, sinon les rapports d'erreur partent en silence. +## 7 ter. L'origine est joignable en direct — à traiter + +Le LB Octavia a une IP publique. Qui la connaît peut taper l'origine sans passer par Cloudflare, +donc sans WAF, sans cache, et **sans le rate limiting**. Le cas le plus gênant : +`lib/rate-limiter.ts` lit `cf-connecting-ip` en premier, un header qu'un appelant direct forge +librement — il peut donc à la fois usurper une IP et remettre son compteur à zéro à chaque requête. + +Le correctif est de n'accepter que les plages Cloudflare ([liste officielle][cf-ips]) : + +```yaml +nginx.ingress.kubernetes.io/whitelist-source-range: '173.245.48.0/20,103.21.244.0/22,...' +``` + +**Vérifier d'abord que nginx voit la vraie IP source.** Derrière un LoadBalancer Octavia en +`externalTrafficPolicy: Cluster` (le défaut), le SNAT remplace l'IP du client par celle du nœud : +la whitelist rejetterait alors tout le trafic, y compris légitime. + +```bash +kubectl -n ingress-nginx logs -l app.kubernetes.io/component=controller --tail=20 +``` + +Si l'IP en début de ligne est une adresse Cloudflare, la whitelist est applicable telle quelle. +Si c'est une adresse en `172.21.x.x` (le subnet du cluster), il faut d'abord passer le Service en +`externalTrafficPolicy: Local` ou activer le PROXY protocol côté Octavia. + +[OPINION] L'alternative sans liste à maintenir est [Authenticated Origin Pulls][cf-mtls] : mTLS +entre Cloudflare et l'origine, via les annotations `auth-tls-*` de l'Ingress. Plus robuste, plus +long à câbler — à privilégier si les mises à jour de plages IP deviennent pénibles. + +[cf-ips]: https://www.cloudflare.com/ips/ +[cf-mtls]: https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/ + ## 8. CI/CD (GitHub Actions) `.github/workflows/deploy.yml` build l'image, la push sur ghcr.io, puis **applique `k8s/app.yaml`** @@ -219,6 +251,24 @@ Deux workflows, deux rythmes : | `NEXT_PUBLIC_SENTRY_DSN` | build-arg (client, https) | | `KUBECONFIG_B64` | `base64 -w0 ~/.kube/config` | +**`NEXT_SERVER_ACTIONS_ENCRYPTION_KEY`** — à générer une fois, puis à ne plus jamais changer : + +```bash +openssl rand -base64 32 +``` + +Next chiffre les variables capturées par une Server Action avant de les envoyer au client, avec +une clé tirée à chaque build par défaut. Pendant un rolling update les deux versions coexistent : +une action chiffrée par l'ancien pod ne se déchiffre pas sur le nouveau, et l'utilisateur reçoit +« Failed to find Server Action ». Une clé fixe supprime la fenêtre. Le build passe sans le secret, +avec un avertissement. + +Le workflow passe aussi `NEXT_DEPLOYMENT_ID=${{ github.sha }}` en build-arg. Next suffixe alors +les assets en `?dpl=` et compare l'identifiant du client à celui du serveur : en cas +d'écart, il déclenche un rechargement complet au lieu de demander un chunk qui n'existe plus. +C'est le correctif de fond des 404 pendant un déploiement — l'affinité par cookie ne faisait +qu'en réduire la probabilité. + Les secrets **serveur** (`SUPABASE_SERVICE_ROLE_KEY`, `SENTRY_DSN`, `TMDB_READ_ACCESS_TOKEN`, `WATCHMODE_API_KEY`) vivent **uniquement** dans le secret K8s `reelmark-secrets`, jamais dans l'image. ## 9. Vérifier From b84801b00d86a8fc4ec4b4a371bd23c2d9ceb417 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 14:32:16 +0200 Subject: [PATCH 88/98] docs(deployment): corrige la note sur l'optimisation des images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le fichier affirmait que le resize/format était délégué à Cloudflare Polish. C'est faux : avec images.unoptimized, next/image émet un src pointant sur image.tmdb.org, un domaine tiers. Les posters ne traversent jamais le proxy Cloudflare du domaine, et Polish ne couvre que les images servies par l'origine. L'état actuel reste le bon compromis à ce dimensionnement — la note explique désormais pourquoi, et ce que coûteraient les deux alternatives. --- DEPLOYMENT.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 78873e22..5a03b29d 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -283,7 +283,14 @@ curl -I https://reelmark.silexio.be ## Notes - **Build** : reste `next build --webpack` (Serwist ne supporte pas Turbopack). Le Dockerfile l'exécute via `pnpm build`. Le stage deps copie `pnpm-workspace.yaml` (il contient `overrides: postcss` + `allowBuilds`) — sinon `pnpm install --frozen-lockfile` échoue (`ERR_PNPM_LOCKFILE_CONFIG_MISMATCH`). -- **Images** : `next.config.ts` est passé en `images.unoptimized: true` → pods légers, pas de `sharp`. Le resize/format est délégué à **Cloudflare Polish**. Les posters TMDB sont déjà dimensionnés par URL (`w500`, etc.). Pour revenir à l'optim Next in-pod : retirer `unoptimized`, ajouter `sharp` au Dockerfile runner. +- **Images** : `next.config.ts` est passé en `images.unoptimized: true` → pods légers, pas de + `sharp`. Les posters sont servis **directement par le CDN de TMDB** (`image.tmdb.org`), déjà + dimensionnés par URL (`w92`, `w500`…). Ils ne traversent donc jamais le proxy Cloudflare du + domaine : **Polish ne s'y applique pas**, il ne couvre que les images servies par l'origine. + Conséquence assumée — les posters restent en JPEG là où de l'AVIF ferait 30 à 40 % de moins. + Les deux façons d'y remédier coûtent plus qu'elles ne rapportent à ce dimensionnement : rendre + `unoptimized` et ajouter `sharp` déplace le resize dans des pods à 2 vCPU (le CPU même qui + déclenche le HPA), et un loader `/cdn-cgi/image/` demande Cloudflare Image Resizing, payant. - **NEXT_PUBLIC_*** : injectés au **build** (inlinés dans le bundle client) ET présents au runtime (SSR). Les secrets serveur sont runtime-only. - **Probes** : `/api/health` (route `app/api/health/route.ts`). Un `startupProbe` couvre le démarrage (60 s max) ; readiness et liveness ne s'activent qu'ensuite, donc un boot lent sur un From 88c5103e323abbc9f8008506e104d684253c282f Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 14:43:22 +0200 Subject: [PATCH 89/98] fix(ci): infra.yml ne casse plus sur un cluster neuf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le namespace reelmark est créé par k8s/app.yaml, donc par le workflow applicatif. Sur un cluster vierge, lancer infra.yml en premier — ce que la doc recommandait pour que le second nœud existe avant le déploiement — faisait échouer l'étape « Apply application Ingress ». Elle sort maintenant proprement avec un message qui dit quoi faire. --- .github/workflows/infra.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/infra.yml b/.github/workflows/infra.yml index ef0a30bc..f64a3399 100644 --- a/.github/workflows/infra.yml +++ b/.github/workflows/infra.yml @@ -49,8 +49,15 @@ jobs: --values k8s/ingress-nginx-values.yaml \ --atomic --timeout 10m + # Le namespace reelmark est créé par k8s/app.yaml, donc par le workflow applicatif. + # Sur un cluster neuf ce job peut tourner en premier : ne pas échouer pour ça. - name: Apply application Ingress - run: kubectl apply --server-side --force-conflicts -f k8s/ingress.yaml + run: | + if ! kubectl get namespace reelmark >/dev/null 2>&1; then + echo "Namespace reelmark absent — relancer ce workflow après le premier déploiement applicatif." + exit 0 + fi + kubectl apply --server-side --force-conflicts -f k8s/ingress.yaml - name: Report edge state run: | From eb0f81d915c01c966de01b7d44f4d99a1e5419b2 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 14:43:22 +0200 Subject: [PATCH 90/98] =?UTF-8?q?refactor(k8s):=20retire=20les=20r=C3=A9gl?= =?UTF-8?q?ages=20de=20buffer=20devenus=20inop=C3=A9rants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proxy-buffers-number et proxy-busy-buffers-size ne sont plus lus depuis que le buffering est désactivé : nginx passe la réponse au client au fil de l'eau et ne garde qu'un seul buffer, dimensionné par proxy_buffer_size. Les laisser donnait à croire que la taille des buffers était réglée. proxy-buffer-size reste, avec la raison : c'est lui qui borne la lecture des en-têtes, et les cookies de session Supabase dépassent le défaut de 4k. Réf: nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering --- k8s/ingress.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml index 9593a537..0cede401 100644 --- a/k8s/ingress.yaml +++ b/k8s/ingress.yaml @@ -9,9 +9,9 @@ metadata: # la page arrive d'un bloc au lieu de peindre la bannière en premier. nginx.ingress.kubernetes.io/proxy-buffering: 'off' nginx.ingress.kubernetes.io/proxy-body-size: '10m' + # Reste actif sans buffering : nginx s'en sert pour lire la première partie de la + # réponse, donc les en-têtes — les cookies de session Supabase dépassent le défaut de 4k. nginx.ingress.kubernetes.io/proxy-buffer-size: '16k' - nginx.ingress.kubernetes.io/proxy-buffers-number: '8' - nginx.ingress.kubernetes.io/proxy-busy-buffers-size: '32k' nginx.ingress.kubernetes.io/affinity: 'cookie' nginx.ingress.kubernetes.io/affinity-mode: 'balanced' nginx.ingress.kubernetes.io/session-cookie-name: 'reelmark-affinity' From 2a152dba1975a32151a4fe8720c8ee30cca399b6 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 14:43:22 +0200 Subject: [PATCH 91/98] docs(deployment): verrouillage de l'origine et surveillance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - whitelist Cloudflare en deux étapes, avec le contrôle préalable qui évite de couper le site (le SNAT d'Octavia peut masquer l'IP source). Les plages sont lues depuis la source officielle au moment de la pose plutôt que recopiées dans le dépôt, où elles se périmeraient en silence - note sur la survie de l'annotation aux déploiements (Server-Side Apply ne touche pas aux champs d'un autre field manager) - section surveillance : pourquoi un Prometheus in-cluster est disproportionné à ce dimensionnement, et les commandes à passer après un incident - secret.example.yaml renvoyait à MIGRATION.md, qui n'existe pas --- DEPLOYMENT.md | 51 +++++++++++++++++++++++++++++++++-------- k8s/secret.example.yaml | 2 +- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 5a03b29d..142ab213 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -194,13 +194,7 @@ donc sans WAF, sans cache, et **sans le rate limiting**. Le cas le plus gênant `lib/rate-limiter.ts` lit `cf-connecting-ip` en premier, un header qu'un appelant direct forge librement — il peut donc à la fois usurper une IP et remettre son compteur à zéro à chaque requête. -Le correctif est de n'accepter que les plages Cloudflare ([liste officielle][cf-ips]) : - -```yaml -nginx.ingress.kubernetes.io/whitelist-source-range: '173.245.48.0/20,103.21.244.0/22,...' -``` - -**Vérifier d'abord que nginx voit la vraie IP source.** Derrière un LoadBalancer Octavia en +**Étape 1 — vérifier que nginx voit la vraie IP source.** Derrière un LoadBalancer Octavia en `externalTrafficPolicy: Cluster` (le défaut), le SNAT remplace l'IP du client par celle du nœud : la whitelist rejetterait alors tout le trafic, y compris légitime. @@ -208,9 +202,27 @@ la whitelist rejetterait alors tout le trafic, y compris légitime. kubectl -n ingress-nginx logs -l app.kubernetes.io/component=controller --tail=20 ``` -Si l'IP en début de ligne est une adresse Cloudflare, la whitelist est applicable telle quelle. -Si c'est une adresse en `172.21.x.x` (le subnet du cluster), il faut d'abord passer le Service en -`externalTrafficPolicy: Local` ou activer le PROXY protocol côté Octavia. +Si l'IP en début de ligne appartient à Cloudflare, passer à l'étape 2. Si c'est une adresse en +`172.21.x.x` (le subnet du cluster), il faut d'abord passer le Service en +`externalTrafficPolicy: Local` ou activer le PROXY protocol côté Octavia — sinon la whitelist +coupe le site. + +**Étape 2 — poser la whitelist**, plages lues depuis [la source officielle][cf-ips] plutôt que +recopiées dans le dépôt, où elles se périmeraient en silence : + +```bash +CF=$(curl -s https://www.cloudflare.com/ips-v4 https://www.cloudflare.com/ips-v6 | paste -sd, -) +kubectl -n reelmark annotate --overwrite ingress reelmark \ + nginx.ingress.kubernetes.io/whitelist-source-range="$CF" + +curl -I https://reelmark.silexio.be # doit répondre 200 +kubectl -n reelmark annotate ingress reelmark \ + nginx.ingress.kubernetes.io/whitelist-source-range- # retour arrière +``` + +L'annotation survit aux déploiements : `infra.yml` applique l'Ingress en `--server-side`, et +Server-Side Apply ne touche pas aux champs qu'un autre field manager possède. À rejouer quand +Cloudflare publie de nouvelles plages — quelques fois par an. [OPINION] L'alternative sans liste à maintenir est [Authenticated Origin Pulls][cf-mtls] : mTLS entre Cloudflare et l'origine, via les annotations `auth-tls-*` de l'Ingress. Plus robuste, plus @@ -219,6 +231,25 @@ long à câbler — à privilégier si les mises à jour de plages IP deviennent [cf-ips]: https://www.cloudflare.com/ips/ [cf-mtls]: https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/ +## 7 quater. Surveillance + +Bugsink couvre les erreurs applicatives, rien ne couvre l'infrastructure : un pod qui redémarre en +boucle, un HPA plafonné ou un nœud plein passent inaperçus jusqu'à ce qu'un visiteur le signale. + +[OPINION] Un `kube-prometheus-stack` in-cluster consommerait environ 1 Go de RAM, soit un quart de +la capacité utile de deux nœuds — disproportionné ici. Un contrôle externe sur `/api/health` +(Cloudflare Health Checks, ou tout service d'uptime) donne l'essentiel du bénéfice sans rien +prendre au cluster, et surveille la chaîne complète plutôt que le seul intérieur du cluster. + +Ce qu'un `kubectl` ne dira pas tout seul, à regarder après chaque incident : + +```bash +kubectl -n reelmark get pods -o wide # redémarrages, répartition sur les nœuds +kubectl -n reelmark describe hpa reelmark | tail -20 # décisions de scaling et leur raison +kubectl top nodes # marge réelle par nœud +kubectl get events -A --sort-by=.lastTimestamp | tail -30 # évictions, drains, OOMKill +``` + ## 8. CI/CD (GitHub Actions) `.github/workflows/deploy.yml` build l'image, la push sur ghcr.io, puis **applique `k8s/app.yaml`** diff --git a/k8s/secret.example.yaml b/k8s/secret.example.yaml index 02441121..78620545 100644 --- a/k8s/secret.example.yaml +++ b/k8s/secret.example.yaml @@ -1,5 +1,5 @@ # Example only — do NOT commit real values. -# Create the real secret out-of-band (see MIGRATION.md), e.g.: +# Create the real secret out-of-band (see DEPLOYMENT.md), e.g.: # kubectl -n reelmark create secret generic reelmark-secrets --from-env-file=.env.production apiVersion: v1 kind: Secret From 16bfe485fb44faa6f4d364e6768ed7f7c61b1958 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Tue, 28 Jul 2026 01:04:27 +0200 Subject: [PATCH 92/98] =?UTF-8?q?fix(routing):=20un=20chemin=20non=20r?= =?UTF-8?q?=C3=A9solu=20renvoie=20404=20au=20lieu=20de=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `app/[lang]/layout.tsx` fait office de layout racine : son `notFound()` n'avait aucune frontière parente et remontait jusqu'à `global-error`, soit un 500 pour chaque chemin qu'un crawler tente (`/config.json`, un fichier statique absent) - `app/not-found.tsx` lui donne une destination ; il porte son propre `` pour la même raison que `global-error` - la CI se déclenche désormais aussi sur `dev`, pour qu'une régression ne puisse plus atteindre `main` sans avoir été testée --- .github/workflows/ci.yml | 4 ++-- app/not-found.tsx | 41 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 app/not-found.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d3a2aa1..c344781d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main] + branches: [main, dev] pull_request: - branches: [main] + branches: [main, dev] concurrency: group: ci-${{ github.ref }} diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 00000000..84e98c20 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,41 @@ +import type { ReactElement } from 'react'; + +/** + * 404 rendered outside any locale segment. + * + * `app/[lang]/layout.tsx` is the root layout, so its `notFound()` for an unknown language had + * no parent boundary to land in and escalated to `global-error` — a 500 for every unmatched + * path a crawler tries (`/config.json`, a missing static file). This page gives it a home, and + * carries its own `` for the same reason `global-error` does. + */ +export default function NotFound(): ReactElement { + return ( + + +
+

+ 404 +

+

+ Cette page n’existe pas. +

+ + Retour à l’accueil + +
+ + + ); +} From cd88aecba80584e7d5f583ffe57a0bd8e20dc547 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 14:50:12 +0200 Subject: [PATCH 93/98] refactor(i18n): aligne la 404 racine sur les conventions du projet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le commit récupéré du worktree posait ses couleurs et son texte en dur (`#0a0a0a`, « Cette page n'existe pas »), alors que `global-error.tsx` — soumis exactement aux mêmes contraintes, puisqu'il porte lui aussi son propre `` hors de tout layout — montrait déjà comment faire : `import './globals.css'`, classes de tokens, `next/font`, et clés i18n. Les clés `notFoundTitle` et `notFoundDescription` existaient déjà. La détection de langue côté navigateur, dupliquée entre les deux écrans, part dans `lib/i18n/client-language.ts`. Elle reste côté client à dessein : lire les cookies au serveur ferait basculer en rendu dynamique une page que le build prérend en statique. --- app/global-error.tsx | 19 ++-------- app/not-found.tsx | 75 +++++++++++++++++++++++-------------- lib/i18n/client-language.ts | 23 ++++++++++++ 3 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 lib/i18n/client-language.ts diff --git a/app/global-error.tsx b/app/global-error.tsx index 868b9599..236e50ef 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -2,7 +2,8 @@ import './globals.css'; import * as Sentry from '@sentry/nextjs'; -import { translations, type Language } from '@/lib/i18n/translations'; +import { translations } from '@/lib/i18n/translations'; +import { detectClientLanguage } from '@/lib/i18n/client-language'; import { useEffect, useState } from 'react'; import { Inter, Bebas_Neue } from 'next/font/google'; @@ -30,21 +31,7 @@ export default function GlobalError({ Sentry.captureException(error); }, [error]); - const [lang] = useState(() => { - if (typeof document === 'undefined') return 'en'; - const match = document.cookie.match( - new RegExp('(^| )preferred-language=([^;]+)') - ); - if (match && (match[2] === 'fr' || match[2] === 'en')) { - return match[2] as Language; - } else if ( - typeof navigator !== 'undefined' && - navigator.language.startsWith('fr') - ) { - return 'fr'; - } - return 'en'; - }); + const [lang] = useState(detectClientLanguage); const t = translations[lang].common; diff --git a/app/not-found.tsx b/app/not-found.tsx index 84e98c20..6fd85c95 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,38 +1,57 @@ -import type { ReactElement } from 'react'; +'use client'; + +import './globals.css'; +import { useState } from 'react'; +import { Inter, Bebas_Neue } from 'next/font/google'; +import { translations } from '@/lib/i18n/translations'; +import { detectClientLanguage } from '@/lib/i18n/client-language'; + +const sans = Inter({ + subsets: ['latin'], + display: 'swap', + variable: '--font-sans', +}); + +const display = Bebas_Neue({ + weight: '400', + subsets: ['latin'], + display: 'swap', + variable: '--font-display', +}); /** - * 404 rendered outside any locale segment. + * 404 for paths outside any locale segment. * - * `app/[lang]/layout.tsx` is the root layout, so its `notFound()` for an unknown language had - * no parent boundary to land in and escalated to `global-error` — a 500 for every unmatched - * path a crawler tries (`/config.json`, a missing static file). This page gives it a home, and - * carries its own `` for the same reason `global-error` does. + * `app/[lang]/layout.tsx` is the root layout, so its `notFound()` had no parent boundary to land + * in and escalated to `global-error` — a 500 for every unmatched path a crawler tries + * (`/config.json`, a missing static file). This page gives it a destination, and carries its own + * `` for the same reason `global-error` does. */ -export default function NotFound(): ReactElement { +export default function NotFound() { + const [lang] = useState(detectClientLanguage); + const t = translations[lang].common; + return ( - + -
-

- 404 -

-

- Cette page n’existe pas. -

-
- Retour à l’accueil +
+

404

+
+

+ {t.notFoundTitle} +

+

+ {t.notFoundDescription} +

+
+ {/* eslint-disable-next-line @next/next/no-html-link-for-pages */} +
+ {t.errorBackHome}
diff --git a/lib/i18n/client-language.ts b/lib/i18n/client-language.ts new file mode 100644 index 00000000..e06b0f77 --- /dev/null +++ b/lib/i18n/client-language.ts @@ -0,0 +1,23 @@ +import { DEFAULT_LANGUAGE, isLanguage } from './config'; +import type { Language } from './translations'; + +/** + * Resolves the visitor's language from the browser alone, for the screens rendered outside the + * `[lang]` segment — `not-found` and `global-error` have no route param and no i18n provider, + * and reading cookies server-side there would opt the prerendered shell into dynamic rendering. + */ +export function detectClientLanguage(): Language { + if (typeof document === 'undefined') return DEFAULT_LANGUAGE; + + const cookie = document.cookie.match(/(?:^|; )preferred-language=([^;]+)/); + if (isLanguage(cookie?.[1])) return cookie[1]; + + if ( + typeof navigator !== 'undefined' && + navigator.language.startsWith('fr') + ) { + return 'fr'; + } + + return DEFAULT_LANGUAGE; +} From cde4c04c7bfca58af4041829665ddd40072aae94 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 14:50:42 +0200 Subject: [PATCH 94/98] docs(contributing): fixe le mode de merge par type de PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squasher `dev → main` écrit sur main un commit qui n'existe pas dans dev : git ne peut plus savoir que les deux historiques se sont rencontrés, chaque PR suivante reliste tout ce qui a déjà été livré, et l'écart se creuse à chaque release. C'est l'origine des « plus de commits que prévu » observés sur les PR. Le squash reste le bon outil pour une branche de feature, éphémère par nature. --- CONTRIBUTING.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 728522ee..f467bcbb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,27 @@ - Branch from `dev`; open pull requests against `main`. - One concern per PR. Keep diffs minimal and scoped — no opportunistic refactors. +### Merging + +| Pull request | Merge mode | +| --------------- | ------------------------- | +| `feature → dev` | **Squash and merge** | +| `dev → main` | **Create a merge commit** | + +Squashing a feature branch is fine — it is short-lived and thrown away afterwards. Squashing +`dev → main` is not: the squash writes a brand-new commit on `main` that does not exist in `dev`, +so git cannot tell the two histories ever met. Every later pull request then relists all the +commits already shipped, and the branches drift further apart with each release. + +Once `dev → main` uses a merge commit, resynchronising is a fast-forward — never a reset: + +```bash +git checkout dev && git pull # after the PR is merged +``` + +If the branches have already drifted, one merge commit is enough to reconcile them; there is +nothing to reset and nothing to force-push. + ## Commits [Conventional Commits](https://www.conventionalcommits.org/): `feat`, `fix`, `chore`, `refactor`, `docs`, `test`. From a56fb4f04b3d1084b3de997edb12a00b42d419c3 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 15:02:18 +0200 Subject: [PATCH 95/98] =?UTF-8?q?fix(deps):=20corrige=2011=20vuln=C3=A9rab?= =?UTF-8?q?ilit=C3=A9s,=20dont=20un=20contournement=20du=20guard=20d'auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit next 16.2.10 → 16.2.12, qui ferme huit CVE. Une compte plus que les autres ici : le contournement de middleware/proxy en App Router. `proxy.ts` porte le guard d'authentification du projet, donc un contournement du proxy est un contournement de l'auth sur les routes protégées. Les sept autres : SSRF dans les Server Actions, SSRF via les rewrites, déni de service en App Router, déni de service sur l'API d'optimisation d'images, confusion de cache sur les corps de réponse (×2), charge utile non bornée en runtime Edge, et divulgation non authentifiée des IDs de Server Actions. sharp est forcé en >=0.35.0 : Next réclame ^0.34.5, dont libvips traîne CVE-2026-33196. Le forçage est sans risque — `images.unoptimized` fait que sharp n'est jamais appelé, et ignoredBuiltDependencies l'empêche d'être compilé. brace-expansion n'a pas eu besoin d'override : le bump d'eslint-config-next tire minimatch@10, qui prend déjà une version saine. `pnpm audit` : 0 vulnérabilité. Validé : ESLint, tsc, 326 tests unitaires, build production, 49 tests E2E. --- package.json | 4 +- pnpm-lock.yaml | 16753 ++++++++++++++++++------------------------ pnpm-workspace.yaml | 4 + 3 files changed, 7294 insertions(+), 9467 deletions(-) diff --git a/package.json b/package.json index 74e2a84c..b91ad90d 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "fast-average-color": "^9.5.2", "fuse.js": "^7.5.0", "lucide-react": "^1.27.0", - "next": "16.2.10", + "next": "16.2.12", "nextjs-toploader": "^3.9.17", "react": "19.2.7", "react-dom": "19.2.7", @@ -53,7 +53,7 @@ "@vitest/coverage-v8": "^4.1.10", "dotenv": "^17.4.2", "eslint": "^9.39.5", - "eslint-config-next": "16.2.10", + "eslint-config-next": "16.2.12", "eslint-config-prettier": "^10.1.8", "lightningcss": "^1.33.0", "prettier": "^3.9.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d07ef99d..70b9c0f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9876 +1,7699 @@ lockfileVersion: '9.0' settings: - autoInstallPeers: true - excludeLinksFromLockfile: false + autoInstallPeers: true + excludeLinksFromLockfile: false overrides: - postcss: '>=8.5.10' - simple-icons: '>=16.27.1' - lucide-react: '>=1.27.0' + postcss: '>=8.5.10' + simple-icons: '>=16.27.1' + lucide-react: '>=1.27.0' + sharp: '>=0.35.0' importers: - .: - dependencies: - '@radix-ui/react-dialog': - specifier: ^1.1.23 - version: 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-dropdown-menu': - specifier: ^2.1.24 - version: 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-label': - specifier: ^2.1.15 - version: 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-popover': - specifier: ^1.1.23 - version: 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-separator': - specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': - specifier: ^1.3.3 - version: 1.3.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-switch': - specifier: ^1.3.7 - version: 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@sentry/nextjs': - specifier: ^10.68.0 - version: 10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) - '@serwist/next': - specifier: ^9.5.12 - version: 9.5.12(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) - '@supabase/ssr': - specifier: ^0.10.3 - version: 0.10.3(@supabase/supabase-js@2.110.8) - '@supabase/supabase-js': - specifier: ^2.110.8 - version: 2.110.8 - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 - fast-average-color: - specifier: ^9.5.2 - version: 9.5.2 - fuse.js: - specifier: ^7.5.0 - version: 7.5.0 - lucide-react: - specifier: '>=1.27.0' - version: 1.27.0(react@19.2.7) - next: - specifier: 16.2.10 - version: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - nextjs-toploader: - specifier: ^3.9.17 - version: 3.9.17(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: - specifier: 19.2.7 - version: 19.2.7 - react-dom: - specifier: 19.2.7 - version: 19.2.7(react@19.2.7) - server-only: - specifier: ^0.0.1 - version: 0.0.1 - serwist: - specifier: ^9.5.12 - version: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) - simple-icons: - specifier: '>=16.27.1' - version: 16.27.1 - sonner: - specifier: ^2.0.7 - version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - tailwind-merge: - specifier: ^3.6.0 - version: 3.6.0 - virtua: - specifier: ^0.49.3 - version: 0.49.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - web-push: - specifier: ^3.6.7 - version: 3.6.7(supports-color@8.1.1) - devDependencies: - '@playwright/test': - specifier: ^1.62.0 - version: 1.62.0 - '@tailwindcss/postcss': - specifier: ^4.3.3 - version: 4.3.3 - '@types/node': - specifier: ^25.9.5 - version: 25.9.5 - '@types/react': - specifier: ^19.2.17 - version: 19.2.17 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) - '@types/web-push': - specifier: ^3.6.4 - version: 3.6.4 - '@vitest/coverage-v8': - specifier: ^4.1.10 - version: 4.1.10(vitest@4.1.10) - dotenv: - specifier: ^17.4.2 - version: 17.4.2 - eslint: - specifier: ^9.39.5 - version: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - eslint-config-next: - specifier: 16.2.10 - version: 16.2.10(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - eslint-config-prettier: - specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) - lightningcss: - specifier: ^1.33.0 - version: 1.33.0 - prettier: - specifier: ^3.9.6 - version: 3.9.6 - tailwindcss: - specifier: ^4.3.3 - version: 4.3.3 - tw-animate-css: - specifier: ^1.4.0 - version: 1.4.0 - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vitest: - specifier: ^4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0)) + + .: + dependencies: + '@radix-ui/react-dialog': + specifier: ^1.1.23 + version: 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.24 + version: 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-label': + specifier: ^2.1.15 + version: 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popover': + specifier: ^1.1.23 + version: 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-separator': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': + specifier: ^1.3.3 + version: 1.3.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-switch': + specifier: ^1.3.7 + version: 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@sentry/nextjs': + specifier: ^10.68.0 + version: 10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(next@16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) + '@serwist/next': + specifier: ^9.5.12 + version: 9.5.12(next@16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) + '@supabase/ssr': + specifier: ^0.10.3 + version: 0.10.3(@supabase/supabase-js@2.110.8) + '@supabase/supabase-js': + specifier: ^2.110.8 + version: 2.110.8 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + fast-average-color: + specifier: ^9.5.2 + version: 9.5.2 + fuse.js: + specifier: ^7.5.0 + version: 7.5.0 + lucide-react: + specifier: '>=1.27.0' + version: 1.27.0(react@19.2.7) + next: + specifier: 16.2.12 + version: 16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + nextjs-toploader: + specifier: ^3.9.17 + version: 3.9.17(next@16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: + specifier: 19.2.7 + version: 19.2.7 + react-dom: + specifier: 19.2.7 + version: 19.2.7(react@19.2.7) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + serwist: + specifier: ^9.5.12 + version: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) + simple-icons: + specifier: '>=16.27.1' + version: 16.27.1 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 + virtua: + specifier: ^0.49.3 + version: 0.49.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + web-push: + specifier: ^3.6.7 + version: 3.6.7(supports-color@8.1.1) + devDependencies: + '@playwright/test': + specifier: ^1.62.0 + version: 1.62.0 + '@tailwindcss/postcss': + specifier: ^4.3.3 + version: 4.3.3 + '@types/node': + specifier: ^25.9.5 + version: 25.9.5 + '@types/react': + specifier: ^19.2.17 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@types/web-push': + specifier: ^3.6.4 + version: 3.6.4 + '@vitest/coverage-v8': + specifier: ^4.1.10 + version: 4.1.10(vitest@4.1.10) + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + eslint-config-next: + specifier: 16.2.12 + version: 16.2.12(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) + lightningcss: + specifier: ^1.33.0 + version: 1.33.0 + prettier: + specifier: ^3.9.6 + version: 3.9.6 + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0)) packages: - '@alloc/quick-lru@5.2.0': - resolution: - { - integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==, - } - engines: { node: '>=10' } - - '@apm-js-collab/code-transformer-bundler-plugins@0.7.1': - resolution: - { - integrity: sha512-Yidf5GOl60db80UxUtNdKK3pnY7obU/gs0xOfA0SCdnvVLMCvfYIer/egC3TqpPiT0Jg22eg3RlzcO+zKfPMcA==, - } - engines: { node: '>=18.0.0' } - - '@apm-js-collab/code-transformer@0.18.1': - resolution: - { - integrity: sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==, - } - hasBin: true - - '@apm-js-collab/tracing-hooks@0.13.0': - resolution: - { - integrity: sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==, - } - - '@babel/code-frame@7.29.7': - resolution: - { - integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==, - } - engines: { node: '>=6.9.0' } - - '@babel/compat-data@7.29.7': - resolution: - { - integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==, - } - engines: { node: '>=6.9.0' } - - '@babel/core@7.29.7': - resolution: - { - integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==, - } - engines: { node: '>=6.9.0' } - - '@babel/generator@7.29.7': - resolution: - { - integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==, - } - engines: { node: '>=6.9.0' } - - '@babel/helper-compilation-targets@7.29.7': - resolution: - { - integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==, - } - engines: { node: '>=6.9.0' } - - '@babel/helper-globals@7.29.7': - resolution: - { - integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==, - } - engines: { node: '>=6.9.0' } - - '@babel/helper-module-imports@7.29.7': - resolution: - { - integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==, - } - engines: { node: '>=6.9.0' } - - '@babel/helper-module-transforms@7.29.7': - resolution: - { - integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==, - } - engines: { node: '>=6.9.0' } - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-string-parser@7.29.7': - resolution: - { - integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==, - } - engines: { node: '>=6.9.0' } - - '@babel/helper-validator-identifier@7.29.7': - resolution: - { - integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==, - } - engines: { node: '>=6.9.0' } - - '@babel/helper-validator-option@7.29.7': - resolution: - { - integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==, - } - engines: { node: '>=6.9.0' } - - '@babel/helpers@7.29.7': - resolution: - { - integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==, - } - engines: { node: '>=6.9.0' } - - '@babel/parser@7.29.7': - resolution: - { - integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==, - } - engines: { node: '>=6.0.0' } - hasBin: true - - '@babel/template@7.29.7': - resolution: - { - integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==, - } - engines: { node: '>=6.9.0' } - - '@babel/traverse@7.29.7': - resolution: - { - integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==, - } - engines: { node: '>=6.9.0' } - - '@babel/types@7.29.7': - resolution: - { - integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==, - } - engines: { node: '>=6.9.0' } - - '@bcoe/v8-coverage@1.0.2': - resolution: - { - integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==, - } - engines: { node: '>=18' } - - '@emnapi/core@1.10.0': - resolution: - { - integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==, - } - - '@emnapi/core@1.11.1': - resolution: - { - integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==, - } - - '@emnapi/runtime@1.10.0': - resolution: - { - integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==, - } - - '@emnapi/runtime@1.11.1': - resolution: - { - integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==, - } - - '@emnapi/runtime@1.11.3': - resolution: - { - integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==, - } - - '@emnapi/wasi-threads@1.2.1': - resolution: - { - integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==, - } - - '@emnapi/wasi-threads@1.2.2': - resolution: - { - integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==, - } - - '@eslint-community/eslint-utils@4.10.1': - resolution: - { - integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: - { - integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, - } - engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } - - '@eslint/config-array@0.21.2': - resolution: - { - integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - '@eslint/config-helpers@0.4.2': - resolution: - { - integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - '@eslint/core@0.17.0': - resolution: - { - integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - '@eslint/eslintrc@3.3.6': - resolution: - { - integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - '@eslint/js@9.39.5': - resolution: - { - integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - '@eslint/object-schema@2.1.7': - resolution: - { - integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - '@eslint/plugin-kit@0.4.1': - resolution: - { - integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - '@floating-ui/core@1.8.0': - resolution: - { - integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==, - } - - '@floating-ui/dom@1.8.0': - resolution: - { - integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==, - } - - '@floating-ui/react-dom@2.1.9': - resolution: - { - integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==, - } - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@floating-ui/utils@0.2.12': - resolution: - { - integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==, - } - - '@humanfs/core@0.19.2': - resolution: - { - integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==, - } - engines: { node: '>=18.18.0' } - - '@humanfs/node@0.16.8': - resolution: - { - integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==, - } - engines: { node: '>=18.18.0' } - - '@humanfs/types@0.15.0': - resolution: - { - integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==, - } - engines: { node: '>=18.18.0' } - - '@humanwhocodes/module-importer@1.0.1': - resolution: - { - integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, - } - engines: { node: '>=12.22' } - - '@humanwhocodes/retry@0.4.3': - resolution: - { - integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, - } - engines: { node: '>=18.18' } - - '@img/colour@1.1.0': - resolution: - { - integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==, - } - engines: { node: '>=18' } - - '@img/sharp-darwin-arm64@0.34.5': - resolution: - { - integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: - { - integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: - { - integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==, - } - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: - { - integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==, - } - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: - { - integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==, - } - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: - { - integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==, - } - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: - { - integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==, - } - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: - { - integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==, - } - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: - { - integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==, - } - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: - { - integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==, - } - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: - { - integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==, - } - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: - { - integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==, - } - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-linux-arm64@0.34.5': - resolution: - { - integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-arm@0.34.5': - resolution: - { - integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: - { - integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: - { - integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-s390x@0.34.5': - resolution: - { - integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-x64@0.34.5': - resolution: - { - integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: - { - integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: - { - integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-wasm32@0.34.5': - resolution: - { - integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: - { - integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: - { - integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.5': - resolution: - { - integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - cpu: [x64] - os: [win32] - - '@jridgewell/gen-mapping@0.3.13': - resolution: - { - integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, - } - - '@jridgewell/remapping@2.3.5': - resolution: - { - integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, - } - - '@jridgewell/resolve-uri@3.1.2': - resolution: - { - integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, - } - engines: { node: '>=6.0.0' } - - '@jridgewell/source-map@0.3.11': - resolution: - { - integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==, - } - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: - { - integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, - } - - '@jridgewell/trace-mapping@0.3.31': - resolution: - { - integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, - } - - '@napi-rs/wasm-runtime@1.1.6': - resolution: - { - integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==, - } - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@next/env@16.2.10': - resolution: - { - integrity: sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==, - } - - '@next/eslint-plugin-next@16.2.10': - resolution: - { - integrity: sha512-Gs8D2m21VnJeFo9qvYIIqJH94frWerWYu41BprU1pLtRVF7PCQNLiFZZ3fG+iPuj3K83Cwv/rt+msLOy8Qgu3Q==, - } - - '@next/swc-darwin-arm64@16.2.10': - resolution: - { - integrity: sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==, - } - engines: { node: '>= 10' } - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-x64@16.2.10': - resolution: - { - integrity: sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==, - } - engines: { node: '>= 10' } - cpu: [x64] - os: [darwin] - - '@next/swc-linux-arm64-gnu@16.2.10': - resolution: - { - integrity: sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==, - } - engines: { node: '>= 10' } - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@next/swc-linux-arm64-musl@16.2.10': - resolution: - { - integrity: sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==, - } - engines: { node: '>= 10' } - cpu: [arm64] - os: [linux] - libc: [musl] - - '@next/swc-linux-x64-gnu@16.2.10': - resolution: - { - integrity: sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==, - } - engines: { node: '>= 10' } - cpu: [x64] - os: [linux] - libc: [glibc] - - '@next/swc-linux-x64-musl@16.2.10': - resolution: - { - integrity: sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==, - } - engines: { node: '>= 10' } - cpu: [x64] - os: [linux] - libc: [musl] - - '@next/swc-win32-arm64-msvc@16.2.10': - resolution: - { - integrity: sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==, - } - engines: { node: '>= 10' } - cpu: [arm64] - os: [win32] - - '@next/swc-win32-x64-msvc@16.2.10': - resolution: - { - integrity: sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==, - } - engines: { node: '>= 10' } - cpu: [x64] - os: [win32] - - '@nodelib/fs.scandir@2.1.5': - resolution: - { - integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, - } - engines: { node: '>= 8' } - - '@nodelib/fs.stat@2.0.5': - resolution: - { - integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, - } - engines: { node: '>= 8' } - - '@nodelib/fs.walk@1.2.8': - resolution: - { - integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, - } - engines: { node: '>= 8' } - - '@nolyfill/is-core-module@1.0.39': - resolution: - { - integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==, - } - engines: { node: '>=12.4.0' } - - '@opentelemetry/api-logs@0.220.0': - resolution: - { - integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==, - } - engines: { node: '>=8.0.0' } - - '@opentelemetry/api@1.9.1': - resolution: - { - integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==, - } - engines: { node: '>=8.0.0' } - - '@opentelemetry/core@2.10.0': - resolution: - { - integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==, - } - engines: { node: ^18.19.0 || >=20.6.0 } - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/instrumentation@0.220.0': - resolution: - { - integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==, - } - engines: { node: ^18.19.0 || >=20.6.0 } - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/resources@2.10.0': - resolution: - { - integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==, - } - engines: { node: ^18.19.0 || >=20.6.0 } - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - - '@opentelemetry/sdk-trace-base@2.10.0': - resolution: - { - integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==, - } - engines: { node: ^18.19.0 || >=20.6.0 } - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - - '@opentelemetry/sdk-trace@2.10.0': - resolution: - { - integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==, - } - engines: { node: ^18.19.0 || >=20.6.0 } - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - - '@opentelemetry/semantic-conventions@1.43.0': - resolution: - { - integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==, - } - engines: { node: '>=14' } - - '@oxc-project/types@0.139.0': - resolution: - { - integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==, - } - - '@playwright/test@1.62.0': - resolution: - { - integrity: sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==, - } - engines: { node: '>=20' } - hasBin: true - - '@radix-ui/primitive@1.1.7': - resolution: - { - integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==, - } - - '@radix-ui/react-arrow@1.1.15': - resolution: - { - integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-collection@1.1.15': - resolution: - { - integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-compose-refs@1.1.5': - resolution: - { - integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-context@1.2.2': - resolution: - { - integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dialog@1.1.23': - resolution: - { - integrity: sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-direction@1.1.4': - resolution: - { - integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dismissable-layer@1.1.19': - resolution: - { - integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-dropdown-menu@2.1.24': - resolution: - { - integrity: sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-focus-guards@1.1.6': - resolution: - { - integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-focus-scope@1.1.16': - resolution: - { - integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-id@1.1.4': - resolution: - { - integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-label@2.1.15': - resolution: - { - integrity: sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-menu@2.1.24': - resolution: - { - integrity: sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popover@1.1.23': - resolution: - { - integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popper@1.3.7': - resolution: - { - integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-portal@1.1.17': - resolution: - { - integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-presence@1.1.10': - resolution: - { - integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-primitive@2.1.10': - resolution: - { - integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-roving-focus@1.1.19': - resolution: - { - integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-separator@1.1.15': - resolution: - { - integrity: sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-slot@1.3.3': - resolution: - { - integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-switch@1.3.7': - resolution: - { - integrity: sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==, - } - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-use-callback-ref@1.1.4': - resolution: - { - integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-controllable-state@1.2.6': - resolution: - { - integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-effect-event@0.0.5': - resolution: - { - integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-is-hydrated@0.1.3': - resolution: - { - integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-layout-effect@1.1.4': - resolution: - { - integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-rect@1.1.4': - resolution: - { - integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-size@1.1.4': - resolution: - { - integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==, - } - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/rect@1.1.3': - resolution: - { - integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==, - } - - '@rolldown/binding-android-arm64@1.1.5': - resolution: - { - integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: - { - integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.1.5': - resolution: - { - integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: - { - integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: - { - integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: - { - integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: - { - integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: - { - integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: - { - integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: - { - integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: - { - integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: - { - integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: - { - integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: - { - integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: - { - integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: - { - integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==, - } - - '@rollup/plugin-commonjs@28.0.1': - resolution: - { - integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==, - } - engines: { node: '>=16.0.0 || 14 >= 14.17' } - peerDependencies: - rollup: ^2.68.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/pluginutils@5.4.0': - resolution: - { - integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==, - } - engines: { node: '>=14.0.0' } - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.62.2': - resolution: - { - integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==, - } - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.62.2': - resolution: - { - integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==, - } - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.62.2': - resolution: - { - integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==, - } - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.62.2': - resolution: - { - integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==, - } - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.62.2': - resolution: - { - integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==, - } - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.62.2': - resolution: - { - integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==, - } - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - resolution: - { - integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==, - } - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - resolution: - { - integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==, - } - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.62.2': - resolution: - { - integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==, - } - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.62.2': - resolution: - { - integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==, - } - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.62.2': - resolution: - { - integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==, - } - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.62.2': - resolution: - { - integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==, - } - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - resolution: - { - integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==, - } - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.62.2': - resolution: - { - integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==, - } - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - resolution: - { - integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==, - } - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.62.2': - resolution: - { - integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==, - } - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.62.2': - resolution: - { - integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==, - } - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.62.2': - resolution: - { - integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==, - } - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.62.2': - resolution: - { - integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==, - } - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.62.2': - resolution: - { - integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==, - } - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.62.2': - resolution: - { - integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==, - } - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.62.2': - resolution: - { - integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==, - } - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.62.2': - resolution: - { - integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==, - } - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.62.2': - resolution: - { - integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==, - } - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.62.2': - resolution: - { - integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==, - } - cpu: [x64] - os: [win32] - - '@rtsao/scc@1.1.0': - resolution: - { - integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==, - } - - '@sentry/babel-plugin-component-annotate@5.3.0': - resolution: - { - integrity: sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==, - } - engines: { node: '>= 18' } - - '@sentry/browser-utils@10.68.0': - resolution: - { - integrity: sha512-be8VtdjCngKc77cstJeV+gO15iH+blyXpBDk8yOehmtX4BkFO33mfTMNCWVR2LA0oOxjIHWRAhf77fIUEhzxPg==, - } - engines: { node: '>=18' } - - '@sentry/browser@10.68.0': - resolution: - { - integrity: sha512-8xVgk7oG2lajXnbXF6a7H1xMZ/U6icqSldHGzQu1+bajfrK8Gan9ULG/Xsj1VM1LlNeK6/7znDJ3u1jgvIwznw==, - } - engines: { node: '>=18' } - - '@sentry/bundler-plugin-core@5.3.0': - resolution: - { - integrity: sha512-L5T60sWdAI3qWwdg3Ptwek/0TY59PERrxyqp4XMUkroayQvGd9r5dIW9Q1kSeXX9iJ442nXbFZKAOyCKV4Z13Q==, - } - engines: { node: '>= 18' } - - '@sentry/bundler-plugins@10.68.0': - resolution: - { - integrity: sha512-XWv7asJuTTUSlacROvqcIFKuNxAf3PYL/mdjMBhBwp3rJ4vMkA73jqNPjqCTm726cHs/Y4yadbbFA/OZLPWzeg==, - } - engines: { node: '>= 18' } - peerDependencies: - rollup: '>=3.2.0' - webpack: '>=5.0.0' - peerDependenciesMeta: - rollup: - optional: true - webpack: - optional: true - - '@sentry/cli-darwin@2.58.6': - resolution: - { - integrity: sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA==, - } - engines: { node: '>=10' } - os: [darwin] - - '@sentry/cli-linux-arm64@2.58.6': - resolution: - { - integrity: sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g==, - } - engines: { node: '>=10' } - cpu: [arm64] - os: [linux, freebsd, android] - - '@sentry/cli-linux-arm@2.58.6': - resolution: - { - integrity: sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw==, - } - engines: { node: '>=10' } - cpu: [arm] - os: [linux, freebsd, android] - - '@sentry/cli-linux-i686@2.58.6': - resolution: - { - integrity: sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg==, - } - engines: { node: '>=10' } - cpu: [x86, ia32] - os: [linux, freebsd, android] - - '@sentry/cli-linux-x64@2.58.6': - resolution: - { - integrity: sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q==, - } - engines: { node: '>=10' } - cpu: [x64] - os: [linux, freebsd, android] - - '@sentry/cli-win32-arm64@2.58.6': - resolution: - { - integrity: sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A==, - } - engines: { node: '>=10' } - cpu: [arm64] - os: [win32] - - '@sentry/cli-win32-i686@2.58.6': - resolution: - { - integrity: sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg==, - } - engines: { node: '>=10' } - cpu: [x86, ia32] - os: [win32] - - '@sentry/cli-win32-x64@2.58.6': - resolution: - { - integrity: sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA==, - } - engines: { node: '>=10' } - cpu: [x64] - os: [win32] - - '@sentry/cli@2.58.6': - resolution: - { - integrity: sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg==, - } - engines: { node: '>= 10' } - hasBin: true - - '@sentry/conventions@0.16.0': - resolution: - { - integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==, - } - engines: { node: '>=14' } - - '@sentry/core@10.68.0': - resolution: - { - integrity: sha512-5Amhx8ltVz7vb1bRGyf3c4J69/iHW8R/H+SJxTRILHlsSOBrnVVc/IQEYDC6PTRdRdZ3x2u7RVjxZi2Mhe525g==, - } - engines: { node: '>=18' } - - '@sentry/feedback@10.68.0': - resolution: - { - integrity: sha512-XbdcXiBnpC3vgw46eHOPeD/ZQ+XzluP75ubdUcaPDW02hCh2nsdXiwjZ2DBImbpvIpTbJgjHf/sIlHWvcZJ2Mg==, - } - engines: { node: '>=18' } - - '@sentry/nextjs@10.68.0': - resolution: - { - integrity: sha512-oMDl9F8jkCgAsd8U9gLTAWr7u/j9/nUw0sBRHoAvK7HXfdrg9fPlsDd3wsHgYDC0U2tIDTXICXAwT2zJ7OHHWg==, - } - engines: { node: '>=18' } - peerDependencies: - next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0 - - '@sentry/node-core@10.68.0': - resolution: - { - integrity: sha512-VreORXnruy8A2SyprZKENAq3ArGwn35KPewLQSQ5dgDXSFtUj2scLuzZoVsZ/Uyt83td0WPvD6Lv0X7MvSYAMQ==, - } - engines: { node: '>=18' } - peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@opentelemetry/core': ^1.30.1 || ^2.1.0 - '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' - '@opentelemetry/instrumentation': '>=0.57.1 <1' - '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@opentelemetry/core': - optional: true - '@opentelemetry/exporter-trace-otlp-http': - optional: true - '@opentelemetry/instrumentation': - optional: true - '@opentelemetry/sdk-trace-base': - optional: true - - '@sentry/node@10.68.0': - resolution: - { - integrity: sha512-bnvRzEehquG/894DD3BWNCBUbDWsLQfYPU+SNCMd6G4Ext75RthVkRs8R0sRaE6b8Tw9HSEgySHL834Tf8lVsA==, - } - engines: { node: '>=18' } - - '@sentry/opentelemetry@10.68.0': - resolution: - { - integrity: sha512-JDNH9dacX0MSi7FRvabPCJUAXRMTe16bE/Gajc/7Gfcw7Rwvo4DZ0nd162PCSAW9QoEjUT12upDehEHJuFmsXg==, - } - engines: { node: '>=18' } - peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@opentelemetry/core': ^1.30.1 || ^2.1.0 - '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - - '@sentry/react@10.68.0': - resolution: - { - integrity: sha512-rIq4QR4ScMHHx9JJZv7Jgw31bMdUVJMx+ykHIJb7htjY6mj78sjKs+KpCsMDnvJxDhSmvftGM1KfKD4BggL7OQ==, - } - engines: { node: '>=18' } - peerDependencies: - react: ^16.14.0 || 17.x || 18.x || 19.x - - '@sentry/replay-canvas@10.68.0': - resolution: - { - integrity: sha512-HusYcr+He+ohnUDHunYrc5St6vdDnBXpUAndnT5ReyUMVSCiWKfY3paXowU/0787HwYfxdcpZgwC5u79+XbEIg==, - } - engines: { node: '>=18' } - - '@sentry/replay@10.68.0': - resolution: - { - integrity: sha512-ZoG2n16vbkx4GWSCnLIqUUN9xlUmccQFbQ2US2rhruQeHTUnHl/ukr8NHOQXZaEbwKyMkX9bEMwfmZHJm+wSTQ==, - } - engines: { node: '>=18' } - - '@sentry/server-utils@10.68.0': - resolution: - { - integrity: sha512-lp1ZSs1auw7HrCESSYt/n4dOUaKPVUIAKyVYRk6xVr4bMIN3RPub/H5Wm7QPj9CpXVC5bQFvB6+dHZXz809oMg==, - } - engines: { node: '>=18' } - - '@sentry/vercel-edge@10.68.0': - resolution: - { - integrity: sha512-6FukkfH1b3T7jMgPVE7todrKcC92zA0gwUA3kyTG645eOdfMTIQ9o5lr5UECixALmzTB0GAgrr2gH9xlR0R9bA==, - } - engines: { node: '>=18' } - - '@sentry/webpack-plugin@5.4.0': - resolution: - { - integrity: sha512-J3a0BvUZ75Qxy+v/Ap3Hx4ZEcSjlPHZ/jDtxdRhXQCyNeEb8xq0uUBTI9VLtGk2eNeNucOxOEJ5ngqdNjnEH/A==, - } - engines: { node: '>= 18' } - peerDependencies: - webpack: '>=5.0.0' - - '@serwist/build@9.5.12': - resolution: - { - integrity: sha512-U2UkA9BjdpniZkXDIG6NQRBEbXccRvqewzL3hfNEQERvM2bL6ezKvsVF9359akUWP8BxV950Kkq9LKGFmOR8Uw==, - } - engines: { node: '>=18.0.0' } - peerDependencies: - typescript: '>=5.0.0' - peerDependenciesMeta: - typescript: - optional: true - - '@serwist/next@9.5.12': - resolution: - { - integrity: sha512-aKSDZmxC2w6mvLC1aOcO4OnZqdKIl4zrW2VndlZT12+m+nIyOWih1xqqw4LsvUGfQCx0AswtKUUQ7woRYZ2thg==, - } - engines: { node: '>=18.0.0' } - peerDependencies: - '@serwist/cli': ^9.5.12 - next: '>=14.0.0' - react: '>=18.0.0' - typescript: '>=5.0.0' - peerDependenciesMeta: - '@serwist/cli': - optional: true - typescript: - optional: true - - '@serwist/utils@9.5.12': - resolution: - { - integrity: sha512-BHDwiGL7H7JS7wFvVlAWTFHGt0ffHuPgKtdkaYP0lEWJFl6fKJRCnTDZhwhusFvBhicf37XP8Y6PouTZbcLmgg==, - } - peerDependencies: - browserslist: '>=4' - peerDependenciesMeta: - browserslist: - optional: true - - '@serwist/webpack-plugin@9.5.12': - resolution: - { - integrity: sha512-+a5Fap7wVR4oN5jg1GxVNbWW+xUv3LO7UWYfg2D8PkJZiTCcX8PwkcmdzMGVr39dyOtIMTSAANw0af1zMjVbOA==, - } - engines: { node: '>=18.0.0' } - peerDependencies: - typescript: '>=5.0.0' - webpack: 4.4.0 || ^5.9.0 - peerDependenciesMeta: - typescript: - optional: true - webpack: - optional: true - - '@serwist/window@9.5.12': - resolution: - { - integrity: sha512-+fjApJme34qfwdGE2kLT8sFx+xWO+a15477MN/B/b1v4HOWBmARuA9C2xGFBCT77XzCrzQ/2DxcDePZsqEYOlw==, - } - peerDependencies: - typescript: '>=5.0.0' - peerDependenciesMeta: - typescript: - optional: true - - '@standard-schema/spec@1.1.0': - resolution: - { - integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, - } - - '@supabase/auth-js@2.110.8': - resolution: - { - integrity: sha512-TQ5neTUDX2C2WmyYa03yGhLMkhdE/SkHXtK8/qxO/APUy3rsymsJCBP48p4jcN6iO2G0ow6RRexQd2mX+dSyJg==, - } - engines: { node: '>=22.0.0' } - - '@supabase/functions-js@2.110.8': - resolution: - { - integrity: sha512-5yB9TLYzvv2oSQxwb0gamEvIAsuH66pVt7AM/pz03S7wN6ehD34GNgbShrccetqPedXQSz7e/1hAJ9NeEhoZVg==, - } - engines: { node: '>=22.0.0' } - - '@supabase/phoenix@0.4.5': - resolution: - { - integrity: sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==, - } - - '@supabase/postgrest-js@2.110.8': - resolution: - { - integrity: sha512-QeRROxl1PpOZw5Jzi7BwdN9icsycMrLlCCvsjS0hYLW+nZoaT46zdagz/glJirj8jHF4jSd5Jyipuae2cBClCw==, - } - engines: { node: '>=22.0.0' } - - '@supabase/realtime-js@2.110.8': - resolution: - { - integrity: sha512-mwX7ituX6O31fLf+0g65rpLlNxqgnMaPltPsQwzox6jfmbfVl3tCxXrfr3HEsQcCRjpjuJG1+A0vFzP1yVjKHA==, - } - engines: { node: '>=22.0.0' } - - '@supabase/ssr@0.10.3': - resolution: - { - integrity: sha512-ux2CJgX89h0Fz2lY7ZNafNG2SkXpyRc5dz77K9eKeBLPdtywQixKwIuetDeIViAJBp/buOUVmgj8PVesOklNpw==, - } - peerDependencies: - '@supabase/supabase-js': ^2.105.3 - - '@supabase/storage-js@2.110.8': - resolution: - { - integrity: sha512-CcfhkZFBLxsthgUabZKxwfsoXdrikIGsL3LsGoV3FZTqCMx/s1y49taT4jT/oya5+1IuB0sFFHw6pF0o0iJniQ==, - } - engines: { node: '>=22.0.0' } - - '@supabase/supabase-js@2.110.8': - resolution: - { - integrity: sha512-E5qzoe74zhJRv4wRcbO9eMYzeQDb/+h6c603pL8shcxLGBjTKsIF7XXj05IcNj23TLDgJN1WkMw7mwAPyu5dZg==, - } - engines: { node: '>=22.0.0' } - - '@swc/helpers@0.5.15': - resolution: - { - integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==, - } - - '@tailwindcss/node@4.3.3': - resolution: - { - integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==, - } - - '@tailwindcss/oxide-android-arm64@4.3.3': - resolution: - { - integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==, - } - engines: { node: '>= 20' } - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.3.3': - resolution: - { - integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==, - } - engines: { node: '>= 20' } - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.3.3': - resolution: - { - integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==, - } - engines: { node: '>= 20' } - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.3.3': - resolution: - { - integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==, - } - engines: { node: '>= 20' } - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': - resolution: - { - integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==, - } - engines: { node: '>= 20' } - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': - resolution: - { - integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==, - } - engines: { node: '>= 20' } - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-arm64-musl@4.3.3': - resolution: - { - integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==, - } - engines: { node: '>= 20' } - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-linux-x64-gnu@4.3.3': - resolution: - { - integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==, - } - engines: { node: '>= 20' } - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-x64-musl@4.3.3': - resolution: - { - integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==, - } - engines: { node: '>= 20' } - cpu: [x64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-wasm32-wasi@4.3.3': - resolution: - { - integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==, - } - engines: { node: '>=14.0.0' } - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': - resolution: - { - integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==, - } - engines: { node: '>= 20' } - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.3.3': - resolution: - { - integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==, - } - engines: { node: '>= 20' } - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.3.3': - resolution: - { - integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==, - } - engines: { node: '>= 20' } - - '@tailwindcss/postcss@4.3.3': - resolution: - { - integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==, - } - - '@tybys/wasm-util@0.10.3': - resolution: - { - integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==, - } - - '@types/chai@5.2.3': - resolution: - { - integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==, - } - - '@types/deep-eql@4.0.2': - resolution: - { - integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==, - } - - '@types/estree@1.0.9': - resolution: - { - integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==, - } - - '@types/json-schema@7.0.15': - resolution: - { - integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, - } - - '@types/json5@0.0.29': - resolution: - { - integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==, - } - - '@types/node@25.9.5': - resolution: - { - integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==, - } - - '@types/react-dom@19.2.3': - resolution: - { - integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==, - } - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.17': - resolution: - { - integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==, - } - - '@types/trusted-types@2.0.7': - resolution: - { - integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==, - } - - '@types/web-push@3.6.4': - resolution: - { - integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==, - } - - '@typescript-eslint/eslint-plugin@8.65.0': - resolution: - { - integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - '@typescript-eslint/parser': ^8.65.0 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/parser@8.65.0': - resolution: - { - integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/project-service@8.65.0': - resolution: - { - integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/scope-manager@8.65.0': - resolution: - { - integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - '@typescript-eslint/tsconfig-utils@8.65.0': - resolution: - { - integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/type-utils@8.65.0': - resolution: - { - integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/types@8.65.0': - resolution: - { - integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - '@typescript-eslint/typescript-estree@8.65.0': - resolution: - { - integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/utils@8.65.0': - resolution: - { - integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/visitor-keys@8.65.0': - resolution: - { - integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - '@unrs/resolver-binding-android-arm-eabi@1.12.2': - resolution: - { - integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==, - } - cpu: [arm] - os: [android] - - '@unrs/resolver-binding-android-arm64@1.12.2': - resolution: - { - integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==, - } - cpu: [arm64] - os: [android] - - '@unrs/resolver-binding-darwin-arm64@1.12.2': - resolution: - { - integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==, - } - cpu: [arm64] - os: [darwin] - - '@unrs/resolver-binding-darwin-x64@1.12.2': - resolution: - { - integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==, - } - cpu: [x64] - os: [darwin] - - '@unrs/resolver-binding-freebsd-x64@1.12.2': - resolution: - { - integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==, - } - cpu: [x64] - os: [freebsd] - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': - resolution: - { - integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==, - } - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': - resolution: - { - integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==, - } - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': - resolution: - { - integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==, - } - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-arm64-musl@1.12.2': - resolution: - { - integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==, - } - cpu: [arm64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': - resolution: - { - integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==, - } - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-loong64-musl@1.12.2': - resolution: - { - integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==, - } - cpu: [loong64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': - resolution: - { - integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==, - } - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': - resolution: - { - integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==, - } - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': - resolution: - { - integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==, - } - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': - resolution: - { - integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==, - } - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-x64-gnu@1.12.2': - resolution: - { - integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==, - } - cpu: [x64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-x64-musl@1.12.2': - resolution: - { - integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==, - } - cpu: [x64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-openharmony-arm64@1.12.2': - resolution: - { - integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==, - } - cpu: [arm64] - os: [openharmony] - - '@unrs/resolver-binding-wasm32-wasi@1.12.2': - resolution: - { - integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==, - } - engines: { node: '>=14.0.0' } - cpu: [wasm32] - - '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': - resolution: - { - integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==, - } - cpu: [arm64] - os: [win32] - - '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': - resolution: - { - integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==, - } - cpu: [ia32] - os: [win32] - - '@unrs/resolver-binding-win32-x64-msvc@1.12.2': - resolution: - { - integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==, - } - cpu: [x64] - os: [win32] - - '@vitest/coverage-v8@4.1.10': - resolution: - { - integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==, - } - peerDependencies: - '@vitest/browser': 4.1.10 - vitest: 4.1.10 - peerDependenciesMeta: - '@vitest/browser': - optional: true - - '@vitest/expect@4.1.10': - resolution: - { - integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==, - } - - '@vitest/mocker@4.1.10': - resolution: - { - integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==, - } - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.1.10': - resolution: - { - integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==, - } - - '@vitest/runner@4.1.10': - resolution: - { - integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==, - } - - '@vitest/snapshot@4.1.10': - resolution: - { - integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==, - } - - '@vitest/spy@4.1.10': - resolution: - { - integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==, - } - - '@vitest/utils@4.1.10': - resolution: - { - integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==, - } - - '@webassemblyjs/ast@1.14.1': - resolution: - { - integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==, - } - - '@webassemblyjs/floating-point-hex-parser@1.13.2': - resolution: - { - integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==, - } - - '@webassemblyjs/helper-api-error@1.13.2': - resolution: - { - integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==, - } - - '@webassemblyjs/helper-buffer@1.14.1': - resolution: - { - integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==, - } - - '@webassemblyjs/helper-numbers@1.13.2': - resolution: - { - integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==, - } - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': - resolution: - { - integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==, - } - - '@webassemblyjs/helper-wasm-section@1.14.1': - resolution: - { - integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==, - } - - '@webassemblyjs/ieee754@1.13.2': - resolution: - { - integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==, - } - - '@webassemblyjs/leb128@1.13.2': - resolution: - { - integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==, - } - - '@webassemblyjs/utf8@1.13.2': - resolution: - { - integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==, - } - - '@webassemblyjs/wasm-edit@1.14.1': - resolution: - { - integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==, - } - - '@webassemblyjs/wasm-gen@1.14.1': - resolution: - { - integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==, - } - - '@webassemblyjs/wasm-opt@1.14.1': - resolution: - { - integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==, - } - - '@webassemblyjs/wasm-parser@1.14.1': - resolution: - { - integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==, - } - - '@webassemblyjs/wast-printer@1.14.1': - resolution: - { - integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==, - } - - '@xtuc/ieee754@1.2.0': - resolution: - { - integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==, - } - - '@xtuc/long@4.2.2': - resolution: - { - integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==, - } - - acorn-jsx@5.3.2: - resolution: - { - integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, - } - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.17.0: - resolution: - { - integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==, - } - engines: { node: '>=0.4.0' } - hasBin: true - - agent-base@6.0.2: - resolution: - { - integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==, - } - engines: { node: '>= 6.0.0' } - - agent-base@7.1.4: - resolution: - { - integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==, - } - engines: { node: '>= 14' } - - ajv-formats@2.1.1: - resolution: - { - integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==, - } - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv-keywords@5.1.0: - resolution: - { - integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==, - } - peerDependencies: - ajv: ^8.8.2 - - ajv@6.15.0: - resolution: - { - integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==, - } - - ajv@8.20.0: - resolution: - { - integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==, - } - - ansi-styles@4.3.0: - resolution: - { - integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, - } - engines: { node: '>=8' } - - argparse@2.0.1: - resolution: - { - integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, - } - - aria-hidden@1.2.6: - resolution: - { - integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==, - } - engines: { node: '>=10' } - - aria-query@5.3.2: - resolution: - { - integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==, - } - engines: { node: '>= 0.4' } - - array-buffer-byte-length@1.0.2: - resolution: - { - integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==, - } - engines: { node: '>= 0.4' } - - array-includes@3.1.9: - resolution: - { - integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==, - } - engines: { node: '>= 0.4' } - - array.prototype.findlast@1.2.5: - resolution: - { - integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==, - } - engines: { node: '>= 0.4' } - - array.prototype.findlastindex@1.2.6: - resolution: - { - integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==, - } - engines: { node: '>= 0.4' } - - array.prototype.flat@1.3.3: - resolution: - { - integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==, - } - engines: { node: '>= 0.4' } - - array.prototype.flatmap@1.3.3: - resolution: - { - integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==, - } - engines: { node: '>= 0.4' } - - array.prototype.tosorted@1.1.4: - resolution: - { - integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==, - } - engines: { node: '>= 0.4' } - - arraybuffer.prototype.slice@1.0.4: - resolution: - { - integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==, - } - engines: { node: '>= 0.4' } - - asn1.js@5.4.1: - resolution: - { - integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==, - } - - assertion-error@2.0.1: - resolution: - { - integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, - } - engines: { node: '>=12' } - - ast-types-flow@0.0.8: - resolution: - { - integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==, - } - - ast-v8-to-istanbul@1.0.5: - resolution: - { - integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==, - } - - astring@1.9.0: - resolution: - { - integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==, - } - hasBin: true - - async-function@1.0.0: - resolution: - { - integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==, - } - engines: { node: '>= 0.4' } - - available-typed-arrays@1.0.7: - resolution: - { - integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==, - } - engines: { node: '>= 0.4' } - - axe-core@4.12.1: - resolution: - { - integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==, - } - engines: { node: '>=4' } - - axobject-query@4.1.0: - resolution: - { - integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==, - } - engines: { node: '>= 0.4' } - - balanced-match@1.0.2: - resolution: - { - integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, - } - - balanced-match@4.0.4: - resolution: - { - integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, - } - engines: { node: 18 || 20 || >=22 } - - baseline-browser-mapping@2.11.1: - resolution: - { - integrity: sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==, - } - engines: { node: '>=6.0.0' } - hasBin: true - - bn.js@4.12.5: - resolution: - { - integrity: sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==, - } - - brace-expansion@1.1.16: - resolution: - { - integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==, - } - - brace-expansion@5.0.8: - resolution: - { - integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==, - } - engines: { node: 20 || >=22 } - - braces@3.0.3: - resolution: - { - integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, - } - engines: { node: '>=8' } - - browserslist@4.28.6: - resolution: - { - integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==, - } - engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } - hasBin: true - - browserslist@4.28.7: - resolution: - { - integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==, - } - engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } - hasBin: true - - buffer-equal-constant-time@1.0.1: - resolution: - { - integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==, - } - - buffer-from@1.1.2: - resolution: - { - integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, - } - - call-bind-apply-helpers@1.0.2: - resolution: - { - integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, - } - engines: { node: '>= 0.4' } - - call-bind@1.0.9: - resolution: - { - integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==, - } - engines: { node: '>= 0.4' } - - call-bound@1.0.4: - resolution: - { - integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, - } - engines: { node: '>= 0.4' } - - callsites@3.1.0: - resolution: - { - integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, - } - engines: { node: '>=6' } - - caniuse-lite@1.0.30001806: - resolution: - { - integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==, - } - - chai@6.2.2: - resolution: - { - integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==, - } - engines: { node: '>=18' } - - chalk@4.1.2: - resolution: - { - integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, - } - engines: { node: '>=10' } - - chrome-trace-event@1.0.4: - resolution: - { - integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==, - } - engines: { node: '>=6.0' } - - cjs-module-lexer@2.2.0: - resolution: - { - integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==, - } - - class-variance-authority@0.7.1: - resolution: - { - integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==, - } - - client-only@0.0.1: - resolution: - { - integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==, - } - - clsx@2.1.1: - resolution: - { - integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==, - } - engines: { node: '>=6' } - - color-convert@2.0.1: - resolution: - { - integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, - } - engines: { node: '>=7.0.0' } - - color-name@1.1.4: - resolution: - { - integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, - } - - commander@2.20.3: - resolution: - { - integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==, - } - - common-tags@1.8.2: - resolution: - { - integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==, - } - engines: { node: '>=4.0.0' } - - commondir@1.0.1: - resolution: - { - integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==, - } - - concat-map@0.0.1: - resolution: - { - integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, - } - - convert-source-map@2.0.0: - resolution: - { - integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, - } - - cookie@1.1.1: - resolution: - { - integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==, - } - engines: { node: '>=18' } - - cross-spawn@7.0.6: - resolution: - { - integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, - } - engines: { node: '>= 8' } - - csstype@3.2.3: - resolution: - { - integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, - } - - damerau-levenshtein@1.0.8: - resolution: - { - integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==, - } - - data-view-buffer@1.0.2: - resolution: - { - integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==, - } - engines: { node: '>= 0.4' } - - data-view-byte-length@1.0.2: - resolution: - { - integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==, - } - engines: { node: '>= 0.4' } - - data-view-byte-offset@1.0.1: - resolution: - { - integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==, - } - engines: { node: '>= 0.4' } - - debug@3.2.7: - resolution: - { - integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==, - } - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.3: - resolution: - { - integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, - } - engines: { node: '>=6.0' } - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: - { - integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, - } - - define-data-property@1.1.4: - resolution: - { - integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==, - } - engines: { node: '>= 0.4' } - - define-properties@1.2.1: - resolution: - { - integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==, - } - engines: { node: '>= 0.4' } - - detect-libc@2.1.2: - resolution: - { - integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, - } - engines: { node: '>=8' } - - detect-node-es@1.1.0: - resolution: - { - integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==, - } - - doctrine@2.1.0: - resolution: - { - integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==, - } - engines: { node: '>=0.10.0' } - - dotenv@16.6.1: - resolution: - { - integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==, - } - engines: { node: '>=12' } - - dotenv@17.4.2: - resolution: - { - integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==, - } - engines: { node: '>=12' } - - dunder-proto@1.0.1: - resolution: - { - integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, - } - engines: { node: '>= 0.4' } - - ecdsa-sig-formatter@1.0.11: - resolution: - { - integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==, - } - - electron-to-chromium@1.5.396: - resolution: - { - integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==, - } - - emoji-regex@9.2.2: - resolution: - { - integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, - } - - enhanced-resolve@5.24.3: - resolution: - { - integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==, - } - engines: { node: '>=10.13.0' } - - es-abstract-get@1.0.0: - resolution: - { - integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==, - } - engines: { node: '>= 0.4' } - - es-abstract@1.24.2: - resolution: - { - integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==, - } - engines: { node: '>= 0.4' } - - es-define-property@1.0.1: - resolution: - { - integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, - } - engines: { node: '>= 0.4' } - - es-errors@1.3.0: - resolution: - { - integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, - } - engines: { node: '>= 0.4' } - - es-iterator-helpers@1.4.0: - resolution: - { - integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==, - } - engines: { node: '>= 0.4' } - - es-module-lexer@2.3.1: - resolution: - { - integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==, - } - - es-object-atoms@1.1.2: - resolution: - { - integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==, - } - engines: { node: '>= 0.4' } - - es-set-tostringtag@2.1.0: - resolution: - { - integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==, - } - engines: { node: '>= 0.4' } - - es-shim-unscopables@1.1.0: - resolution: - { - integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==, - } - engines: { node: '>= 0.4' } - - es-to-primitive@1.3.4: - resolution: - { - integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==, - } - engines: { node: '>= 0.4' } - - escalade@3.2.0: - resolution: - { - integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, - } - engines: { node: '>=6' } - - escape-string-regexp@4.0.0: - resolution: - { - integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, - } - engines: { node: '>=10' } - - eslint-config-next@16.2.10: - resolution: - { - integrity: sha512-HSybLOY0QKf39i4FWUqPN0xWiNDi6A6UqJmZtgDkS3zMqjXTqULvj/sueXx3cdCG0mVG+qH6k5/qdegklH1d1w==, - } - peerDependencies: - eslint: '>=9.0.0' - typescript: '>=3.3.1' - peerDependenciesMeta: - typescript: - optional: true - - eslint-config-prettier@10.1.8: - resolution: - { - integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==, - } - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-import-resolver-node@0.3.10: - resolution: - { - integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==, - } - - eslint-import-resolver-typescript@3.10.1: - resolution: - { - integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==, - } - engines: { node: ^14.18.0 || >=16.0.0 } - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - eslint-plugin-import-x: '*' - peerDependenciesMeta: - eslint-plugin-import: - optional: true - eslint-plugin-import-x: - optional: true - - eslint-module-utils@2.14.0: - resolution: - { - integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==, - } - engines: { node: '>=4' } - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-import@2.32.0: - resolution: - { - integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==, - } - engines: { node: '>=4' } - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - - eslint-plugin-jsx-a11y@6.10.2: - resolution: - { - integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==, - } - engines: { node: '>=4.0' } - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - - eslint-plugin-react-hooks@7.1.1: - resolution: - { - integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==, - } - engines: { node: '>=18' } - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 - - eslint-plugin-react@7.37.5: - resolution: - { - integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==, - } - engines: { node: '>=4' } - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - - eslint-scope@5.1.1: - resolution: - { - integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==, - } - engines: { node: '>=8.0.0' } - - eslint-scope@8.4.0: - resolution: - { - integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - eslint-visitor-keys@3.4.3: - resolution: - { - integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } - - eslint-visitor-keys@4.2.1: - resolution: - { - integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - eslint-visitor-keys@5.0.1: - resolution: - { - integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - - eslint@9.39.5: - resolution: - { - integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.4.0: - resolution: - { - integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - esquery@1.7.0: - resolution: - { - integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==, - } - engines: { node: '>=0.10' } - - esrecurse@4.3.0: - resolution: - { - integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, - } - engines: { node: '>=4.0' } - - estraverse@4.3.0: - resolution: - { - integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==, - } - engines: { node: '>=4.0' } - - estraverse@5.3.0: - resolution: - { - integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, - } - engines: { node: '>=4.0' } - - estree-walker@2.0.2: - resolution: - { - integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, - } - - estree-walker@3.0.3: - resolution: - { - integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, - } - - esutils@2.0.3: - resolution: - { - integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, - } - engines: { node: '>=0.10.0' } - - events@3.3.0: - resolution: - { - integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==, - } - engines: { node: '>=0.8.x' } - - expect-type@1.4.0: - resolution: - { - integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==, - } - engines: { node: '>=12.0.0' } - - fast-average-color@9.5.2: - resolution: - { - integrity: sha512-FbaU8iPTPljP7tmnVhXbCyASNw/zxnmaNDf88gn5pTXlNvejl9w4FapeWMh6UNDwIjhJJU28EPfQWwW032YgPA==, - } - engines: { node: '>= 12' } - - fast-deep-equal@3.1.3: - resolution: - { - integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, - } - - fast-glob@3.3.1: - resolution: - { - integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==, - } - engines: { node: '>=8.6.0' } - - fast-json-stable-stringify@2.1.0: - resolution: - { - integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, - } - - fast-levenshtein@2.0.6: - resolution: - { - integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, - } - - fast-uri@3.1.4: - resolution: - { - integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==, - } - - fastq@1.20.1: - resolution: - { - integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==, - } - - fdir@6.5.0: - resolution: - { - integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, - } - engines: { node: '>=12.0.0' } - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: - { - integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, - } - engines: { node: '>=16.0.0' } - - fill-range@7.1.1: - resolution: - { - integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, - } - engines: { node: '>=8' } - - find-up@5.0.0: - resolution: - { - integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, - } - engines: { node: '>=10' } - - flat-cache@4.0.1: - resolution: - { - integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, - } - engines: { node: '>=16' } - - flatted@3.4.3: - resolution: - { - integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==, - } - - for-each@0.3.5: - resolution: - { - integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==, - } - engines: { node: '>= 0.4' } - - fsevents@2.3.2: - resolution: - { - integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } - os: [darwin] - - fsevents@2.3.3: - resolution: - { - integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } - os: [darwin] - - function-bind@1.1.2: - resolution: - { - integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, - } - - function.prototype.name@1.2.0: - resolution: - { - integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==, - } - engines: { node: '>= 0.4' } - - functions-have-names@1.2.3: - resolution: - { - integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==, - } - - fuse.js@7.5.0: - resolution: - { - integrity: sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==, - } - engines: { node: '>=10' } - - generator-function@2.0.1: - resolution: - { - integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==, - } - engines: { node: '>= 0.4' } - - gensync@1.0.0-beta.2: - resolution: - { - integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, - } - engines: { node: '>=6.9.0' } - - get-intrinsic@1.3.0: - resolution: - { - integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, - } - engines: { node: '>= 0.4' } - - get-nonce@1.0.1: - resolution: - { - integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==, - } - engines: { node: '>=6' } - - get-proto@1.0.1: - resolution: - { - integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, - } - engines: { node: '>= 0.4' } - - get-symbol-description@1.1.0: - resolution: - { - integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==, - } - engines: { node: '>= 0.4' } - - get-tsconfig@4.14.0: - resolution: - { - integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==, - } - - glob-parent@5.1.2: - resolution: - { - integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, - } - engines: { node: '>= 6' } - - glob-parent@6.0.2: - resolution: - { - integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, - } - engines: { node: '>=10.13.0' } - - glob@13.0.6: - resolution: - { - integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==, - } - engines: { node: 18 || 20 || >=22 } - - globals@14.0.0: - resolution: - { - integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==, - } - engines: { node: '>=18' } - - globals@16.4.0: - resolution: - { - integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==, - } - engines: { node: '>=18' } - - globalthis@1.0.4: - resolution: - { - integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==, - } - engines: { node: '>= 0.4' } - - gopd@1.2.0: - resolution: - { - integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, - } - engines: { node: '>= 0.4' } - - graceful-fs@4.2.11: - resolution: - { - integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, - } - - has-bigints@1.1.0: - resolution: - { - integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==, - } - engines: { node: '>= 0.4' } - - has-flag@4.0.0: - resolution: - { - integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, - } - engines: { node: '>=8' } - - has-property-descriptors@1.0.2: - resolution: - { - integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==, - } - - has-proto@1.2.0: - resolution: - { - integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==, - } - engines: { node: '>= 0.4' } - - has-symbols@1.1.0: - resolution: - { - integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, - } - engines: { node: '>= 0.4' } - - has-tostringtag@1.0.2: - resolution: - { - integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, - } - engines: { node: '>= 0.4' } - - hasown@2.0.4: - resolution: - { - integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==, - } - engines: { node: '>= 0.4' } - - hermes-estree@0.25.1: - resolution: - { - integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==, - } - - hermes-parser@0.25.1: - resolution: - { - integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==, - } - - html-escaper@2.0.2: - resolution: - { - integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==, - } - - http_ece@1.2.0: - resolution: - { - integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==, - } - engines: { node: '>=16' } - - https-proxy-agent@5.0.1: - resolution: - { - integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==, - } - engines: { node: '>= 6' } - - https-proxy-agent@7.0.6: - resolution: - { - integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==, - } - engines: { node: '>= 14' } - - iceberg-js@0.8.1: - resolution: - { - integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==, - } - engines: { node: '>=20.0.0' } - - idb@8.0.3: - resolution: - { - integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==, - } - - ignore@5.3.2: - resolution: - { - integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, - } - engines: { node: '>= 4' } - - ignore@7.0.6: - resolution: - { - integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==, - } - engines: { node: '>= 4' } - - import-fresh@3.3.1: - resolution: - { - integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, - } - engines: { node: '>=6' } - - import-in-the-middle@3.3.2: - resolution: - { - integrity: sha512-jTd2FfOgOWOdgjkHuk/1Ms8VKFXkPs15ymYBETw1sAOrO/dY3XeGVRWir9qBbw7pXr0T2eTFwfCZ+N02HmiNGA==, - } - engines: { node: '>=18' } - - imurmurhash@0.1.4: - resolution: - { - integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, - } - engines: { node: '>=0.8.19' } - - inherits@2.0.4: - resolution: - { - integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, - } - - internal-slot@1.1.0: - resolution: - { - integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==, - } - engines: { node: '>= 0.4' } - - is-array-buffer@3.0.5: - resolution: - { - integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==, - } - engines: { node: '>= 0.4' } - - is-async-function@2.1.1: - resolution: - { - integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==, - } - engines: { node: '>= 0.4' } - - is-bigint@1.1.0: - resolution: - { - integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==, - } - engines: { node: '>= 0.4' } - - is-boolean-object@1.2.2: - resolution: - { - integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==, - } - engines: { node: '>= 0.4' } - - is-bun-module@2.0.0: - resolution: - { - integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==, - } - - is-callable@1.2.7: - resolution: - { - integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==, - } - engines: { node: '>= 0.4' } - - is-core-module@2.16.2: - resolution: - { - integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==, - } - engines: { node: '>= 0.4' } - - is-data-view@1.0.2: - resolution: - { - integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==, - } - engines: { node: '>= 0.4' } - - is-date-object@1.1.0: - resolution: - { - integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==, - } - engines: { node: '>= 0.4' } - - is-document.all@1.0.0: - resolution: - { - integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==, - } - engines: { node: '>= 0.4' } - - is-extglob@2.1.1: - resolution: - { - integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, - } - engines: { node: '>=0.10.0' } - - is-finalizationregistry@1.1.1: - resolution: - { - integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==, - } - engines: { node: '>= 0.4' } - - is-generator-function@1.1.2: - resolution: - { - integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==, - } - engines: { node: '>= 0.4' } - - is-glob@4.0.3: - resolution: - { - integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, - } - engines: { node: '>=0.10.0' } - - is-map@2.0.3: - resolution: - { - integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==, - } - engines: { node: '>= 0.4' } - - is-negative-zero@2.0.3: - resolution: - { - integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==, - } - engines: { node: '>= 0.4' } - - is-number-object@1.1.1: - resolution: - { - integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==, - } - engines: { node: '>= 0.4' } - - is-number@7.0.0: - resolution: - { - integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, - } - engines: { node: '>=0.12.0' } - - is-reference@1.2.1: - resolution: - { - integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==, - } - - is-regex@1.2.1: - resolution: - { - integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==, - } - engines: { node: '>= 0.4' } - - is-set@2.0.3: - resolution: - { - integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==, - } - engines: { node: '>= 0.4' } - - is-shared-array-buffer@1.0.4: - resolution: - { - integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==, - } - engines: { node: '>= 0.4' } - - is-string@1.1.1: - resolution: - { - integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==, - } - engines: { node: '>= 0.4' } - - is-symbol@1.1.1: - resolution: - { - integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==, - } - engines: { node: '>= 0.4' } - - is-typed-array@1.1.15: - resolution: - { - integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==, - } - engines: { node: '>= 0.4' } - - is-weakmap@2.0.2: - resolution: - { - integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==, - } - engines: { node: '>= 0.4' } - - is-weakref@1.1.1: - resolution: - { - integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==, - } - engines: { node: '>= 0.4' } - - is-weakset@2.0.4: - resolution: - { - integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==, - } - engines: { node: '>= 0.4' } - - isarray@2.0.5: - resolution: - { - integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==, - } - - isexe@2.0.0: - resolution: - { - integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, - } - - istanbul-lib-coverage@3.2.2: - resolution: - { - integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==, - } - engines: { node: '>=8' } - - istanbul-lib-report@3.0.1: - resolution: - { - integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==, - } - engines: { node: '>=10' } - - istanbul-reports@3.2.0: - resolution: - { - integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==, - } - engines: { node: '>=8' } - - iterator.prototype@1.1.5: - resolution: - { - integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==, - } - engines: { node: '>= 0.4' } - - jest-worker@27.5.1: - resolution: - { - integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==, - } - engines: { node: '>= 10.13.0' } - - jiti@2.7.0: - resolution: - { - integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==, - } - hasBin: true - - js-tokens@10.0.0: - resolution: - { - integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==, - } - - js-tokens@4.0.0: - resolution: - { - integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, - } - - js-yaml@4.3.0: - resolution: - { - integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==, - } - hasBin: true - - jsesc@3.1.0: - resolution: - { - integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, - } - engines: { node: '>=6' } - hasBin: true - - json-buffer@3.0.1: - resolution: - { - integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, - } - - json-schema-traverse@0.4.1: - resolution: - { - integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, - } - - json-schema-traverse@1.0.0: - resolution: - { - integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==, - } - - json-stable-stringify-without-jsonify@1.0.1: - resolution: - { - integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, - } - - json5@1.0.2: - resolution: - { - integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==, - } - hasBin: true - - json5@2.2.3: - resolution: - { - integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, - } - engines: { node: '>=6' } - hasBin: true - - jsx-ast-utils@3.3.5: - resolution: - { - integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==, - } - engines: { node: '>=4.0' } - - jwa@2.0.1: - resolution: - { - integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==, - } - - jws@4.0.1: - resolution: - { - integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==, - } - - keyv@4.5.4: - resolution: - { - integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, - } - - kolorist@1.8.0: - resolution: - { - integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==, - } - - language-subtag-registry@0.3.23: - resolution: - { - integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==, - } - - language-tags@1.0.9: - resolution: - { - integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==, - } - engines: { node: '>=0.10' } - - levn@0.4.1: - resolution: - { - integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, - } - engines: { node: '>= 0.8.0' } - - lightningcss-android-arm64@1.32.0: - resolution: - { - integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm64] - os: [android] - - lightningcss-android-arm64@1.33.0: - resolution: - { - integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: - { - integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-arm64@1.33.0: - resolution: - { - integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: - { - integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==, - } - engines: { node: '>= 12.0.0' } - cpu: [x64] - os: [darwin] - - lightningcss-darwin-x64@1.33.0: - resolution: - { - integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==, - } - engines: { node: '>= 12.0.0' } - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: - { - integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==, - } - engines: { node: '>= 12.0.0' } - cpu: [x64] - os: [freebsd] - - lightningcss-freebsd-x64@1.33.0: - resolution: - { - integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==, - } - engines: { node: '>= 12.0.0' } - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: - { - integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm] - os: [linux] - - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: - { - integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: - { - integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-gnu@1.33.0: - resolution: - { - integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: - { - integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-arm64-musl@1.33.0: - resolution: - { - integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: - { - integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==, - } - engines: { node: '>= 12.0.0' } - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-gnu@1.33.0: - resolution: - { - integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==, - } - engines: { node: '>= 12.0.0' } - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: - { - integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==, - } - engines: { node: '>= 12.0.0' } - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-musl@1.33.0: - resolution: - { - integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==, - } - engines: { node: '>= 12.0.0' } - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: - { - integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm64] - os: [win32] - - lightningcss-win32-arm64-msvc@1.33.0: - resolution: - { - integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==, - } - engines: { node: '>= 12.0.0' } - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: - { - integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==, - } - engines: { node: '>= 12.0.0' } - cpu: [x64] - os: [win32] - - lightningcss-win32-x64-msvc@1.33.0: - resolution: - { - integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==, - } - engines: { node: '>= 12.0.0' } - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: - { - integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==, - } - engines: { node: '>= 12.0.0' } - - lightningcss@1.33.0: - resolution: - { - integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==, - } - engines: { node: '>= 12.0.0' } - - locate-path@6.0.0: - resolution: - { - integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, - } - engines: { node: '>=10' } - - lodash.merge@4.6.2: - resolution: - { - integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, - } - - loose-envify@1.4.0: - resolution: - { - integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, - } - hasBin: true - - lru-cache@11.5.2: - resolution: - { - integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==, - } - engines: { node: 20 || >=22 } - - lru-cache@5.1.1: - resolution: - { - integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, - } - - lucide-react@1.27.0: - resolution: - { - integrity: sha512-rJicGl/3Fly/E0rOH1YmPZ6e49JCnKknh1ox1vpHnkfjujAkKA6sqUZvH3MTAaXXjgexyUwgNwTJzTtYuAFYJw==, - } - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - magic-string@0.30.21: - resolution: - { - integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, - } - - magicast@0.5.3: - resolution: - { - integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==, - } - - make-dir@4.0.0: - resolution: - { - integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==, - } - engines: { node: '>=10' } - - math-intrinsics@1.1.0: - resolution: - { - integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, - } - engines: { node: '>= 0.4' } - - merge-stream@2.0.0: - resolution: - { - integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, - } - - merge2@1.4.1: - resolution: - { - integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, - } - engines: { node: '>= 8' } - - meriyah@6.1.4: - resolution: - { - integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==, - } - engines: { node: '>=18.0.0' } - - micromatch@4.0.8: - resolution: - { - integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, - } - engines: { node: '>=8.6' } - - mime-db@1.54.0: - resolution: - { - integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==, - } - engines: { node: '>= 0.6' } - - minimalistic-assert@1.0.1: - resolution: - { - integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==, - } - - minimatch@10.2.5: - resolution: - { - integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==, - } - engines: { node: 18 || 20 || >=22 } - - minimatch@3.1.5: - resolution: - { - integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==, - } - - minimist@1.2.8: - resolution: - { - integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, - } - - minimizer-webpack-plugin@5.6.1: - resolution: - { - integrity: sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==, - } - engines: { node: '>= 10.13.0' } - peerDependencies: - '@minify-html/node': '*' - '@swc/core': '*' - '@swc/css': '*' - '@swc/html': '*' - clean-css: '*' - cssnano: '*' - csso: '*' - esbuild: '*' - html-minifier-terser: '*' - lightningcss: '*' - postcss: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@minify-html/node': - optional: true - '@swc/core': - optional: true - '@swc/css': - optional: true - '@swc/html': - optional: true - clean-css: - optional: true - cssnano: - optional: true - csso: - optional: true - esbuild: - optional: true - html-minifier-terser: - optional: true - lightningcss: - optional: true - postcss: - optional: true - uglify-js: - optional: true - - minipass@7.1.3: - resolution: - { - integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==, - } - engines: { node: '>=16 || 14 >=14.17' } - - module-details-from-path@1.0.4: - resolution: - { - integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==, - } - - ms@2.1.3: - resolution: - { - integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, - } - - nanoid@3.3.16: - resolution: - { - integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==, - } - engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } - hasBin: true - - napi-postinstall@0.3.4: - resolution: - { - integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==, - } - engines: { node: ^12.20.0 || ^14.18.0 || >=16.0.0 } - hasBin: true - - natural-compare@1.4.0: - resolution: - { - integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, - } - - neo-async@2.6.2: - resolution: - { - integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==, - } - - next@16.2.10: - resolution: - { - integrity: sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==, - } - engines: { node: '>=20.9.0' } - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.51.1 - babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - - nextjs-toploader@3.9.17: - resolution: - { - integrity: sha512-9OF0KSSLtoSAuNg2LZ3aTl4hR9mBDj5L9s9DZiFCbMlXehyICGjkIz5dVGzuATU2bheJZoBdFgq9w07AKSuQQw==, - } - peerDependencies: - next: '>= 6.0.0' - react: '>= 16.0.0' - react-dom: '>= 16.0.0' - - node-exports-info@1.6.2: - resolution: - { - integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==, - } - engines: { node: '>= 0.4' } - - node-fetch@2.7.0: - resolution: - { - integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==, - } - engines: { node: 4.x || >=6.0.0 } - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - node-releases@2.0.51: - resolution: - { - integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==, - } - engines: { node: '>=18' } - - nprogress@0.2.0: - resolution: - { - integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==, - } - - object-assign@4.1.1: - resolution: - { - integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, - } - engines: { node: '>=0.10.0' } - - object-inspect@1.13.4: - resolution: - { - integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, - } - engines: { node: '>= 0.4' } - - object-keys@1.1.1: - resolution: - { - integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==, - } - engines: { node: '>= 0.4' } - - object.assign@4.1.7: - resolution: - { - integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==, - } - engines: { node: '>= 0.4' } - - object.entries@1.1.9: - resolution: - { - integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==, - } - engines: { node: '>= 0.4' } - - object.fromentries@2.0.8: - resolution: - { - integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==, - } - engines: { node: '>= 0.4' } - - object.groupby@1.0.3: - resolution: - { - integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==, - } - engines: { node: '>= 0.4' } - - object.values@1.2.1: - resolution: - { - integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==, - } - engines: { node: '>= 0.4' } - - obug@2.1.4: - resolution: - { - integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==, - } - engines: { node: '>=12.20.0' } - - optionator@0.9.4: - resolution: - { - integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, - } - engines: { node: '>= 0.8.0' } - - own-keys@1.0.2: - resolution: - { - integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==, - } - engines: { node: '>= 0.4' } - - p-limit@3.1.0: - resolution: - { - integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, - } - engines: { node: '>=10' } - - p-locate@5.0.0: - resolution: - { - integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, - } - engines: { node: '>=10' } - - parent-module@1.0.1: - resolution: - { - integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, - } - engines: { node: '>=6' } - - path-exists@4.0.0: - resolution: - { - integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, - } - engines: { node: '>=8' } - - path-key@3.1.1: - resolution: - { - integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, - } - engines: { node: '>=8' } - - path-parse@1.0.7: - resolution: - { - integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, - } - - path-scurry@2.0.2: - resolution: - { - integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==, - } - engines: { node: 18 || 20 || >=22 } - - pathe@2.0.3: - resolution: - { - integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, - } - - picocolors@1.1.1: - resolution: - { - integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, - } - - picomatch@2.3.2: - resolution: - { - integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==, - } - engines: { node: '>=8.6' } - - picomatch@4.0.5: - resolution: - { - integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==, - } - engines: { node: '>=12' } - - playwright-core@1.62.0: - resolution: - { - integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==, - } - engines: { node: '>=20' } - hasBin: true - - playwright@1.62.0: - resolution: - { - integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==, - } - engines: { node: '>=20' } - hasBin: true - - possible-typed-array-names@1.1.0: - resolution: - { - integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==, - } - engines: { node: '>= 0.4' } - - postcss@8.5.23: - resolution: - { - integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==, - } - engines: { node: ^10 || ^12 || >=14 } - - prelude-ls@1.2.1: - resolution: - { - integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, - } - engines: { node: '>= 0.8.0' } - - prettier@3.9.6: - resolution: - { - integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==, - } - engines: { node: '>=14' } - hasBin: true - - pretty-bytes@6.1.1: - resolution: - { - integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==, - } - engines: { node: ^14.13.1 || >=16.0.0 } - - progress@2.0.3: - resolution: - { - integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==, - } - engines: { node: '>=0.4.0' } - - prop-types@15.8.1: - resolution: - { - integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, - } - - proxy-from-env@1.1.0: - resolution: - { - integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, - } - - punycode@2.3.1: - resolution: - { - integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, - } - engines: { node: '>=6' } - - queue-microtask@1.2.3: - resolution: - { - integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, - } - - react-dom@19.2.7: - resolution: - { - integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==, - } - peerDependencies: - react: ^19.2.7 - - react-is@16.13.1: - resolution: - { - integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==, - } - - react-remove-scroll-bar@2.3.8: - resolution: - { - integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==, - } - engines: { node: '>=10' } - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - react-remove-scroll@2.7.2: - resolution: - { - integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==, - } - engines: { node: '>=10' } - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - react-style-singleton@2.2.3: - resolution: - { - integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==, - } - engines: { node: '>=10' } - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - react@19.2.7: - resolution: - { - integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==, - } - engines: { node: '>=0.10.0' } - - reflect.getprototypeof@1.0.10: - resolution: - { - integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==, - } - engines: { node: '>= 0.4' } - - regexp.prototype.flags@1.5.4: - resolution: - { - integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==, - } - engines: { node: '>= 0.4' } - - require-from-string@2.0.2: - resolution: - { - integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==, - } - engines: { node: '>=0.10.0' } - - require-in-the-middle@8.0.1: - resolution: - { - integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==, - } - engines: { node: '>=9.3.0 || >=8.10.0 <9.0.0' } - - resolve-from@4.0.0: - resolution: - { - integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, - } - engines: { node: '>=4' } - - resolve-pkg-maps@1.0.0: - resolution: - { - integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, - } - - resolve@2.0.0-next.7: - resolution: - { - integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==, - } - engines: { node: '>= 0.4' } - hasBin: true - - reusify@1.1.0: - resolution: - { - integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==, - } - engines: { iojs: '>=1.0.0', node: '>=0.10.0' } - - rolldown@1.1.5: - resolution: - { - integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - hasBin: true - - rollup@4.62.2: - resolution: - { - integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==, - } - engines: { node: '>=18.0.0', npm: '>=8.0.0' } - hasBin: true - - run-parallel@1.2.0: - resolution: - { - integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, - } - - safe-array-concat@1.1.4: - resolution: - { - integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==, - } - engines: { node: '>=0.4' } - - safe-buffer@5.2.1: - resolution: - { - integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, - } - - safe-push-apply@1.0.0: - resolution: - { - integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==, - } - engines: { node: '>= 0.4' } - - safe-regex-test@1.1.0: - resolution: - { - integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==, - } - engines: { node: '>= 0.4' } - - safer-buffer@2.1.2: - resolution: - { - integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, - } - - scheduler@0.27.0: - resolution: - { - integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==, - } - - schema-utils@4.3.3: - resolution: - { - integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==, - } - engines: { node: '>= 10.13.0' } - - semifies@1.0.0: - resolution: - { - integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==, - } - - semver@6.3.1: - resolution: - { - integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, - } - hasBin: true - - semver@7.8.5: - resolution: - { - integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==, - } - engines: { node: '>=10' } - hasBin: true - - server-only@0.0.1: - resolution: - { - integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==, - } - - serwist@9.5.12: - resolution: - { - integrity: sha512-PwREKJrSb3ja40XJyBntPOm7okcYZJCJPd++jUVz8tzxY1VYO9dyKSm0MimZ8LtUk0pIUTN0q2cfGe7R0wQsaQ==, - } - peerDependencies: - typescript: '>=5.0.0' - peerDependenciesMeta: - typescript: - optional: true - - set-function-length@1.2.2: - resolution: - { - integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==, - } - engines: { node: '>= 0.4' } - - set-function-name@2.0.2: - resolution: - { - integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==, - } - engines: { node: '>= 0.4' } - - set-proto@1.0.0: - resolution: - { - integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==, - } - engines: { node: '>= 0.4' } - - sharp@0.34.5: - resolution: - { - integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } - - shebang-command@2.0.0: - resolution: - { - integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, - } - engines: { node: '>=8' } - - shebang-regex@3.0.0: - resolution: - { - integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, - } - engines: { node: '>=8' } - - side-channel-list@1.0.1: - resolution: - { - integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==, - } - engines: { node: '>= 0.4' } - - side-channel-map@1.0.1: - resolution: - { - integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, - } - engines: { node: '>= 0.4' } - - side-channel-weakmap@1.0.2: - resolution: - { - integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, - } - engines: { node: '>= 0.4' } - - side-channel@1.1.1: - resolution: - { - integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==, - } - engines: { node: '>= 0.4' } - - siginfo@2.0.0: - resolution: - { - integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, - } - - simple-icons@16.27.1: - resolution: - { - integrity: sha512-slZF8iKxkv7Lb9SF3L1AcIl5iYLNvDrKKm8yV69cG66XvxJ12SX+bYTgxRwCY4bXSmklGXoXaKEyVmEypOCeqw==, - } - engines: { node: '>=0.12.18' } - - sonner@2.0.7: - resolution: - { - integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==, - } - peerDependencies: - react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc - react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc - - source-map-js@1.2.1: - resolution: - { - integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, - } - engines: { node: '>=0.10.0' } - - source-map-support@0.5.21: - resolution: - { - integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==, - } - - source-map@0.6.1: - resolution: - { - integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, - } - engines: { node: '>=0.10.0' } - - source-map@0.8.0: - resolution: - { - integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==, - } - engines: { node: '>= 12' } - - stable-hash@0.0.5: - resolution: - { - integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==, - } - - stackback@0.0.2: - resolution: - { - integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, - } - - stacktrace-parser@0.1.11: - resolution: - { - integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==, - } - engines: { node: '>=6' } - - std-env@4.2.0: - resolution: - { - integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==, - } - - stop-iteration-iterator@1.1.0: - resolution: - { - integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==, - } - engines: { node: '>= 0.4' } - - string.prototype.includes@2.0.1: - resolution: - { - integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==, - } - engines: { node: '>= 0.4' } - - string.prototype.matchall@4.0.12: - resolution: - { - integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==, - } - engines: { node: '>= 0.4' } - - string.prototype.repeat@1.0.0: - resolution: - { - integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==, - } - - string.prototype.trim@1.2.11: - resolution: - { - integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==, - } - engines: { node: '>= 0.4' } - - string.prototype.trimend@1.0.10: - resolution: - { - integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==, - } - engines: { node: '>= 0.4' } - - string.prototype.trimstart@1.0.8: - resolution: - { - integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==, - } - engines: { node: '>= 0.4' } - - strip-bom@3.0.0: - resolution: - { - integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, - } - engines: { node: '>=4' } - - strip-json-comments@3.1.1: - resolution: - { - integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, - } - engines: { node: '>=8' } - - styled-jsx@5.1.6: - resolution: - { - integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==, - } - engines: { node: '>= 12.0.0' } - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - - supports-color@7.2.0: - resolution: - { - integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, - } - engines: { node: '>=8' } - - supports-color@8.1.1: - resolution: - { - integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==, - } - engines: { node: '>=10' } - - supports-preserve-symlinks-flag@1.0.0: - resolution: - { - integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, - } - engines: { node: '>= 0.4' } - - tagged-tag@1.0.0: - resolution: - { - integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==, - } - engines: { node: '>=20' } - - tailwind-merge@3.6.0: - resolution: - { - integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==, - } - - tailwindcss@4.3.3: - resolution: - { - integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==, - } - - tapable@2.3.3: - resolution: - { - integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==, - } - engines: { node: '>=6' } - - terser@5.49.0: - resolution: - { - integrity: sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==, - } - engines: { node: '>=10' } - hasBin: true - - tinybench@2.9.0: - resolution: - { - integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==, - } - - tinyexec@1.2.4: - resolution: - { - integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==, - } - engines: { node: '>=18' } - - tinyglobby@0.2.17: - resolution: - { - integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==, - } - engines: { node: '>=12.0.0' } - - tinyrainbow@3.1.0: - resolution: - { - integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==, - } - engines: { node: '>=14.0.0' } - - to-regex-range@5.0.1: - resolution: - { - integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, - } - engines: { node: '>=8.0' } - - tr46@0.0.3: - resolution: - { - integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, - } - - ts-api-utils@2.5.0: - resolution: - { - integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==, - } - engines: { node: '>=18.12' } - peerDependencies: - typescript: '>=4.8.4' - - tsconfig-paths@3.15.0: - resolution: - { - integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==, - } - - tslib@2.8.1: - resolution: - { - integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, - } - - tw-animate-css@1.4.0: - resolution: - { - integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==, - } - - type-check@0.4.0: - resolution: - { - integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, - } - engines: { node: '>= 0.8.0' } - - type-fest@0.7.1: - resolution: - { - integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==, - } - engines: { node: '>=8' } - - type-fest@5.8.0: - resolution: - { - integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==, - } - engines: { node: '>=20' } - - typed-array-buffer@1.0.3: - resolution: - { - integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==, - } - engines: { node: '>= 0.4' } - - typed-array-byte-length@1.0.3: - resolution: - { - integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==, - } - engines: { node: '>= 0.4' } - - typed-array-byte-offset@1.0.4: - resolution: - { - integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==, - } - engines: { node: '>= 0.4' } - - typed-array-length@1.0.8: - resolution: - { - integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==, - } - engines: { node: '>= 0.4' } - - typescript-eslint@8.65.0: - resolution: - { - integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - typescript@6.0.3: - resolution: - { - integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==, - } - engines: { node: '>=14.17' } - hasBin: true - - unbox-primitive@1.1.0: - resolution: - { - integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==, - } - engines: { node: '>= 0.4' } - - undici-types@7.24.6: - resolution: - { - integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==, - } - - unrs-resolver@1.12.2: - resolution: - { - integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==, - } - - update-browserslist-db@1.2.3: - resolution: - { - integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==, - } - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: - { - integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, - } - - use-callback-ref@1.3.3: - resolution: - { - integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==, - } - engines: { node: '>=10' } - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - use-sidecar@1.1.3: - resolution: - { - integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==, - } - engines: { node: '>=10' } - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - virtua@0.49.3: - resolution: - { - integrity: sha512-k1Yn988Vz/L40uDtEWPjfdVo15Suumh4tU4/z5Srs0elNcU9DgBskqdh3llpHyAlXrCeHWTfcclYvY1uU4MmIg==, - } - peerDependencies: - react: '>=16.14.0' - react-dom: '>=16.14.0' - solid-js: '>=1.0' - svelte: '>=5.0' - vue: '>=3.2' - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - solid-js: - optional: true - svelte: - optional: true - vue: - optional: true - - vite@8.1.5: - resolution: - { - integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==, - } - engines: { node: ^20.19.0 || >=22.12.0 } - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@4.1.10: - resolution: - { - integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==, - } - engines: { node: ^20.0.0 || ^22.0.0 || >=24.0.0 } - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - watchpack@2.5.2: - resolution: - { - integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==, - } - engines: { node: '>=10.13.0' } - - web-push@3.6.7: - resolution: - { - integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==, - } - engines: { node: '>= 16' } - hasBin: true - - webidl-conversions@3.0.1: - resolution: - { - integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, - } - - webpack-sources@3.5.1: - resolution: - { - integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==, - } - engines: { node: '>=10.13.0' } - - webpack@5.109.0: - resolution: - { - integrity: sha512-vomrngskVVXEZF9sMZfYAd4pXZUnfaWdJGlF+BTNF+gJBCKYCQBnOeVPlrh39Ewl7nlCsirDplMy6o5g9xJHBg==, - } - engines: { node: '>=10.13.0' } - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - - whatwg-url@5.0.0: - resolution: - { - integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, - } - - which-boxed-primitive@1.1.1: - resolution: - { - integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==, - } - engines: { node: '>= 0.4' } - - which-builtin-type@1.2.1: - resolution: - { - integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==, - } - engines: { node: '>= 0.4' } - - which-collection@1.0.2: - resolution: - { - integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==, - } - engines: { node: '>= 0.4' } - - which-typed-array@1.1.22: - resolution: - { - integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==, - } - engines: { node: '>= 0.4' } - - which@2.0.2: - resolution: - { - integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, - } - engines: { node: '>= 8' } - hasBin: true - - why-is-node-running@2.3.0: - resolution: - { - integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==, - } - engines: { node: '>=8' } - hasBin: true - - word-wrap@1.2.5: - resolution: - { - integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, - } - engines: { node: '>=0.10.0' } - - yallist@3.1.1: - resolution: - { - integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, - } - - yocto-queue@0.1.0: - resolution: - { - integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, - } - engines: { node: '>=10' } - - zod-validation-error@4.0.2: - resolution: - { - integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==, - } - engines: { node: '>=18.0.0' } - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - - zod@4.4.3: - resolution: - { - integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==, - } -snapshots: - '@alloc/quick-lru@5.2.0': {} - - '@apm-js-collab/code-transformer-bundler-plugins@0.7.1': - dependencies: - '@apm-js-collab/code-transformer': 0.18.1 - es-module-lexer: 2.3.1 - magic-string: 0.30.21 - module-details-from-path: 1.0.4 - - '@apm-js-collab/code-transformer@0.18.1': - dependencies: - '@types/estree': 1.0.9 - astring: 1.9.0 - esquery: 1.7.0 - meriyah: 6.1.4 - semifies: 1.0.0 - source-map: 0.6.1 - - '@apm-js-collab/tracing-hooks@0.13.0(supports-color@8.1.1)': - dependencies: - '@apm-js-collab/code-transformer': 0.18.1 - debug: 4.4.3(supports-color@8.1.1) - module-details-from-path: 1.0.4 - transitivePeerDependencies: - - supports-color - - '@babel/code-frame@7.29.7': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.7': {} - - '@babel/core@7.29.7(supports-color@8.1.1)': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) - '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@8.1.1) - '@babel/types': 7.29.7 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@8.1.1) - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.7': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.29.7': - dependencies: - '@babel/compat-data': 7.29.7 - '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.7 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.29.7': {} - - '@babel/helper-module-imports@7.29.7(supports-color@8.1.1)': - dependencies: - '@babel/traverse': 7.29.7(supports-color@8.1.1) - '@babel/types': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': - dependencies: - '@babel/core': 7.29.7(supports-color@8.1.1) - '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) - '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - '@babel/helper-string-parser@7.29.7': {} - - '@babel/helper-validator-identifier@7.29.7': {} - - '@babel/helper-validator-option@7.29.7': {} - - '@babel/helpers@7.29.7': - dependencies: - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/parser@7.29.7': - dependencies: - '@babel/types': 7.29.7 - - '@babel/template@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/traverse@7.29.7(supports-color@8.1.1)': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - '@babel/types@7.29.7': - dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - - '@bcoe/v8-coverage@1.0.2': {} - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.3': - dependencies: - tslib: 2.8.1 - optional: true + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true + '@apm-js-collab/code-transformer-bundler-plugins@0.7.1': + resolution: {integrity: sha512-Yidf5GOl60db80UxUtNdKK3pnY7obU/gs0xOfA0SCdnvVLMCvfYIer/egC3TqpPiT0Jg22eg3RlzcO+zKfPMcA==} + engines: {node: '>=18.0.0'} - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true + '@apm-js-collab/code-transformer@0.18.1': + resolution: {integrity: sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==} + hasBin: true - '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))': - dependencies: - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - eslint-visitor-keys: 3.4.3 + '@apm-js-collab/tracing-hooks@0.13.0': + resolution: {integrity: sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==} - '@eslint-community/regexpp@4.12.2': {} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} - '@eslint/config-array@0.21.2(supports-color@8.1.1)': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@8.1.1) - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} - '@eslint/eslintrc@3.3.6(supports-color@8.1.1)': - dependencies: - ajv: 6.15.0 - debug: 4.4.3(supports-color@8.1.1) - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.3.0 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} - '@eslint/js@9.39.5': {} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} - '@eslint/object-schema@2.1.7': {} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 - '@floating-ui/core@1.8.0': - dependencies: - '@floating-ui/utils': 0.2.12 + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} - '@floating-ui/dom@1.8.0': - dependencies: - '@floating-ui/core': 1.8.0 - '@floating-ui/utils': 0.2.12 + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} - '@floating-ui/react-dom@2.1.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@floating-ui/dom': 1.8.0 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} - '@floating-ui/utils@0.2.12': {} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} - '@humanfs/core@0.19.2': - dependencies: - '@humanfs/types': 0.15.0 + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true - '@humanfs/node@0.16.8': - dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} - '@humanfs/types@0.15.0': {} + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} - '@humanwhocodes/module-importer@1.0.1': {} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} - '@humanwhocodes/retry@0.4.3': {} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} - '@img/colour@1.1.0': - optional: true + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.6': + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@next/env@16.2.12': + resolution: {integrity: sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==} + + '@next/eslint-plugin-next@16.2.12': + resolution: {integrity: sha512-uF2z/qAK2q7B5/6CpnFcBRX6jOq5iCO+Uqh1UkJhXljX1JwLarLYhhoJadO6dPb6moTprOKewMXheBcbIoSbug==} + + '@next/swc-darwin-arm64@16.2.12': + resolution: {integrity: sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.2.12': + resolution: {integrity: sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.2.12': + resolution: {integrity: sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.2.12': + resolution: {integrity: sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.2.12': + resolution: {integrity: sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.2.12': + resolution: {integrity: sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.2.12': + resolution: {integrity: sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.2.12': + resolution: {integrity: sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@opentelemetry/api-logs@0.220.0': + resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.10.0': + resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation@0.220.0': + resolution: {integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.10.0': + resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.10.0': + resolution: {integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.10.0': + resolution: {integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@playwright/test@1.62.0': + resolution: {integrity: sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==} + engines: {node: '>=20'} + hasBin: true + + '@radix-ui/primitive@1.1.7': + resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} + + '@radix-ui/react-arrow@1.1.15': + resolution: {integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.15': + resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.5': + resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.2.2': + resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.23': + resolution: {integrity: sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.4': + resolution: {integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.19': + resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.24': + resolution: {integrity: sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.6': + resolution: {integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.16': + resolution: {integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.4': + resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.15': + resolution: {integrity: sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.24': + resolution: {integrity: sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.23': + resolution: {integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.3.7': + resolution: {integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.17': + resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.10': + resolution: {integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.10': + resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.19': + resolution: {integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.15': + resolution: {integrity: sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.3.3': + resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.3.7': + resolution: {integrity: sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.4': + resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.6': + resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.5': + resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.3': + resolution: {integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.4': + resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.4': + resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.4': + resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/rect@1.1.3': + resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/plugin-commonjs@28.0.1': + resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sentry/babel-plugin-component-annotate@5.3.0': + resolution: {integrity: sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==} + engines: {node: '>= 18'} + + '@sentry/browser-utils@10.68.0': + resolution: {integrity: sha512-be8VtdjCngKc77cstJeV+gO15iH+blyXpBDk8yOehmtX4BkFO33mfTMNCWVR2LA0oOxjIHWRAhf77fIUEhzxPg==} + engines: {node: '>=18'} + + '@sentry/browser@10.68.0': + resolution: {integrity: sha512-8xVgk7oG2lajXnbXF6a7H1xMZ/U6icqSldHGzQu1+bajfrK8Gan9ULG/Xsj1VM1LlNeK6/7znDJ3u1jgvIwznw==} + engines: {node: '>=18'} + + '@sentry/bundler-plugin-core@5.3.0': + resolution: {integrity: sha512-L5T60sWdAI3qWwdg3Ptwek/0TY59PERrxyqp4XMUkroayQvGd9r5dIW9Q1kSeXX9iJ442nXbFZKAOyCKV4Z13Q==} + engines: {node: '>= 18'} + + '@sentry/bundler-plugins@10.68.0': + resolution: {integrity: sha512-XWv7asJuTTUSlacROvqcIFKuNxAf3PYL/mdjMBhBwp3rJ4vMkA73jqNPjqCTm726cHs/Y4yadbbFA/OZLPWzeg==} + engines: {node: '>= 18'} + peerDependencies: + rollup: '>=3.2.0' + webpack: '>=5.0.0' + peerDependenciesMeta: + rollup: + optional: true + webpack: + optional: true + + '@sentry/cli-darwin@2.58.6': + resolution: {integrity: sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA==} + engines: {node: '>=10'} + os: [darwin] + + '@sentry/cli-linux-arm64@2.58.6': + resolution: {integrity: sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd, android] + + '@sentry/cli-linux-arm@2.58.6': + resolution: {integrity: sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd, android] + + '@sentry/cli-linux-i686@2.58.6': + resolution: {integrity: sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd, android] + + '@sentry/cli-linux-x64@2.58.6': + resolution: {integrity: sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd, android] + + '@sentry/cli-win32-arm64@2.58.6': + resolution: {integrity: sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@sentry/cli-win32-i686@2.58.6': + resolution: {integrity: sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + + '@sentry/cli-win32-x64@2.58.6': + resolution: {integrity: sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@sentry/cli@2.58.6': + resolution: {integrity: sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg==} + engines: {node: '>= 10'} + hasBin: true + + '@sentry/conventions@0.16.0': + resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} + engines: {node: '>=14'} + + '@sentry/core@10.68.0': + resolution: {integrity: sha512-5Amhx8ltVz7vb1bRGyf3c4J69/iHW8R/H+SJxTRILHlsSOBrnVVc/IQEYDC6PTRdRdZ3x2u7RVjxZi2Mhe525g==} + engines: {node: '>=18'} + + '@sentry/feedback@10.68.0': + resolution: {integrity: sha512-XbdcXiBnpC3vgw46eHOPeD/ZQ+XzluP75ubdUcaPDW02hCh2nsdXiwjZ2DBImbpvIpTbJgjHf/sIlHWvcZJ2Mg==} + engines: {node: '>=18'} + + '@sentry/nextjs@10.68.0': + resolution: {integrity: sha512-oMDl9F8jkCgAsd8U9gLTAWr7u/j9/nUw0sBRHoAvK7HXfdrg9fPlsDd3wsHgYDC0U2tIDTXICXAwT2zJ7OHHWg==} + engines: {node: '>=18'} + peerDependencies: + next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0 + + '@sentry/node-core@10.68.0': + resolution: {integrity: sha512-VreORXnruy8A2SyprZKENAq3ArGwn35KPewLQSQ5dgDXSFtUj2scLuzZoVsZ/Uyt83td0WPvD6Lv0X7MvSYAMQ==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/core': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/instrumentation': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + + '@sentry/node@10.68.0': + resolution: {integrity: sha512-bnvRzEehquG/894DD3BWNCBUbDWsLQfYPU+SNCMd6G4Ext75RthVkRs8R0sRaE6b8Tw9HSEgySHL834Tf8lVsA==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@10.68.0': + resolution: {integrity: sha512-JDNH9dacX0MSi7FRvabPCJUAXRMTe16bE/Gajc/7Gfcw7Rwvo4DZ0nd162PCSAW9QoEjUT12upDehEHJuFmsXg==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + + '@sentry/react@10.68.0': + resolution: {integrity: sha512-rIq4QR4ScMHHx9JJZv7Jgw31bMdUVJMx+ykHIJb7htjY6mj78sjKs+KpCsMDnvJxDhSmvftGM1KfKD4BggL7OQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + + '@sentry/replay-canvas@10.68.0': + resolution: {integrity: sha512-HusYcr+He+ohnUDHunYrc5St6vdDnBXpUAndnT5ReyUMVSCiWKfY3paXowU/0787HwYfxdcpZgwC5u79+XbEIg==} + engines: {node: '>=18'} + + '@sentry/replay@10.68.0': + resolution: {integrity: sha512-ZoG2n16vbkx4GWSCnLIqUUN9xlUmccQFbQ2US2rhruQeHTUnHl/ukr8NHOQXZaEbwKyMkX9bEMwfmZHJm+wSTQ==} + engines: {node: '>=18'} + + '@sentry/server-utils@10.68.0': + resolution: {integrity: sha512-lp1ZSs1auw7HrCESSYt/n4dOUaKPVUIAKyVYRk6xVr4bMIN3RPub/H5Wm7QPj9CpXVC5bQFvB6+dHZXz809oMg==} + engines: {node: '>=18'} + + '@sentry/vercel-edge@10.68.0': + resolution: {integrity: sha512-6FukkfH1b3T7jMgPVE7todrKcC92zA0gwUA3kyTG645eOdfMTIQ9o5lr5UECixALmzTB0GAgrr2gH9xlR0R9bA==} + engines: {node: '>=18'} + + '@sentry/webpack-plugin@5.4.0': + resolution: {integrity: sha512-J3a0BvUZ75Qxy+v/Ap3Hx4ZEcSjlPHZ/jDtxdRhXQCyNeEb8xq0uUBTI9VLtGk2eNeNucOxOEJ5ngqdNjnEH/A==} + engines: {node: '>= 18'} + peerDependencies: + webpack: '>=5.0.0' + + '@serwist/build@9.5.12': + resolution: {integrity: sha512-U2UkA9BjdpniZkXDIG6NQRBEbXccRvqewzL3hfNEQERvM2bL6ezKvsVF9359akUWP8BxV950Kkq9LKGFmOR8Uw==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + + '@serwist/next@9.5.12': + resolution: {integrity: sha512-aKSDZmxC2w6mvLC1aOcO4OnZqdKIl4zrW2VndlZT12+m+nIyOWih1xqqw4LsvUGfQCx0AswtKUUQ7woRYZ2thg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@serwist/cli': ^9.5.12 + next: '>=14.0.0' + react: '>=18.0.0' + typescript: '>=5.0.0' + peerDependenciesMeta: + '@serwist/cli': + optional: true + typescript: + optional: true + + '@serwist/utils@9.5.12': + resolution: {integrity: sha512-BHDwiGL7H7JS7wFvVlAWTFHGt0ffHuPgKtdkaYP0lEWJFl6fKJRCnTDZhwhusFvBhicf37XP8Y6PouTZbcLmgg==} + peerDependencies: + browserslist: '>=4' + peerDependenciesMeta: + browserslist: + optional: true + + '@serwist/webpack-plugin@9.5.12': + resolution: {integrity: sha512-+a5Fap7wVR4oN5jg1GxVNbWW+xUv3LO7UWYfg2D8PkJZiTCcX8PwkcmdzMGVr39dyOtIMTSAANw0af1zMjVbOA==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: '>=5.0.0' + webpack: 4.4.0 || ^5.9.0 + peerDependenciesMeta: + typescript: + optional: true + webpack: + optional: true + + '@serwist/window@9.5.12': + resolution: {integrity: sha512-+fjApJme34qfwdGE2kLT8sFx+xWO+a15477MN/B/b1v4HOWBmARuA9C2xGFBCT77XzCrzQ/2DxcDePZsqEYOlw==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@supabase/auth-js@2.110.8': + resolution: {integrity: sha512-TQ5neTUDX2C2WmyYa03yGhLMkhdE/SkHXtK8/qxO/APUy3rsymsJCBP48p4jcN6iO2G0ow6RRexQd2mX+dSyJg==} + engines: {node: '>=22.0.0'} + + '@supabase/functions-js@2.110.8': + resolution: {integrity: sha512-5yB9TLYzvv2oSQxwb0gamEvIAsuH66pVt7AM/pz03S7wN6ehD34GNgbShrccetqPedXQSz7e/1hAJ9NeEhoZVg==} + engines: {node: '>=22.0.0'} + + '@supabase/phoenix@0.4.5': + resolution: {integrity: sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==} + + '@supabase/postgrest-js@2.110.8': + resolution: {integrity: sha512-QeRROxl1PpOZw5Jzi7BwdN9icsycMrLlCCvsjS0hYLW+nZoaT46zdagz/glJirj8jHF4jSd5Jyipuae2cBClCw==} + engines: {node: '>=22.0.0'} + + '@supabase/realtime-js@2.110.8': + resolution: {integrity: sha512-mwX7ituX6O31fLf+0g65rpLlNxqgnMaPltPsQwzox6jfmbfVl3tCxXrfr3HEsQcCRjpjuJG1+A0vFzP1yVjKHA==} + engines: {node: '>=22.0.0'} + + '@supabase/ssr@0.10.3': + resolution: {integrity: sha512-ux2CJgX89h0Fz2lY7ZNafNG2SkXpyRc5dz77K9eKeBLPdtywQixKwIuetDeIViAJBp/buOUVmgj8PVesOklNpw==} + peerDependencies: + '@supabase/supabase-js': ^2.105.3 + + '@supabase/storage-js@2.110.8': + resolution: {integrity: sha512-CcfhkZFBLxsthgUabZKxwfsoXdrikIGsL3LsGoV3FZTqCMx/s1y49taT4jT/oya5+1IuB0sFFHw6pF0o0iJniQ==} + engines: {node: '>=22.0.0'} + + '@supabase/supabase-js@2.110.8': + resolution: {integrity: sha512-E5qzoe74zhJRv4wRcbO9eMYzeQDb/+h6c603pL8shcxLGBjTKsIF7XXj05IcNj23TLDgJN1WkMw7mwAPyu5dZg==} + engines: {node: '>=22.0.0'} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.3.3': + resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/web-push@3.6.4': + resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} + + '@typescript-eslint/eslint-plugin@8.65.0': + resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.65.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.65.0': + resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.65.0': + resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + peerDependencies: + '@vitest/browser': 4.1.10 + vitest: 4.1.10 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.11.3 - optional: true + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - '@img/sharp-win32-arm64@0.34.5': - optional: true + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - '@img/sharp-win32-ia32@0.34.5': - optional: true + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - '@img/sharp-win32-x64@0.34.5': - optional: true + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - '@jridgewell/resolve-uri@3.1.2': {} + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - '@jridgewell/source-map@0.3.11': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - '@jridgewell/sourcemap-codec@1.5.5': {} + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.3 - optional: true + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: optional: true - '@next/env@16.2.10': {} + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 - '@next/eslint-plugin-next@16.2.10': - dependencies: - fast-glob: 3.3.1 + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - '@next/swc-darwin-arm64@16.2.10': - optional: true + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - '@next/swc-darwin-x64@16.2.10': - optional: true + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} - '@next/swc-linux-arm64-gnu@16.2.10': - optional: true + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - '@next/swc-linux-arm64-musl@16.2.10': - optional: true + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} - '@next/swc-linux-x64-gnu@16.2.10': - optional: true + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} - '@next/swc-linux-x64-musl@16.2.10': - optional: true + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} - '@next/swc-win32-arm64-msvc@16.2.10': - optional: true + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} - '@next/swc-win32-x64-msvc@16.2.10': - optional: true + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} - '@nodelib/fs.stat@2.0.5': {} + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} - '@nolyfill/is-core-module@1.0.39': {} + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} - '@opentelemetry/api-logs@0.220.0': - dependencies: - '@opentelemetry/api': 1.9.1 + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} - '@opentelemetry/api@1.9.1': {} + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} - '@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/semantic-conventions': 1.43.0 + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} - '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - import-in-the-middle: 3.3.2 - require-in-the-middle: 8.0.1(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.43.0 - - '@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.43.0 - - '@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.43.0 - - '@opentelemetry/semantic-conventions@1.43.0': {} - - '@oxc-project/types@0.139.0': {} - - '@playwright/test@1.62.0': - dependencies: - playwright: 1.62.0 - - '@radix-ui/primitive@1.1.7': {} - - '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-context@1.2.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) - aria-hidden: 1.2.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-direction@1.1.4(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-id@1.1.4(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-label@2.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) - aria-hidden: 1.2.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) - aria-hidden: 1.2.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@floating-ui/react-dom': 2.1.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/rect': 1.1.3 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-separator@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-slot@1.3.3(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-switch@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 + ast-v8-to-istanbul@1.0.5: + resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} - '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/rect': 1.1.3 - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true - '@radix-ui/react-use-size@1.1.4(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} - '@radix-ui/rect@1.1.3': {} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} - '@rolldown/binding-android-arm64@1.1.5': - optional: true + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + engines: {node: '>=4'} - '@rolldown/binding-darwin-arm64@1.1.5': - optional: true + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} - '@rolldown/binding-darwin-x64@1.1.5': - optional: true + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - '@rolldown/binding-freebsd-x64@1.1.5': - optional: true + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - optional: true + baseline-browser-mapping@2.11.1: + resolution: {integrity: sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==} + engines: {node: '>=6.0.0'} + hasBin: true - '@rolldown/binding-linux-arm64-gnu@1.1.5': - optional: true + bn.js@4.12.5: + resolution: {integrity: sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==} - '@rolldown/binding-linux-arm64-musl@1.1.5': - optional: true + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - optional: true + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} - '@rolldown/binding-linux-s390x-gnu@1.1.5': - optional: true + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} - '@rolldown/binding-linux-x64-gnu@1.1.5': - optional: true + browserslist@4.28.6: + resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true - '@rolldown/binding-linux-x64-musl@1.1.5': - optional: true + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true - '@rolldown/binding-openharmony-arm64@1.1.5': - optional: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - '@rolldown/binding-wasm32-wasi@1.1.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - optional: true + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - '@rolldown/binding-win32-arm64-msvc@1.1.5': - optional: true + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} - '@rolldown/binding-win32-x64-msvc@1.1.5': - optional: true + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} - '@rolldown/pluginutils@1.0.1': {} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} - '@rollup/plugin-commonjs@28.0.1(rollup@4.62.2)': - dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.2) - commondir: 1.0.1 - estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.5) - is-reference: 1.2.1 - magic-string: 0.30.21 - picomatch: 4.0.5 - optionalDependencies: - rollup: 4.62.2 + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} - '@rollup/pluginutils@5.4.0(rollup@4.62.2)': - dependencies: - '@types/estree': 1.0.9 - estree-walker: 2.0.2 - picomatch: 4.0.5 - optionalDependencies: - rollup: 4.62.2 + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} - '@rollup/rollup-android-arm-eabi@4.62.2': - optional: true + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} - '@rollup/rollup-android-arm64@4.62.2': - optional: true + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} - '@rollup/rollup-darwin-arm64@4.62.2': - optional: true + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} - '@rollup/rollup-darwin-x64@4.62.2': - optional: true + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} - '@rollup/rollup-freebsd-arm64@4.62.2': - optional: true + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - '@rollup/rollup-freebsd-x64@4.62.2': - optional: true + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - optional: true + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - optional: true + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} - '@rollup/rollup-linux-arm64-gnu@4.62.2': - optional: true + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - '@rollup/rollup-linux-arm64-musl@4.62.2': - optional: true + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - '@rollup/rollup-linux-loong64-gnu@4.62.2': - optional: true + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} - '@rollup/rollup-linux-loong64-musl@4.62.2': - optional: true + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - optional: true + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - '@rollup/rollup-linux-ppc64-musl@4.62.2': - optional: true + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - optional: true + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} - '@rollup/rollup-linux-riscv64-musl@4.62.2': - optional: true + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} - '@rollup/rollup-linux-s390x-gnu@4.62.2': - optional: true + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - '@rollup/rollup-linux-x64-gnu@4.62.2': - optional: true + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - '@rollup/rollup-linux-x64-musl@4.62.2': - optional: true + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} - '@rollup/rollup-openbsd-x64@4.62.2': - optional: true + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} - '@rollup/rollup-openharmony-arm64@4.62.2': - optional: true + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} - '@rollup/rollup-win32-arm64-msvc@4.62.2': + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: optional: true - '@rollup/rollup-win32-ia32-msvc@4.62.2': + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: optional: true - '@rollup/rollup-win32-x64-gnu@4.62.2': - optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - '@rollup/rollup-win32-x64-msvc@4.62.2': - optional: true + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} - '@rtsao/scc@1.1.0': {} + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} - '@sentry/babel-plugin-component-annotate@5.3.0': {} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} - '@sentry/browser-utils@10.68.0': - dependencies: - '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - '@sentry/browser@10.68.0': - dependencies: - '@sentry/browser-utils': 10.68.0 - '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - '@sentry/feedback': 10.68.0 - '@sentry/replay': 10.68.0 - '@sentry/replay-canvas': 10.68.0 + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} - '@sentry/bundler-plugin-core@5.3.0(supports-color@8.1.1)': - dependencies: - '@babel/core': 7.29.7(supports-color@8.1.1) - '@sentry/babel-plugin-component-annotate': 5.3.0 - '@sentry/cli': 2.58.6(supports-color@8.1.1) - dotenv: 16.6.1 - find-up: 5.0.0 - glob: 13.0.6 - magic-string: 0.30.21 - transitivePeerDependencies: - - encoding - - supports-color + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} - '@sentry/bundler-plugins@10.68.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': - dependencies: - '@babel/core': 7.29.7(supports-color@8.1.1) - '@sentry/cli': 2.58.6(supports-color@8.1.1) - '@sentry/core': 10.68.0 - dotenv: 17.4.2 - find-up: 5.0.0 - glob: 13.0.6 - magic-string: 0.30.21 - optionalDependencies: - rollup: 4.62.2 - webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) - transitivePeerDependencies: - - encoding - - supports-color + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} - '@sentry/cli-darwin@2.58.6': - optional: true + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} - '@sentry/cli-linux-arm64@2.58.6': - optional: true + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - '@sentry/cli-linux-arm@2.58.6': - optional: true + electron-to-chromium@1.5.396: + resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==} - '@sentry/cli-linux-i686@2.58.6': - optional: true + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - '@sentry/cli-linux-x64@2.58.6': - optional: true + enhanced-resolve@5.24.3: + resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} + engines: {node: '>=10.13.0'} - '@sentry/cli-win32-arm64@2.58.6': - optional: true + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} - '@sentry/cli-win32-i686@2.58.6': - optional: true + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} - '@sentry/cli-win32-x64@2.58.6': - optional: true + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} - '@sentry/cli@2.58.6(supports-color@8.1.1)': - dependencies: - https-proxy-agent: 5.0.1(supports-color@8.1.1) - node-fetch: 2.7.0 - progress: 2.0.3 - proxy-from-env: 1.1.0 - which: 2.0.2 - optionalDependencies: - '@sentry/cli-darwin': 2.58.6 - '@sentry/cli-linux-arm': 2.58.6 - '@sentry/cli-linux-arm64': 2.58.6 - '@sentry/cli-linux-i686': 2.58.6 - '@sentry/cli-linux-x64': 2.58.6 - '@sentry/cli-win32-arm64': 2.58.6 - '@sentry/cli-win32-i686': 2.58.6 - '@sentry/cli-win32-x64': 2.58.6 - transitivePeerDependencies: - - encoding - - supports-color - - '@sentry/conventions@0.16.0': {} - - '@sentry/core@10.68.0': - dependencies: - '@sentry/conventions': 0.16.0 - - '@sentry/feedback@10.68.0': - dependencies: - '@sentry/core': 10.68.0 - - '@sentry/nextjs@10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': - dependencies: - '@opentelemetry/api': 1.9.1 - '@rollup/plugin-commonjs': 28.0.1(rollup@4.62.2) - '@sentry/browser-utils': 10.68.0 - '@sentry/bundler-plugin-core': 5.3.0(supports-color@8.1.1) - '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - '@sentry/node': 10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@8.1.1) - '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) - '@sentry/react': 10.68.0(react@19.2.7) - '@sentry/server-utils': 10.68.0(supports-color@8.1.1) - '@sentry/vercel-edge': 10.68.0 - '@sentry/webpack-plugin': 5.4.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) - next: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - rollup: 4.62.2 - stacktrace-parser: 0.1.11 - transitivePeerDependencies: - - '@opentelemetry/core' - - '@opentelemetry/exporter-trace-otlp-http' - - '@opentelemetry/sdk-trace-base' - - encoding - - react - - supports-color - - webpack - - '@sentry/node-core@10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': - dependencies: - '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) - import-in-the-middle: 3.3.2 - optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) - '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) - - '@sentry/node@10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@8.1.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) - '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) - '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - '@sentry/node-core': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) - '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) - '@sentry/server-utils': 10.68.0(supports-color@8.1.1) - import-in-the-middle: 3.3.2 - transitivePeerDependencies: - - '@opentelemetry/core' - - '@opentelemetry/exporter-trace-otlp-http' - - supports-color - - '@sentry/opentelemetry@10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) - '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - - '@sentry/react@10.68.0(react@19.2.7)': - dependencies: - '@sentry/browser': 10.68.0 - '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - react: 19.2.7 - - '@sentry/replay-canvas@10.68.0': - dependencies: - '@sentry/core': 10.68.0 - '@sentry/replay': 10.68.0 - - '@sentry/replay@10.68.0': - dependencies: - '@sentry/browser-utils': 10.68.0 - '@sentry/core': 10.68.0 - - '@sentry/server-utils@10.68.0(supports-color@8.1.1)': - dependencies: - '@apm-js-collab/code-transformer-bundler-plugins': 0.7.1 - '@apm-js-collab/tracing-hooks': 0.13.0(supports-color@8.1.1) - '@sentry/conventions': 0.16.0 - '@sentry/core': 10.68.0 - meriyah: 6.1.4 - transitivePeerDependencies: - - supports-color - - '@sentry/vercel-edge@10.68.0': - dependencies: - '@opentelemetry/api': 1.9.1 - '@sentry/core': 10.68.0 - - '@sentry/webpack-plugin@5.4.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': - dependencies: - '@sentry/bundler-plugins': 10.68.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) - webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) - transitivePeerDependencies: - - encoding - - rollup - - supports-color - - '@serwist/build@9.5.12(browserslist@4.28.6)(typescript@6.0.3)': - dependencies: - '@serwist/utils': 9.5.12(browserslist@4.28.6) - common-tags: 1.8.2 - glob: 13.0.6 - pretty-bytes: 6.1.1 - source-map: 0.8.0 - type-fest: 5.8.0 - zod: 4.4.3 - optionalDependencies: - typescript: 6.0.3 - transitivePeerDependencies: - - browserslist - - '@serwist/next@9.5.12(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': - dependencies: - '@serwist/build': 9.5.12(browserslist@4.28.6)(typescript@6.0.3) - '@serwist/utils': 9.5.12(browserslist@4.28.6) - '@serwist/webpack-plugin': 9.5.12(browserslist@4.28.6)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) - '@serwist/window': 9.5.12(browserslist@4.28.6)(typescript@6.0.3) - browserslist: 4.28.6 - glob: 13.0.6 - kolorist: 1.8.0 - next: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - semver: 7.8.5 - serwist: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) - zod: 4.4.3 - optionalDependencies: - typescript: 6.0.3 - transitivePeerDependencies: - - webpack - - '@serwist/utils@9.5.12(browserslist@4.28.6)': - optionalDependencies: - browserslist: 4.28.6 - - '@serwist/webpack-plugin@9.5.12(browserslist@4.28.6)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': - dependencies: - '@serwist/build': 9.5.12(browserslist@4.28.6)(typescript@6.0.3) - '@serwist/utils': 9.5.12(browserslist@4.28.6) - pretty-bytes: 6.1.1 - zod: 4.4.3 - optionalDependencies: - typescript: 6.0.3 - webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) - transitivePeerDependencies: - - browserslist - - '@serwist/window@9.5.12(browserslist@4.28.6)(typescript@6.0.3)': - dependencies: - '@types/trusted-types': 2.0.7 - serwist: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) - optionalDependencies: - typescript: 6.0.3 - transitivePeerDependencies: - - browserslist - - '@standard-schema/spec@1.1.0': {} - - '@supabase/auth-js@2.110.8': - dependencies: - tslib: 2.8.1 - - '@supabase/functions-js@2.110.8': - dependencies: - tslib: 2.8.1 - - '@supabase/phoenix@0.4.5': {} - - '@supabase/postgrest-js@2.110.8': - dependencies: - tslib: 2.8.1 - - '@supabase/realtime-js@2.110.8': - dependencies: - '@supabase/phoenix': 0.4.5 - tslib: 2.8.1 - - '@supabase/ssr@0.10.3(@supabase/supabase-js@2.110.8)': - dependencies: - '@supabase/supabase-js': 2.110.8 - cookie: 1.1.1 + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} - '@supabase/storage-js@2.110.8': - dependencies: - iceberg-js: 0.8.1 - tslib: 2.8.1 + es-iterator-helpers@1.4.0: + resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==} + engines: {node: '>= 0.4'} - '@supabase/supabase-js@2.110.8': - dependencies: - '@supabase/auth-js': 2.110.8 - '@supabase/functions-js': 2.110.8 - '@supabase/postgrest-js': 2.110.8 - '@supabase/realtime-js': 2.110.8 - '@supabase/storage-js': 2.110.8 + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - '@swc/helpers@0.5.15': - dependencies: - tslib: 2.8.1 + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} - '@tailwindcss/node@4.3.3': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.24.3 - jiti: 2.7.0 - lightningcss: 1.32.0 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.3.3 + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} - '@tailwindcss/oxide-android-arm64@4.3.3': - optional: true + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} - '@tailwindcss/oxide-darwin-arm64@4.3.3': - optional: true + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} - '@tailwindcss/oxide-darwin-x64@4.3.3': - optional: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} - '@tailwindcss/oxide-freebsd-x64@4.3.3': - optional: true + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': - optional: true + eslint-config-next@16.2.12: + resolution: {integrity: sha512-iaaf4vvKo5h2LBdGt0JuRv7t0Ysqr9FMCiFxbptDg8LqOE//mIKR80DdpOnSVM7qjLH3jT8P0aFiwXxBEGZRXw==} + peerDependencies: + eslint: '>=9.0.0' + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.14.0: + resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': - optional: true + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - '@tailwindcss/oxide-linux-arm64-musl@4.3.3': - optional: true + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} - '@tailwindcss/oxide-linux-x64-gnu@4.3.3': - optional: true + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@tailwindcss/oxide-linux-x64-musl@4.3.3': - optional: true + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@tailwindcss/oxide-wasm32-wasi@4.3.3': - optional: true + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': - optional: true + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: optional: true - '@tailwindcss/oxide@4.3.3': - 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 + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-average-color@9.5.2: + resolution: {integrity: sha512-FbaU8iPTPljP7tmnVhXbCyASNw/zxnmaNDf88gn5pTXlNvejl9w4FapeWMh6UNDwIjhJJU28EPfQWwW032YgPA==} + engines: {node: '>= 12'} - '@tailwindcss/postcss@4.3.3': - dependencies: - '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.3.3 - '@tailwindcss/oxide': 4.3.3 - postcss: 8.5.23 - tailwindcss: 4.3.3 + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: optional: true - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} - '@types/deep-eql@4.0.2': {} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} - '@types/estree@1.0.9': {} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} - '@types/json-schema@7.0.15': {} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} - '@types/json5@0.0.29': {} + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} - '@types/node@25.9.5': - dependencies: - undici-types: 7.24.6 + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} - '@types/react-dom@19.2.3(@types/react@19.2.17)': - dependencies: - '@types/react': 19.2.17 + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] - '@types/trusted-types@2.0.7': {} + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - '@types/web-push@3.6.4': - dependencies: - '@types/node': 25.9.5 + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.65.0 - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - ignore: 7.0.6 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - '@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.65.0 - debug: 4.4.3(supports-color@8.1.1) - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color + fuse.js@7.5.0: + resolution: {integrity: sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==} + engines: {node: '>=10'} - '@typescript-eslint/project-service@8.65.0(supports-color@8.1.1)(typescript@6.0.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) - '@typescript-eslint/types': 8.65.0 - debug: 4.4.3(supports-color@8.1.1) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} - '@typescript-eslint/scope-manager@8.65.0': - dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} - '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': - dependencies: - typescript: 6.0.3 + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} - '@typescript-eslint/type-utils@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': - dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - debug: 4.4.3(supports-color@8.1.1) - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} - '@typescript-eslint/types@8.65.0': {} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} - '@typescript-eslint/typescript-estree@8.65.0(supports-color@8.1.1)(typescript@6.0.3)': - dependencies: - '@typescript-eslint/project-service': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 - debug: 4.4.3(supports-color@8.1.1) - minimatch: 10.2.5 - semver: 7.8.5 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} - '@typescript-eslint/utils@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': - dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - '@typescript-eslint/visitor-keys@8.65.0': - dependencies: - '@typescript-eslint/types': 8.65.0 - eslint-visitor-keys: 5.0.1 + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} - '@unrs/resolver-binding-android-arm-eabi@1.12.2': - optional: true + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} - '@unrs/resolver-binding-android-arm64@1.12.2': - optional: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} - '@unrs/resolver-binding-darwin-arm64@1.12.2': - optional: true + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} - '@unrs/resolver-binding-darwin-x64@1.12.2': - optional: true + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} - '@unrs/resolver-binding-freebsd-x64@1.12.2': - optional: true + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} - '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': - optional: true + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} - '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': - optional: true + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http_ece@1.2.0: + resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} + engines: {node: '>=16'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iceberg-js@0.8.1: + resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} + engines: {node: '>=20.0.0'} + + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-in-the-middle@3.3.2: + resolution: {integrity: sha512-jTd2FfOgOWOdgjkHuk/1Ms8VKFXkPs15ymYBETw1sAOrO/dY3XeGVRWir9qBbw7pXr0T2eTFwfCZ+N02HmiNGA==} + engines: {node: '>=18'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} - '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.27.0: + resolution: {integrity: sha512-rJicGl/3Fly/E0rOH1YmPZ6e49JCnKknh1ox1vpHnkfjujAkKA6sqUZvH3MTAaXXjgexyUwgNwTJzTtYuAFYJw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + meriyah@6.1.4: + resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==} + engines: {node: '>=18.0.0'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minimizer-webpack-plugin@5.6.1: + resolution: {integrity: sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true - '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + next@16.2.12: + resolution: {integrity: sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: optional: true - '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + nextjs-toploader@3.9.17: + resolution: {integrity: sha512-9OF0KSSLtoSAuNg2LZ3aTl4hR9mBDj5L9s9DZiFCbMlXehyICGjkIz5dVGzuATU2bheJZoBdFgq9w07AKSuQQw==} + peerDependencies: + next: '>= 6.0.0' + react: '>= 16.0.0' + react-dom: '>= 16.0.0' + + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: optional: true - '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.2: + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + playwright-core@1.62.0: + resolution: {integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.0: + resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==} + engines: {node: '>=20'} + hasBin: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': optional: true - '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': optional: true - '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': optional: true - '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + semifies@1.0.0: + resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + serwist@9.5.12: + resolution: {integrity: sha512-PwREKJrSb3ja40XJyBntPOm7okcYZJCJPd++jUVz8tzxY1VYO9dyKSm0MimZ8LtUk0pIUTN0q2cfGe7R0wQsaQ==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: optional: true - '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': optional: true - '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + simple-icons@16.27.1: + resolution: {integrity: sha512-slZF8iKxkv7Lb9SF3L1AcIl5iYLNvDrKKm8yV69cG66XvxJ12SX+bYTgxRwCY4bXSmklGXoXaKEyVmEypOCeqw==} + engines: {node: '>=0.12.18'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.8.0: + resolution: {integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==} + engines: {node: '>= 12'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stacktrace-parser@0.1.11: + resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} + engines: {node: '>=6'} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: optional: true - '@unrs/resolver-binding-linux-x64-musl@1.12.2': + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser@5.49.0: + resolution: {integrity: sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==} + engines: {node: '>=10'} + hasBin: true + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.65.0: + resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': optional: true - '@unrs/resolver-binding-openharmony-arm64@1.12.2': + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': optional: true - '@unrs/resolver-binding-wasm32-wasi@1.12.2': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + virtua@0.49.3: + resolution: {integrity: sha512-k1Yn988Vz/L40uDtEWPjfdVo15Suumh4tU4/z5Srs0elNcU9DgBskqdh3llpHyAlXrCeHWTfcclYvY1uU4MmIg==} + peerDependencies: + react: '>=16.14.0' + react-dom: '>=16.14.0' + solid-js: '>=1.0' + svelte: '>=5.0' + vue: '>=3.2' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vue: optional: true - '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: optional: true - '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} - '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + web-push@3.6.7: + resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} + engines: {node: '>= 16'} + hasBin: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} + engines: {node: '>=10.13.0'} + + webpack@5.109.0: + resolution: {integrity: sha512-vomrngskVVXEZF9sMZfYAd4pXZUnfaWdJGlF+BTNF+gJBCKYCQBnOeVPlrh39Ewl7nlCsirDplMy6o5g9xJHBg==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: optional: true - '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': - dependencies: - '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.10 - ast-v8-to-istanbul: 1.0.5 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.3 - obug: 2.1.4 - std-env: 4.2.0 - tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0)) + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - '@vitest/expect@4.1.10': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - chai: 6.2.2 - tinyrainbow: 3.1.0 + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0))': - dependencies: - '@vitest/spy': 4.1.10 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0) + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} - '@vitest/pretty-format@4.1.10': - dependencies: - tinyrainbow: 3.1.0 + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} - '@vitest/runner@4.1.10': - dependencies: - '@vitest/utils': 4.1.10 - pathe: 2.0.3 + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} - '@vitest/snapshot@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 - magic-string: 0.30.21 - pathe: 2.0.3 + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true - '@vitest/spy@4.1.10': {} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true - '@vitest/utils@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} - '@webassemblyjs/ast@1.14.1': - dependencies: - '@webassemblyjs/helper-numbers': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} - '@webassemblyjs/helper-api-error@1.13.2': {} + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 - '@webassemblyjs/helper-buffer@1.14.1': {} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - '@webassemblyjs/helper-numbers@1.13.2': - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.13.2 - '@webassemblyjs/helper-api-error': 1.13.2 - '@xtuc/long': 4.2.2 +snapshots: - '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} - - '@webassemblyjs/helper-wasm-section@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/wasm-gen': 1.14.1 - - '@webassemblyjs/ieee754@1.13.2': - dependencies: - '@xtuc/ieee754': 1.2.0 - - '@webassemblyjs/leb128@1.13.2': - dependencies: - '@xtuc/long': 4.2.2 - - '@webassemblyjs/utf8@1.13.2': {} - - '@webassemblyjs/wasm-edit@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/helper-wasm-section': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-opt': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - '@webassemblyjs/wast-printer': 1.14.1 - - '@webassemblyjs/wasm-gen@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wasm-opt@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - - '@webassemblyjs/wasm-parser@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-api-error': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wast-printer@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@xtuc/long': 4.2.2 - - '@xtuc/ieee754@1.2.0': {} - - '@xtuc/long@4.2.2': {} - - acorn-jsx@5.3.2(acorn@8.17.0): - dependencies: - acorn: 8.17.0 - - acorn@8.17.0: {} - - agent-base@6.0.2(supports-color@8.1.1): - dependencies: - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - agent-base@7.1.4: {} - - ajv-formats@2.1.1(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 - - ajv-keywords@5.1.0(ajv@8.20.0): - dependencies: - ajv: 8.20.0 - fast-deep-equal: 3.1.3 - - ajv@6.15.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ajv@8.20.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.4 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - argparse@2.0.1: {} - - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - - aria-query@5.3.2: {} - - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array.prototype.findlast@1.2.5: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - es-shim-unscopables: 1.1.0 - - array.prototype.findlastindex@1.2.6: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 - - array.prototype.tosorted@1.1.4: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - asn1.js@5.4.1: - dependencies: - bn.js: 4.12.5 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - safer-buffer: 2.1.2 - - assertion-error@2.0.1: {} - - ast-types-flow@0.0.8: {} - - ast-v8-to-istanbul@1.0.5: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - estree-walker: 3.0.3 - js-tokens: 10.0.0 - - astring@1.9.0: {} - - async-function@1.0.0: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 + '@alloc/quick-lru@5.2.0': {} + + '@apm-js-collab/code-transformer-bundler-plugins@0.7.1': + dependencies: + '@apm-js-collab/code-transformer': 0.18.1 + es-module-lexer: 2.3.1 + magic-string: 0.30.21 + module-details-from-path: 1.0.4 + + '@apm-js-collab/code-transformer@0.18.1': + dependencies: + '@types/estree': 1.0.9 + astring: 1.9.0 + esquery: 1.7.0 + meriyah: 6.1.4 + semifies: 1.0.0 + source-map: 0.6.1 + + '@apm-js-collab/tracing-hooks@0.13.0(supports-color@8.1.1)': + dependencies: + '@apm-js-collab/code-transformer': 0.18.1 + debug: 4.4.3(supports-color@8.1.1) + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7(supports-color@8.1.1)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.7 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7(supports-color@8.1.1)': + dependencies: + '@babel/traverse': 7.29.7(supports-color@8.1.1) + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7(supports-color@8.1.1)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))': + dependencies: + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2(supports-color@8.1.1)': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.6(supports-color@8.1.1)': + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@8.1.1) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/react-dom@2.1.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/dom': 1.8.0 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@floating-ui/utils@0.2.12': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.2 + optional: true + + '@img/sharp-darwin-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.2 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.2': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.2': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.2': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + optional: true + + '@img/sharp-linux-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.2 + optional: true - axe-core@4.12.1: {} + '@img/sharp-linux-arm@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.2 + optional: true - axobject-query@4.1.0: {} + '@img/sharp-linux-ppc64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.2 + optional: true - balanced-match@1.0.2: {} + '@img/sharp-linux-riscv64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.2 + optional: true - balanced-match@4.0.4: {} - - baseline-browser-mapping@2.11.1: {} - - bn.js@4.12.5: {} + '@img/sharp-linux-s390x@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.2 + optional: true - brace-expansion@1.1.16: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 + '@img/sharp-linux-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.2 + optional: true - brace-expansion@5.0.8: - dependencies: - balanced-match: 4.0.4 + '@img/sharp-linuxmusl-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + optional: true - braces@3.0.3: - dependencies: - fill-range: 7.1.1 + '@img/sharp-linuxmusl-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + optional: true - browserslist@4.28.6: - dependencies: - baseline-browser-mapping: 2.11.1 - caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.396 - node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.6) + '@img/sharp-wasm32@0.35.3': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true - browserslist@4.28.7: - dependencies: - baseline-browser-mapping: 2.11.1 - caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.396 - node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.7) + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true - buffer-equal-constant-time@1.0.1: {} + '@img/sharp-win32-arm64@0.35.3': + optional: true - buffer-from@1.1.2: {} + '@img/sharp-win32-ia32@0.35.3': + optional: true - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 + '@img/sharp-win32-x64@0.35.3': + optional: true - call-bind@1.0.9: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - callsites@3.1.0: {} + '@jridgewell/resolve-uri@3.1.2': {} - caniuse-lite@1.0.30001806: {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - chai@6.2.2: {} + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@next/env@16.2.12': {} + + '@next/eslint-plugin-next@16.2.12': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@16.2.12': + optional: true + + '@next/swc-darwin-x64@16.2.12': + optional: true + + '@next/swc-linux-arm64-gnu@16.2.12': + optional: true + + '@next/swc-linux-arm64-musl@16.2.12': + optional: true + + '@next/swc-linux-x64-gnu@16.2.12': + optional: true + + '@next/swc-linux-x64-musl@16.2.12': + optional: true + + '@next/swc-win32-arm64-msvc@16.2.12': + optional: true + + '@next/swc-win32-x64-msvc@16.2.12': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@opentelemetry/api-logs@0.220.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.220.0 + import-in-the-middle: 3.3.2 + require-in-the-middle: 8.0.1(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/semantic-conventions@1.43.0': {} + + '@oxc-project/types@0.139.0': {} + + '@playwright/test@1.62.0': + dependencies: + playwright: 1.62.0 + + '@radix-ui/primitive@1.1.7': {} + + '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-context@1.2.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-direction@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-id@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-label@2.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/rect': 1.1.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-separator@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-slot@1.3.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-switch@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/rect': 1.1.3 + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-size@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/rect@1.1.3': {} + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true - chrome-trace-event@1.0.4: {} + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true - cjs-module-lexer@2.2.0: {} + '@rolldown/pluginutils@1.0.1': {} - class-variance-authority@0.7.1: - dependencies: - clsx: 2.1.1 + '@rollup/plugin-commonjs@28.0.1(rollup@4.62.2)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.5) + is-reference: 1.2.1 + magic-string: 0.30.21 + picomatch: 4.0.5 + optionalDependencies: + rollup: 4.62.2 - client-only@0.0.1: {} + '@rollup/pluginutils@5.4.0(rollup@4.62.2)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + optionalDependencies: + rollup: 4.62.2 - clsx@2.1.1: {} + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 + '@rollup/rollup-android-arm64@4.62.2': + optional: true - color-name@1.1.4: {} + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true - commander@2.20.3: {} + '@rollup/rollup-darwin-x64@4.62.2': + optional: true - common-tags@1.8.2: {} + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true - commondir@1.0.1: {} + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true - concat-map@0.0.1: {} + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true - convert-source-map@2.0.0: {} + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true - cookie@1.1.1: {} + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true - csstype@3.2.3: {} + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true - damerau-levenshtein@1.0.8: {} - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - debug@3.2.7(supports-color@8.1.1): - dependencies: - ms: 2.1.3 - optionalDependencies: - supports-color: 8.1.1 - - debug@4.4.3(supports-color@8.1.1): - dependencies: - ms: 2.1.3 - optionalDependencies: - supports-color: 8.1.1 - - deep-is@0.1.4: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - detect-libc@2.1.2: {} - - detect-node-es@1.1.0: {} - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - dotenv@16.6.1: {} - - dotenv@17.4.2: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - - electron-to-chromium@1.5.396: {} - - emoji-regex@9.2.2: {} - - enhanced-resolve@5.24.3: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - es-abstract-get@1.0.0: - dependencies: - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - is-callable: 1.2.7 - object-inspect: 1.13.4 - - es-abstract@1.24.2: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.4 - function.prototype.name: 1.2.0 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.2 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.4 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.11 - string.prototype.trimend: 1.0.10 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.8 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.22 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-iterator-helpers@1.4.0: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - math-intrinsics: 1.1.0 - - es-module-lexer@2.3.1: {} - - es-object-atoms@1.1.2: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.4 - - es-to-primitive@1.3.4: - dependencies: - es-abstract-get: 1.0.0 - es-define-property: 1.0.1 - es-errors: 1.3.0 - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - - escalade@3.2.0: {} - - escape-string-regexp@4.0.0: {} - - eslint-config-next@16.2.10(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): - dependencies: - '@next/eslint-plugin-next': 16.2.10 - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) - eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) - eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - globals: 16.4.0 - typescript-eslint: 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - optionalDependencies: - typescript: 6.0.3 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - - eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)): - dependencies: - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - - eslint-import-resolver-node@0.3.10(supports-color@8.1.1): - dependencies: - debug: 3.2.7(supports-color@8.1.1) - is-core-module: 2.16.2 - resolve: 2.0.0-next.7 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3(supports-color@8.1.1) - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - get-tsconfig: 4.14.0 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.17 - unrs-resolver: 1.12.2 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): - dependencies: - debug: 3.2.7(supports-color@8.1.1) - optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7(supports-color@8.1.1) - doctrine: 2.1.0 - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - hasown: 2.0.4 - is-core-module: 2.16.2 - is-glob: 4.0.3 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.10 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)): - dependencies: - aria-query: 5.3.2 - array-includes: 3.1.9 - array.prototype.flatmap: 1.3.3 - ast-types-flow: 0.0.8 - axe-core: 4.12.1 - axobject-query: 4.1.0 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - hasown: 2.0.4 - jsx-ast-utils: 3.3.5 - language-tags: 1.0.9 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - safe-regex-test: 1.1.0 - string.prototype.includes: 2.0.1 - - eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): - dependencies: - '@babel/core': 7.29.7(supports-color@8.1.1) - '@babel/parser': 7.29.7 - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - hermes-parser: 0.25.1 - zod: 4.4.3 - zod-validation-error: 4.0.2(zod@4.4.3) - transitivePeerDependencies: - - supports-color - - eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.4.0 - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - estraverse: 5.3.0 - hasown: 2.0.4 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.5 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.7 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - - eslint-scope@5.1.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint-visitor-keys@5.0.1: {} - - eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1): - dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2(supports-color@8.1.1) - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.6(supports-color@8.1.1) - '@eslint/js': 9.39.5 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.8 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.9 - ajv: 6.15.0 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@8.1.1) - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.7.0 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) - eslint-visitor-keys: 4.2.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@4.3.0: {} - - estraverse@5.3.0: {} - - estree-walker@2.0.2: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.9 - - esutils@2.0.3: {} - - events@3.3.0: {} - - expect-type@1.4.0: {} - - fast-average-color@9.5.2: {} - - fast-deep-equal@3.1.3: {} - - fast-glob@3.3.1: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fast-uri@3.1.4: {} - - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true - flat-cache@4.0.1: - dependencies: - flatted: 3.4.3 - keyv: 4.5.4 + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true - flatted@3.4.3: {} + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true - fsevents@2.3.2: - optional: true + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true - fsevents@2.3.3: - optional: true + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true - function-bind@1.1.2: {} + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@sentry/babel-plugin-component-annotate@5.3.0': {} + + '@sentry/browser-utils@10.68.0': + dependencies: + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + + '@sentry/browser@10.68.0': + dependencies: + '@sentry/browser-utils': 10.68.0 + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + '@sentry/feedback': 10.68.0 + '@sentry/replay': 10.68.0 + '@sentry/replay-canvas': 10.68.0 + + '@sentry/bundler-plugin-core@5.3.0(supports-color@8.1.1)': + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@sentry/babel-plugin-component-annotate': 5.3.0 + '@sentry/cli': 2.58.6(supports-color@8.1.1) + dotenv: 16.6.1 + find-up: 5.0.0 + glob: 13.0.6 + magic-string: 0.30.21 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/bundler-plugins@10.68.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@sentry/cli': 2.58.6(supports-color@8.1.1) + '@sentry/core': 10.68.0 + dotenv: 17.4.2 + find-up: 5.0.0 + glob: 13.0.6 + magic-string: 0.30.21 + optionalDependencies: + rollup: 4.62.2 + webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/cli-darwin@2.58.6': + optional: true + + '@sentry/cli-linux-arm64@2.58.6': + optional: true + + '@sentry/cli-linux-arm@2.58.6': + optional: true + + '@sentry/cli-linux-i686@2.58.6': + optional: true + + '@sentry/cli-linux-x64@2.58.6': + optional: true + + '@sentry/cli-win32-arm64@2.58.6': + optional: true + + '@sentry/cli-win32-i686@2.58.6': + optional: true + + '@sentry/cli-win32-x64@2.58.6': + optional: true + + '@sentry/cli@2.58.6(supports-color@8.1.1)': + dependencies: + https-proxy-agent: 5.0.1(supports-color@8.1.1) + node-fetch: 2.7.0 + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.58.6 + '@sentry/cli-linux-arm': 2.58.6 + '@sentry/cli-linux-arm64': 2.58.6 + '@sentry/cli-linux-i686': 2.58.6 + '@sentry/cli-linux-x64': 2.58.6 + '@sentry/cli-win32-arm64': 2.58.6 + '@sentry/cli-win32-i686': 2.58.6 + '@sentry/cli-win32-x64': 2.58.6 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/conventions@0.16.0': {} + + '@sentry/core@10.68.0': + dependencies: + '@sentry/conventions': 0.16.0 + + '@sentry/feedback@10.68.0': + dependencies: + '@sentry/core': 10.68.0 + + '@sentry/nextjs@10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(next@16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@rollup/plugin-commonjs': 28.0.1(rollup@4.62.2) + '@sentry/browser-utils': 10.68.0 + '@sentry/bundler-plugin-core': 5.3.0(supports-color@8.1.1) + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + '@sentry/node': 10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@8.1.1) + '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/react': 10.68.0(react@19.2.7) + '@sentry/server-utils': 10.68.0(supports-color@8.1.1) + '@sentry/vercel-edge': 10.68.0 + '@sentry/webpack-plugin': 5.4.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) + next: 16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + rollup: 4.62.2 + stacktrace-parser: 0.1.11 + transitivePeerDependencies: + - '@opentelemetry/core' + - '@opentelemetry/exporter-trace-otlp-http' + - '@opentelemetry/sdk-trace-base' + - encoding + - react + - supports-color + - webpack + + '@sentry/node-core@10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': + dependencies: + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + import-in-the-middle: 3.3.2 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + + '@sentry/node@10.68.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@8.1.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + '@sentry/node-core': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/opentelemetry': 10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/server-utils': 10.68.0(supports-color@8.1.1) + import-in-the-middle: 3.3.2 + transitivePeerDependencies: + - '@opentelemetry/core' + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + + '@sentry/opentelemetry@10.68.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + + '@sentry/react@10.68.0(react@19.2.7)': + dependencies: + '@sentry/browser': 10.68.0 + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + react: 19.2.7 + + '@sentry/replay-canvas@10.68.0': + dependencies: + '@sentry/core': 10.68.0 + '@sentry/replay': 10.68.0 + + '@sentry/replay@10.68.0': + dependencies: + '@sentry/browser-utils': 10.68.0 + '@sentry/core': 10.68.0 + + '@sentry/server-utils@10.68.0(supports-color@8.1.1)': + dependencies: + '@apm-js-collab/code-transformer-bundler-plugins': 0.7.1 + '@apm-js-collab/tracing-hooks': 0.13.0(supports-color@8.1.1) + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + meriyah: 6.1.4 + transitivePeerDependencies: + - supports-color + + '@sentry/vercel-edge@10.68.0': + dependencies: + '@opentelemetry/api': 1.9.1 + '@sentry/core': 10.68.0 + + '@sentry/webpack-plugin@5.4.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': + dependencies: + '@sentry/bundler-plugins': 10.68.0(rollup@4.62.2)(supports-color@8.1.1)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) + webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@serwist/build@9.5.12(browserslist@4.28.6)(typescript@6.0.3)': + dependencies: + '@serwist/utils': 9.5.12(browserslist@4.28.6) + common-tags: 1.8.2 + glob: 13.0.6 + pretty-bytes: 6.1.1 + source-map: 0.8.0 + type-fest: 5.8.0 + zod: 4.4.3 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - browserslist + + '@serwist/next@9.5.12(next@16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': + dependencies: + '@serwist/build': 9.5.12(browserslist@4.28.6)(typescript@6.0.3) + '@serwist/utils': 9.5.12(browserslist@4.28.6) + '@serwist/webpack-plugin': 9.5.12(browserslist@4.28.6)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) + '@serwist/window': 9.5.12(browserslist@4.28.6)(typescript@6.0.3) + browserslist: 4.28.6 + glob: 13.0.6 + kolorist: 1.8.0 + next: 16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + semver: 7.8.5 + serwist: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) + zod: 4.4.3 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - webpack + + '@serwist/utils@9.5.12(browserslist@4.28.6)': + optionalDependencies: + browserslist: 4.28.6 + + '@serwist/webpack-plugin@9.5.12(browserslist@4.28.6)(typescript@6.0.3)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23))': + dependencies: + '@serwist/build': 9.5.12(browserslist@4.28.6)(typescript@6.0.3) + '@serwist/utils': 9.5.12(browserslist@4.28.6) + pretty-bytes: 6.1.1 + zod: 4.4.3 + optionalDependencies: + typescript: 6.0.3 + webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) + transitivePeerDependencies: + - browserslist + + '@serwist/window@9.5.12(browserslist@4.28.6)(typescript@6.0.3)': + dependencies: + '@types/trusted-types': 2.0.7 + serwist: 9.5.12(browserslist@4.28.6)(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - browserslist + + '@standard-schema/spec@1.1.0': {} + + '@supabase/auth-js@2.110.8': + dependencies: + tslib: 2.8.1 + + '@supabase/functions-js@2.110.8': + dependencies: + tslib: 2.8.1 + + '@supabase/phoenix@0.4.5': {} + + '@supabase/postgrest-js@2.110.8': + dependencies: + tslib: 2.8.1 + + '@supabase/realtime-js@2.110.8': + dependencies: + '@supabase/phoenix': 0.4.5 + tslib: 2.8.1 + + '@supabase/ssr@0.10.3(@supabase/supabase-js@2.110.8)': + dependencies: + '@supabase/supabase-js': 2.110.8 + cookie: 1.1.1 + + '@supabase/storage-js@2.110.8': + dependencies: + iceberg-js: 0.8.1 + tslib: 2.8.1 + + '@supabase/supabase-js@2.110.8': + dependencies: + '@supabase/auth-js': 2.110.8 + '@supabase/functions-js': 2.110.8 + '@supabase/postgrest-js': 2.110.8 + '@supabase/realtime-js': 2.110.8 + '@supabase/storage-js': 2.110.8 + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + 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 + + '@tailwindcss/postcss@4.3.3': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + postcss: 8.5.23 + tailwindcss: 4.3.3 + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@25.9.5': + dependencies: + undici-types: 7.24.6 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': {} + + '@types/web-push@3.6.4': + dependencies: + '@types/node': 25.9.5 + + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/type-utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.65.0 + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.65.0(supports-color@8.1.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + debug: 4.4.3(supports-color@8.1.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.65.0': {} + + '@typescript-eslint/typescript-estree@8.65.0(supports-color@8.1.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + eslint-visitor-keys: 5.0.1 + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.10 + ast-v8-to-istanbul: 1.0.5 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.4 + std-env: 4.2.0 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0)) + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + agent-base@6.0.2(supports-color@8.1.1): + dependencies: + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.4 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.5 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + + assertion-error@2.0.1: {} + + ast-types-flow@0.0.8: {} + + ast-v8-to-istanbul@1.0.5: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + astring@1.9.0: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 - function.prototype.name@1.2.0: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - es-define-property: 1.0.1 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - hasown: 2.0.4 - is-callable: 1.2.7 - is-document.all: 1.0.0 + axe-core@4.12.1: {} - functions-have-names@1.2.3: {} + axobject-query@4.1.0: {} - fuse.js@7.5.0: {} + balanced-match@1.0.2: {} - generator-function@2.0.1: {} + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.1: {} + + bn.js@4.12.5: {} - gensync@1.0.0-beta.2: {} + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - math-intrinsics: 1.1.0 + brace-expansion@5.0.8: + dependencies: + balanced-match: 4.0.4 - get-nonce@1.0.1: {} + braces@3.0.3: + dependencies: + fill-range: 7.1.1 - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.2 + browserslist@4.28.6: + dependencies: + baseline-browser-mapping: 2.11.1 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.396 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.6) - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 + browserslist@4.28.7: + dependencies: + baseline-browser-mapping: 2.11.1 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.396 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.7) - get-tsconfig@4.14.0: - dependencies: - resolve-pkg-maps: 1.0.0 + buffer-equal-constant-time@1.0.1: {} - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 + buffer-from@1.1.2: {} - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 - glob@13.0.6: - dependencies: - minimatch: 10.2.5 - minipass: 7.1.3 - path-scurry: 2.0.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 - globals@14.0.0: {} + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 - globals@16.4.0: {} + callsites@3.1.0: {} - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 + caniuse-lite@1.0.30001806: {} - gopd@1.2.0: {} + chai@6.2.2: {} - graceful-fs@4.2.11: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 - has-bigints@1.1.0: {} + chrome-trace-event@1.0.4: {} - has-flag@4.0.0: {} + cjs-module-lexer@2.2.0: {} - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 + client-only@0.0.1: {} - has-symbols@1.1.0: {} + clsx@2.1.1: {} - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 + color-name@1.1.4: {} - hermes-estree@0.25.1: {} + commander@2.20.3: {} - hermes-parser@0.25.1: - dependencies: - hermes-estree: 0.25.1 + common-tags@1.8.2: {} - html-escaper@2.0.2: {} + commondir@1.0.1: {} - http_ece@1.2.0: {} + concat-map@0.0.1: {} - https-proxy-agent@5.0.1(supports-color@8.1.1): - dependencies: - agent-base: 6.0.2(supports-color@8.1.1) - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color + convert-source-map@2.0.0: {} - https-proxy-agent@7.0.6(supports-color@8.1.1): - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color + cookie@1.1.1: {} - iceberg-js@0.8.1: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 - idb@8.0.3: {} + csstype@3.2.3: {} - ignore@5.3.2: {} + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dotenv@16.6.1: {} + + dotenv@17.4.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + electron-to-chromium@1.5.396: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.24.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.2 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.4.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@2.3.1: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-next@16.2.12(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): + dependencies: + '@next/eslint-plugin-next': 16.2.12 + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + globals: 16.4.0 + typescript-eslint: 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)): + dependencies: + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + + eslint-import-resolver-node@0.3.10(supports-color@8.1.1): + dependencies: + debug: 3.2.7(supports-color@8.1.1) + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + get-tsconfig: 4.14.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): + dependencies: + debug: 3.2.7(supports-color@8.1.1) + optionalDependencies: + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7(supports-color@8.1.1) + doctrine: 2.1.0 + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + hasown: 2.0.4 + is-core-module: 2.16.2 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.10 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.12.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/parser': 7.29.7 + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.4.0 + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + estraverse: 5.3.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2(supports-color@8.1.1) + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.6(supports-color@8.1.1) + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + events@3.3.0: {} + + expect-type@1.4.0: {} + + fast-average-color@9.5.2: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.4: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.3 + keyv: 4.5.4 + + flatted@3.4.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 - ignore@7.0.6: {} + fsevents@2.3.2: + optional: true - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 + fsevents@2.3.3: + optional: true - import-in-the-middle@3.3.2: - dependencies: - cjs-module-lexer: 2.2.0 - es-module-lexer: 2.3.1 - module-details-from-path: 1.0.4 + function-bind@1.1.2: {} - imurmurhash@0.1.4: {} + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 - inherits@2.0.4: {} + functions-have-names@1.2.3: {} - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.4 - side-channel: 1.1.1 + fuse.js@7.5.0: {} - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 + generator-function@2.0.1: {} - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 + gensync@1.0.0-beta.2: {} - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 + get-nonce@1.0.1: {} - is-bun-module@2.0.0: - dependencies: - semver: 7.8.5 + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 - is-callable@1.2.7: {} + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 - is-core-module@2.16.2: - dependencies: - hasown: 2.0.4 + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-document.all@1.0.0: - dependencies: - call-bound: 1.0.4 - - is-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 - is-map@2.0.3: {} + globals@14.0.0: {} - is-negative-zero@2.0.3: {} + globals@16.4.0: {} - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 - is-number@7.0.0: {} + gopd@1.2.0: {} - is-reference@1.2.1: - dependencies: - '@types/estree': 1.0.9 + graceful-fs@4.2.11: {} - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 + has-bigints@1.1.0: {} - is-set@2.0.3: {} + has-flag@4.0.0: {} - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 + has-symbols@1.1.0: {} - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.22 + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 - is-weakmap@2.0.2: {} + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 + hermes-estree@0.25.1: {} - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 - isarray@2.0.5: {} + html-escaper@2.0.2: {} - isexe@2.0.0: {} + http_ece@1.2.0: {} - istanbul-lib-coverage@3.2.2: {} + https-proxy-agent@5.0.1(supports-color@8.1.1): + dependencies: + agent-base: 6.0.2(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 + https-proxy-agent@7.0.6(supports-color@8.1.1): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 + iceberg-js@0.8.1: {} - iterator.prototype@1.1.5: - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 + idb@8.0.3: {} - jest-worker@27.5.1: - dependencies: - '@types/node': 25.9.5 - merge-stream: 2.0.0 - supports-color: 8.1.1 + ignore@5.3.2: {} - jiti@2.7.0: {} + ignore@7.0.6: {} - js-tokens@10.0.0: {} + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 - js-tokens@4.0.0: {} + import-in-the-middle@3.3.2: + dependencies: + cjs-module-lexer: 2.2.0 + es-module-lexer: 2.3.1 + module-details-from-path: 1.0.4 - js-yaml@4.3.0: - dependencies: - argparse: 2.0.1 + imurmurhash@0.1.4: {} - jsesc@3.1.0: {} + inherits@2.0.4: {} - json-buffer@3.0.1: {} + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 - json-schema-traverse@0.4.1: {} + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 - json-schema-traverse@1.0.0: {} + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 - json-stable-stringify-without-jsonify@1.0.1: {} + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 - json5@1.0.2: - dependencies: - minimist: 1.2.8 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 - json5@2.2.3: {} + is-bun-module@2.0.0: + dependencies: + semver: 7.8.5 - jsx-ast-utils@3.3.5: - dependencies: - array-includes: 3.1.9 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 + is-callable@1.2.7: {} - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 - jws@4.0.1: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.2.1 + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 - kolorist@1.8.0: {} + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 - language-subtag-registry@0.3.23: {} + is-map@2.0.3: {} - language-tags@1.0.9: - dependencies: - language-subtag-registry: 0.3.23 + is-negative-zero@2.0.3: {} - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 - lightningcss-android-arm64@1.32.0: - optional: true + is-number@7.0.0: {} - lightningcss-android-arm64@1.33.0: - optional: true + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.9 - lightningcss-darwin-arm64@1.32.0: - optional: true + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 - lightningcss-darwin-arm64@1.33.0: - optional: true + is-set@2.0.3: {} - lightningcss-darwin-x64@1.32.0: - optional: true + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 - lightningcss-darwin-x64@1.33.0: - optional: true + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 - lightningcss-freebsd-x64@1.32.0: - optional: true + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 - lightningcss-freebsd-x64@1.33.0: - optional: true + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true + is-weakmap@2.0.2: {} - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 - lightningcss-linux-arm64-gnu@1.32.0: - optional: true + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 - lightningcss-linux-arm64-gnu@1.33.0: - optional: true + isarray@2.0.5: {} - lightningcss-linux-arm64-musl@1.32.0: - optional: true + isexe@2.0.0: {} - lightningcss-linux-arm64-musl@1.33.0: - optional: true + istanbul-lib-coverage@3.2.2: {} - lightningcss-linux-x64-gnu@1.32.0: - optional: true + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 - lightningcss-linux-x64-gnu@1.33.0: - optional: true + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 - lightningcss-linux-x64-musl@1.32.0: - optional: true + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 - lightningcss-linux-x64-musl@1.33.0: - optional: true + jest-worker@27.5.1: + dependencies: + '@types/node': 25.9.5 + merge-stream: 2.0.0 + supports-color: 8.1.1 - lightningcss-win32-arm64-msvc@1.32.0: - optional: true + jiti@2.7.0: {} - lightningcss-win32-arm64-msvc@1.33.0: - optional: true + js-tokens@10.0.0: {} - lightningcss-win32-x64-msvc@1.32.0: - optional: true + js-tokens@4.0.0: {} - lightningcss-win32-x64-msvc@1.33.0: - optional: true + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kolorist@1.8.0: {} + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - 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 + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 + lightningcss-linux-arm64-gnu@1.32.0: + optional: true - lodash.merge@4.6.2: {} + lightningcss-linux-arm64-gnu@1.33.0: + optional: true - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 + lightningcss-linux-arm64-musl@1.32.0: + optional: true - lru-cache@11.5.2: {} + lightningcss-linux-arm64-musl@1.33.0: + optional: true - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 + lightningcss-linux-x64-gnu@1.32.0: + optional: true - lucide-react@1.27.0(react@19.2.7): - dependencies: - react: 19.2.7 + lightningcss-linux-x64-gnu@1.33.0: + optional: true - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + lightningcss-linux-x64-musl@1.32.0: + optional: true - magicast@0.5.3: - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - source-map-js: 1.2.1 + lightningcss-linux-x64-musl@1.33.0: + optional: true - make-dir@4.0.0: - dependencies: - semver: 7.8.5 + lightningcss-win32-arm64-msvc@1.32.0: + optional: true - math-intrinsics@1.1.0: {} + lightningcss-win32-arm64-msvc@1.33.0: + optional: true - merge-stream@2.0.0: {} + lightningcss-win32-x64-msvc@1.32.0: + optional: true - merge2@1.4.1: {} + lightningcss-win32-x64-msvc@1.33.0: + optional: true - meriyah@6.1.4: {} + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + 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 - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.2 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 - mime-db@1.54.0: {} + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 - minimalistic-assert@1.0.1: {} + lodash.merge@4.6.2: {} - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.8 - - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.16 - - minimist@1.2.8: {} - - minimizer-webpack-plugin@5.6.1(lightningcss@1.33.0)(postcss@8.5.23)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - terser: 5.49.0 - webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) - optionalDependencies: - lightningcss: 1.33.0 - postcss: 8.5.23 - - minipass@7.1.3: {} - - module-details-from-path@1.0.4: {} - - ms@2.1.3: {} - - nanoid@3.3.16: {} - - napi-postinstall@0.3.4: {} - - natural-compare@1.4.0: {} - - neo-async@2.6.2: {} - - next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - '@next/env': 16.2.10 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.11.1 - caniuse-lite: 1.0.30001806 - postcss: 8.5.23 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.7) - optionalDependencies: - '@next/swc-darwin-arm64': 16.2.10 - '@next/swc-darwin-x64': 16.2.10 - '@next/swc-linux-arm64-gnu': 16.2.10 - '@next/swc-linux-arm64-musl': 16.2.10 - '@next/swc-linux-x64-gnu': 16.2.10 - '@next/swc-linux-x64-musl': 16.2.10 - '@next/swc-win32-arm64-msvc': 16.2.10 - '@next/swc-win32-x64-msvc': 16.2.10 - '@opentelemetry/api': 1.9.1 - '@playwright/test': 1.62.0 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - - nextjs-toploader@3.9.17(next@16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - next: 16.2.10(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - nprogress: 0.2.0 - prop-types: 15.8.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - - node-exports-info@1.6.2: - dependencies: - array.prototype.flatmap: 1.3.3 - es-errors: 1.3.0 - object.entries: 1.1.9 - semver: 6.3.1 - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - - node-releases@2.0.51: {} - - nprogress@0.2.0: {} - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.entries@1.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.27.0(react@19.2.7): + dependencies: + react: 19.2.7 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + math-intrinsics@1.1.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + meriyah@6.1.4: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.54.0: {} + + minimalistic-assert@1.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.8 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + + minimist@1.2.8: {} + + minimizer-webpack-plugin@5.6.1(lightningcss@1.33.0)(postcss@8.5.23)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.49.0 + webpack: 5.109.0(lightningcss@1.33.0)(postcss@8.5.23) + optionalDependencies: + lightningcss: 1.33.0 + postcss: 8.5.23 + + minipass@7.1.3: {} + + module-details-from-path@1.0.4: {} + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + next@16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@next/env': 16.2.12 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.11.1 + caniuse-lite: 1.0.30001806 + postcss: 8.5.23 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.7) + optionalDependencies: + '@next/swc-darwin-arm64': 16.2.12 + '@next/swc-darwin-x64': 16.2.12 + '@next/swc-linux-arm64-gnu': 16.2.12 + '@next/swc-linux-arm64-musl': 16.2.12 + '@next/swc-linux-x64-gnu': 16.2.12 + '@next/swc-linux-x64-musl': 16.2.12 + '@next/swc-win32-arm64-msvc': 16.2.12 + '@next/swc-win32-x64-msvc': 16.2.12 + '@opentelemetry/api': 1.9.1 + '@playwright/test': 1.62.0 + sharp: 0.35.3(@types/node@25.9.5) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + + nextjs-toploader@3.9.17(next@16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + next: 16.2.12(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.0)(@types/node@25.9.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + nprogress: 0.2.0 + prop-types: 15.8.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + node-exports-info@1.6.2: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-releases@2.0.51: {} + + nprogress@0.2.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 - object.values@1.2.1: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - obug@2.1.4: {} - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - own-keys@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-scurry@2.0.2: - dependencies: - lru-cache: 11.5.2 - minipass: 7.1.3 - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@2.3.2: {} - - picomatch@4.0.5: {} - - playwright-core@1.62.0: {} - - playwright@1.62.0: - dependencies: - playwright-core: 1.62.0 - optionalDependencies: - fsevents: 2.3.2 - - possible-typed-array-names@1.1.0: {} - - postcss@8.5.23: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - prettier@3.9.6: {} - - pretty-bytes@6.1.1: {} - - progress@2.0.3: {} - - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - - proxy-from-env@1.1.0: {} - - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} - - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 - - react-is@16.13.1: {} - - react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): - dependencies: - react: 19.2.7 - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - - react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7): - dependencies: - react: 19.2.7 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.7) - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) - tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) - use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - - react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7): - dependencies: - get-nonce: 1.0.1 - react: 19.2.7 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - - react@19.2.7: {} - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - - require-from-string@2.0.2: {} - - require-in-the-middle@8.0.1(supports-color@8.1.1): - dependencies: - debug: 4.4.3(supports-color@8.1.1) - module-details-from-path: 1.0.4 - transitivePeerDependencies: - - supports-color - - resolve-from@4.0.0: {} - - resolve-pkg-maps@1.0.0: {} - - resolve@2.0.0-next.7: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - node-exports-info: 1.6.2 - object-keys: 1.1.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.1.0: {} - - rolldown@1.1.5: - dependencies: - '@oxc-project/types': 0.139.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 - - rollup@4.62.2: - dependencies: - '@types/estree': 1.0.9 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.62.2 - '@rollup/rollup-android-arm64': 4.62.2 - '@rollup/rollup-darwin-arm64': 4.62.2 - '@rollup/rollup-darwin-x64': 4.62.2 - '@rollup/rollup-freebsd-arm64': 4.62.2 - '@rollup/rollup-freebsd-x64': 4.62.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 - '@rollup/rollup-linux-arm-musleabihf': 4.62.2 - '@rollup/rollup-linux-arm64-gnu': 4.62.2 - '@rollup/rollup-linux-arm64-musl': 4.62.2 - '@rollup/rollup-linux-loong64-gnu': 4.62.2 - '@rollup/rollup-linux-loong64-musl': 4.62.2 - '@rollup/rollup-linux-ppc64-gnu': 4.62.2 - '@rollup/rollup-linux-ppc64-musl': 4.62.2 - '@rollup/rollup-linux-riscv64-gnu': 4.62.2 - '@rollup/rollup-linux-riscv64-musl': 4.62.2 - '@rollup/rollup-linux-s390x-gnu': 4.62.2 - '@rollup/rollup-linux-x64-gnu': 4.62.2 - '@rollup/rollup-linux-x64-musl': 4.62.2 - '@rollup/rollup-openbsd-x64': 4.62.2 - '@rollup/rollup-openharmony-arm64': 4.62.2 - '@rollup/rollup-win32-arm64-msvc': 4.62.2 - '@rollup/rollup-win32-ia32-msvc': 4.62.2 - '@rollup/rollup-win32-x64-gnu': 4.62.2 - '@rollup/rollup-win32-x64-msvc': 4.62.2 - fsevents: 2.3.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safe-array-concat@1.1.4: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-buffer@5.2.1: {} - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - - safer-buffer@2.1.2: {} - - scheduler@0.27.0: {} - - schema-utils@4.3.3: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 8.20.0 - ajv-formats: 2.1.1(ajv@8.20.0) - ajv-keywords: 5.1.0(ajv@8.20.0) - - semifies@1.0.0: {} - - semver@6.3.1: {} - - semver@7.8.5: {} - - server-only@0.0.1: {} - - serwist@9.5.12(browserslist@4.28.6)(typescript@6.0.3): - dependencies: - '@serwist/utils': 9.5.12(browserslist@4.28.6) - idb: 8.0.3 - optionalDependencies: - typescript: 6.0.3 - transitivePeerDependencies: - - browserslist - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.8.5 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - optional: true - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - siginfo@2.0.0: {} - - simple-icons@16.27.1: {} - - sonner@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - - source-map-js@1.2.1: {} - - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - - source-map@0.8.0: {} - - stable-hash@0.0.5: {} - - stackback@0.0.2: {} - - stacktrace-parser@0.1.11: - dependencies: - type-fest: 0.7.1 - - std-env@4.2.0: {} - - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - - string.prototype.includes@2.0.1: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - - string.prototype.matchall@4.0.12: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.1 - - string.prototype.repeat@1.0.0: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.24.2 - - string.prototype.trim@1.2.11: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - has-property-descriptors: 1.0.2 - safe-regex-test: 1.1.0 - - string.prototype.trimend@1.0.10: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - strip-bom@3.0.0: {} - - strip-json-comments@3.1.1: {} - - styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.7): - dependencies: - client-only: 0.0.1 - react: 19.2.7 - optionalDependencies: - '@babel/core': 7.29.7(supports-color@8.1.1) - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - tagged-tag@1.0.0: {} - - tailwind-merge@3.6.0: {} - - tailwindcss@4.3.3: {} - - tapable@2.3.3: {} - - terser@5.49.0: - dependencies: - '@jridgewell/source-map': 0.3.11 - acorn: 8.17.0 - commander: 2.20.3 - source-map-support: 0.5.21 - - tinybench@2.9.0: {} - - tinyexec@1.2.4: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - - tinyrainbow@3.1.0: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - tr46@0.0.3: {} - - ts-api-utils@2.5.0(typescript@6.0.3): - dependencies: - typescript: 6.0.3 - - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - - tslib@2.8.1: {} - - tw-animate-css@1.4.0: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@0.7.1: {} - - type-fest@5.8.0: - dependencies: - tagged-tag: 1.0.0 - - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.8: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - typescript-eslint@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - typescript@6.0.3: {} - - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - - undici-types@7.24.6: {} - - unrs-resolver@1.12.2: - dependencies: - napi-postinstall: 0.3.4 - optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.12.2 - '@unrs/resolver-binding-android-arm64': 1.12.2 - '@unrs/resolver-binding-darwin-arm64': 1.12.2 - '@unrs/resolver-binding-darwin-x64': 1.12.2 - '@unrs/resolver-binding-freebsd-x64': 1.12.2 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 - '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 - '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 - '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 - '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 - '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 - '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 - '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 - '@unrs/resolver-binding-linux-x64-musl': 1.12.2 - '@unrs/resolver-binding-openharmony-arm64': 1.12.2 - '@unrs/resolver-binding-wasm32-wasi': 1.12.2 - '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 - '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 - '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - - update-browserslist-db@1.2.3(browserslist@4.28.6): - dependencies: - browserslist: 4.28.6 - escalade: 3.2.0 - picocolors: 1.1.1 - - update-browserslist-db@1.2.3(browserslist@4.28.7): - dependencies: - browserslist: 4.28.7 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): - dependencies: - react: 19.2.7 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - - use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): - dependencies: - detect-node-es: 1.1.0 - react: 19.2.7 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - - virtua@0.49.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - optionalDependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - - vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.23 - rolldown: 1.1.5 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 25.9.5 - fsevents: 2.3.3 - jiti: 2.7.0 - terser: 5.49.0 - - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.1 - expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@types/node': 25.9.5 - '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) - transitivePeerDependencies: - - msw - - watchpack@2.5.2: - dependencies: - graceful-fs: 4.2.11 - - web-push@3.6.7(supports-color@8.1.1): - dependencies: - asn1.js: 5.4.1 - http_ece: 1.2.0 - https-proxy-agent: 7.0.6(supports-color@8.1.1) - jws: 4.0.1 - minimist: 1.2.8 - transitivePeerDependencies: - - supports-color - - webidl-conversions@3.0.1: {} - - webpack-sources@3.5.1: {} - - webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23): - dependencies: - '@types/estree': 1.0.9 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - browserslist: 4.28.7 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.3 - es-module-lexer: 2.3.1 - eslint-scope: 5.1.1 - events: 3.3.0 - graceful-fs: 4.2.11 - mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(lightningcss@1.33.0)(postcss@8.5.23)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.3 - watchpack: 2.5.2 - webpack-sources: 3.5.1 - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - uglify-js - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.2.0 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.22 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.22: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - word-wrap@1.2.5: {} - - yallist@3.1.1: {} - - yocto-queue@0.1.0: {} - - zod-validation-error@4.0.2(zod@4.4.3): - dependencies: - zod: 4.4.3 - - zod@4.4.3: {} + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + obug@2.1.4: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.5: {} + + playwright-core@1.62.0: {} + + playwright@1.62.0: + dependencies: + playwright-core: 1.62.0 + optionalDependencies: + fsevents: 2.3.2 + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.9.6: {} + + pretty-bytes@6.1.1: {} + + progress@2.0.3: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): + dependencies: + react: 19.2.7 + react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7): + dependencies: + react: 19.2.7 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.7) + react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) + use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7): + dependencies: + get-nonce: 1.0.1 + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + react@19.2.7: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-from-string@2.0.2: {} + + require-in-the-middle@8.0.1(supports-color@8.1.1): + dependencies: + debug: 4.4.3(supports-color@8.1.1) + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.2 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + + semifies@1.0.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + server-only@0.0.1: {} + + serwist@9.5.12(browserslist@4.28.6)(typescript@6.0.3): + dependencies: + '@serwist/utils': 9.5.12(browserslist@4.28.6) + idb: 8.0.3 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - browserslist + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + sharp@0.35.3(@types/node@25.9.5): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 25.9.5 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + simple-icons@16.27.1: {} + + sonner@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.8.0: {} + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + stacktrace-parser@0.1.11: + dependencies: + type-fest: 0.7.1 + + std-env@4.2.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@8.1.1))(react@19.2.7): + dependencies: + client-only: 0.0.1 + react: 19.2.7 + optionalDependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tagged-tag@1.0.0: {} + + tailwind-merge@3.6.0: {} + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + terser@5.49.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.17.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tw-animate-css@1.4.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.7.1: {} + + type-fest@5.8.0: + dependencies: + tagged-tag: 1.0.0 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.65.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@8.1.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@7.24.6: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.2.3(browserslist@4.28.6): + dependencies: + browserslist: 4.28.6 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-browserslist-db@1.2.3(browserslist@4.28.7): + dependencies: + browserslist: 4.28.7 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): + dependencies: + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + virtua@0.49.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.23 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.5 + fsevents: 2.3.3 + jiti: 2.7.0 + terser: 5.49.0 + + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.5(@types/node@25.9.5)(jiti@2.7.0)(terser@5.49.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 25.9.5 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + transitivePeerDependencies: + - msw + + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + + web-push@3.6.7(supports-color@8.1.1): + dependencies: + asn1.js: 5.4.1 + http_ece: 1.2.0 + https-proxy-agent: 7.0.6(supports-color@8.1.1) + jws: 4.0.1 + minimist: 1.2.8 + transitivePeerDependencies: + - supports-color + + webidl-conversions@3.0.1: {} + + webpack-sources@3.5.1: {} + + webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23): + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.17.0 + browserslist: 4.28.7 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.3 + es-module-lexer: 2.3.1 + eslint-scope: 5.1.1 + events: 3.3.0 + graceful-fs: 4.2.11 + mime-db: 1.54.0 + minimizer-webpack-plugin: 5.6.1(lightningcss@1.33.0)(postcss@8.5.23)(webpack@5.109.0(lightningcss@1.33.0)(postcss@8.5.23)) + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + watchpack: 2.5.2 + webpack-sources: 3.5.1 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e9b6cdab..f394ed9b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,3 +16,7 @@ overrides: postcss: '>=8.5.10' simple-icons: '>=16.27.1' lucide-react: '>=1.27.0' + # Next réclame ^0.34.5, dont libvips traîne CVE-2026-33196. Le forçage est sans risque ici : + # `images.unoptimized` fait que sharp n'est jamais appelé, et il est déjà dans + # ignoredBuiltDependencies donc jamais compilé. + sharp: '>=0.35.0' From e111f27193671c7fce690204f4bebbc6391a16b3 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 15:25:21 +0200 Subject: [PATCH 96/98] =?UTF-8?q?fix(explorer):=20le=20bandeau=20=C3=A0=20?= =?UTF-8?q?la=20une=20ne=20d=C3=A9borde=20plus=20sur=20mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mesuré sur iPhone 13 : la card fait 191 px de haut, le bloc de contenu en faisait 260 avec le titre du jour et 296 avec un titre sur trois lignes. Le débordement était systématique, pas un cas limite — `overflow-hidden` le masquait en rognant le badge et le haut du titre. `text-4xl` déborde dès que le titre passe à deux lignes, ce qui arrive à partir de 23 caractères selon les glyphes. Un palier indexé sur la longueur de la chaîne serait donc faux : « WWW WWW WWW WWW WWW WWW » (23) tient sur deux lignes là où « Spider-Man: Brand New Day » (25) tient sur une. `text-3xl` plus un `line-clamp-2` couvre tous les cas mesurés — jusqu'à 75 caractères — avec 11 px de marge au pire, et reste plus lisible que le `text-2xl` qu'un palier aurait imposé aux titres longs. Le synopsis passe en `sm:` : sur 191 px il coûtait 52 px pour deux lignes illisibles. Espacements resserrés sur mobile uniquement, desktop inchangé (marge de 218 px, vérifiée dans Chrome). Vérifié dans les deux thèmes, mobile et desktop. 326 tests unitaires, 49 tests E2E. --- components/explorer/SpotlightPick.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/explorer/SpotlightPick.tsx b/components/explorer/SpotlightPick.tsx index 7b2553de..b6a7c0ac 100644 --- a/components/explorer/SpotlightPick.tsx +++ b/components/explorer/SpotlightPick.tsx @@ -51,20 +51,20 @@ export async function SpotlightPick({
-
- +
+ {badgeLabel} -

+

{item.title}

{item.overview && ( -

+

{item.overview}

)} - + {ctaLabel} From 2297456287065ca16e38c35cad1efaa4c1116897 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 23:03:38 +0200 Subject: [PATCH 97/98] =?UTF-8?q?fix(ci):=20sort=20le=20lockfile=20du=20p?= =?UTF-8?q?=C3=A9rim=C3=A8tre=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `prettier --check .` échouait sur pnpm-lock.yaml, et faisait tomber le job de formatage du CI depuis la mise à jour des dépendances — pnpm avait réécrit le fichier à son propre format. Le formater ne réglerait rien : pnpm le réécrit à chaque install, donc le CI redeviendrait rouge au prochain changement de dépendance, pour un diff de 300 ko à chaque fois. Prettier respecte déjà .gitignore, ce qui couvre .next, public/sw.js et les rapports de test ; seul le lockfile, versionné par nécessité, devait être écarté explicitement. `pnpm format` ne modifie aucun autre fichier du dépôt. --- .prettierignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..2f812e5d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Généré par pnpm, qui le réécrit à son propre format à chaque install : +# le passer sous Prettier produit un diff de 300 ko aussitôt annulé. +pnpm-lock.yaml From f26bb056f4a5d59f47f2f15e4f72f59c9dd3b5e8 Mon Sep 17 00:00:00 2001 From: SAWKIT Date: Sat, 1 Aug 2026 23:39:52 +0200 Subject: [PATCH 98/98] =?UTF-8?q?fix(ci):=20r=C3=A9pare=20l'upgrade=20Helm?= =?UTF-8?q?=20de=20l'ingress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux causes cumulées, constatées sur le cluster : - le Deployment de l'ingress porte des champs encore détenus par le field manager `kubectl-client-side-apply`, hérités de son installation manuelle. Helm refusait d'y toucher : `.spec.replicas`, les fieldRef POD_NAME et POD_NAMESPACE, et `resources.requests.memory`. `--force-conflicts` lève ces conflits sans recréer la ressource, donc sans coupure. - `--atomic` est déprécié depuis Helm v4 et aggravait la panne : il tentait un rollback vers une release réussie qui n'existait pas. L'historique montrait les révisions 6, 7 et 8 toutes en échec depuis le 7 juillet — l'edge tournait donc à un seul replica depuis, ce qui explique les coupures Cloudflare. `--rollback-on-failure` le remplace. Réparé en production : révision 9 `deployed`, deux replicas sur des nœuds distincts, PDB actif, aucune interruption pendant la bascule. --- .github/workflows/infra.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/infra.yml b/.github/workflows/infra.yml index f64a3399..5dc07fb0 100644 --- a/.github/workflows/infra.yml +++ b/.github/workflows/infra.yml @@ -37,9 +37,13 @@ jobs: echo "$KUBECONFIG_B64" | base64 -d > ~/.kube/config chmod 600 ~/.kube/config - # --atomic rollback l'edge si l'upgrade échoue. Le timeout large couvre le - # provisioning d'un nœud par le cluster autoscaler quand l'anti-affinité laisse - # le second replica en Pending. + # --force-conflicts : le Deployment de l'ingress porte des champs encore détenus par + # `kubectl-client-side-apply`, hérités de son installation manuelle. Sans lui, Helm + # refuse d'y toucher et l'upgrade échoue — c'est ce qui laissait la release en échec + # depuis juillet, donc l'edge à un seul replica. + # --rollback-on-failure remplace --atomic, déprécié depuis Helm v4. Le timeout large + # couvre le provisioning d'un nœud par l'autoscaler quand l'anti-affinité laisse le + # second replica en Pending. - name: Upgrade ingress-nginx run: | helm upgrade --install ingress-nginx ingress-nginx \ @@ -47,7 +51,7 @@ jobs: --version "$CHART_VERSION" \ --namespace ingress-nginx --create-namespace \ --values k8s/ingress-nginx-values.yaml \ - --atomic --timeout 10m + --force-conflicts --rollback-on-failure --timeout 10m # Le namespace reelmark est créé par k8s/app.yaml, donc par le workflow applicatif. # Sur un cluster neuf ce job peut tourner en premier : ne pas échouer pour ça.